From 16bae006158aad8b493362ce1a51391626a95dca Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Wed, 28 Oct 2026 09:45:06 +0800 Subject: [PATCH] fix(saham): apply Table 74 before legacy rows (BUG-1474, upstream cb8a1ca7) Directed-arc sign stays as BUG-1387. formula_traces remains the 36-row report list. Book rows are no longer overwritten by the old house approximation. Co-Authored-By: Grok (agent loop) --- references/saham_rules.json | 131 +++++++++- scripts/tajika.py | 234 +++++++++--------- ...t_upstream_sync9_saham_pvr_directed_arc.py | 90 +++++++ ...upstream_sync9_saham_table74_completion.py | 72 ++++++ 4 files changed, 400 insertions(+), 127 deletions(-) create mode 100644 tests/test_upstream_sync9_saham_pvr_directed_arc.py create mode 100644 tests/test_upstream_sync9_saham_table74_completion.py diff --git a/references/saham_rules.json b/references/saham_rules.json index 0f90dd94..f1d72571 100644 --- a/references/saham_rules.json +++ b/references/saham_rules.json @@ -1,5 +1,8 @@ { "schema_version": "1.0", + "source": "PVR Integrated Approach, section 28.8.1, Table 74, source pages 394-395", + "house_operand_profile": "pvr_equal_30_degrees_from_ascendant", + "house_operand_source": "PVR Example 121, page 396: house longitude retains the Ascendant degree in successive signs; use the traditional sign lord", "description": "Tajika Saham Formulas. Format: P1 - P2 + P3. If birth is at night, the formula often flips to P2 - P1 + P3 (depending on the specific Saham rule).", "sahams": { "Punya_Saham": { @@ -22,8 +25,8 @@ }, "Mitra_Saham": { "name_en": "Friends", - "formula_day": ["Jupiter", "Venus", "Ascendant"], - "formula_night": ["Venus", "Jupiter", "Ascendant"], + "formula_day": ["Jupiter", "Punya_Saham", "Venus"], + "formula_night": ["Punya_Saham", "Jupiter", "Venus"], "description": "Indicates friends and social circle." }, "Mahatmya_Saham": { @@ -34,10 +37,128 @@ }, "Gaurava_Saham": { "name_en": "Respect", - "formula_day": ["Jupiter", "Moon", "Ascendant"], - "formula_night": ["Moon", "Jupiter", "Ascendant"], + "formula_day": ["Jupiter", "Moon", "Sun"], + "formula_night": ["Moon", "Jupiter", "Sun"], "description": "Indicates respect and honor in society." }, + "Vivaha_Saham": { + "formula_day": ["Venus", "Saturn", "Ascendant"], + "formula_night": ["Saturn", "Venus", "Ascendant"] + }, + "Rajya_Saham": { + "formula_day": ["Saturn", "Sun", "Ascendant"], + "formula_night": ["Sun", "Saturn", "Ascendant"] + }, + "Satru_Saham": { + "formula_day": ["Mars", "Saturn", "Ascendant"], + "formula_night": ["Saturn", "Mars", "Ascendant"] + }, + "Putra_Saham": { + "formula_day": ["Jupiter", "Moon", "Ascendant"], + "formula_night": ["Moon", "Jupiter", "Ascendant"] + }, + "Bandhu_Saham": { + "formula_day": ["Mercury", "Moon", "Ascendant"], + "formula_night": ["Moon", "Mercury", "Ascendant"] + }, + "Matri_Saham": { + "formula_day": ["Moon", "Venus", "Ascendant"], + "formula_night": ["Venus", "Moon", "Ascendant"] + }, + "Pitri_Saham": { + "formula_day": ["Saturn", "Sun", "Ascendant"], + "formula_night": ["Sun", "Saturn", "Ascendant"] + }, + "Bhratri_Saham": { + "formula_day": ["Jupiter", "Saturn", "Ascendant"], + "formula_night": ["Jupiter", "Saturn", "Ascendant"] + }, + "Roga_Saham": { + "formula_day": ["Ascendant", "Moon", "Ascendant"], + "formula_night": ["Moon", "Ascendant", "Ascendant"] + }, + "Mrityu_Saham": { + "formula_day": ["House8Cusp", "Moon", "Ascendant"], + "formula_night": ["House8Cusp", "Moon", "Ascendant"] + }, + "Paradesa_Saham": { + "formula_day": ["House9Cusp", "House9Lord", "Ascendant"], + "formula_night": ["House9Cusp", "House9Lord", "Ascendant"] + }, + "Artha_Saham": { + "formula_day": ["House2Cusp", "House2Lord", "Ascendant"], + "formula_night": ["House2Cusp", "House2Lord", "Ascendant"] + }, + "Labha_Saham": { + "formula_day": ["House11Cusp", "House11Lord", "Ascendant"], + "formula_night": ["House11Cusp", "House11Lord", "Ascendant"] + }, + "Asha_Saham": { + "formula_day": ["Saturn", "Mars", "Ascendant"], + "formula_night": ["Mars", "Saturn", "Ascendant"] + }, + "Samartha_Saham": { + "formula_day": ["Mars", "House1Lord", "Ascendant"], + "formula_night": ["House1Lord", "Mars", "Ascendant"], + "mars_lagna_formula_day": ["Jupiter", "Mars", "Ascendant"], + "mars_lagna_formula_night": ["Mars", "Jupiter", "Ascendant"] + }, + "Jeeva_Saham": { + "formula_day": ["Saturn", "Jupiter", "Ascendant"], + "formula_night": ["Jupiter", "Saturn", "Ascendant"] + }, + "Kali_Saham": { + "formula_day": ["Jupiter", "Mars", "Ascendant"], + "formula_night": ["Mars", "Jupiter", "Ascendant"] + }, + "Sastra_Saham": { + "formula_day": ["Jupiter", "Saturn", "Mercury"], + "formula_night": ["Saturn", "Jupiter", "Mercury"] + }, + "Paradara_Saham": { + "formula_day": ["Venus", "Sun", "Ascendant"], + "formula_night": ["Sun", "Venus", "Ascendant"] + }, + "Vanik_Saham": { + "formula_day": ["Moon", "Mercury", "Ascendant"], + "formula_night": ["Mercury", "Moon", "Ascendant"] + }, + "Karyasiddhi_Saham": { + "formula_day": ["Saturn", "Sun", "SunSignLord"], + "formula_night": ["Saturn", "Moon", "MoonSignLord"] + }, + "Santapa_Saham": { + "formula_day": ["Saturn", "Moon", "House6Cusp"], + "formula_night": ["Moon", "Saturn", "House6Cusp"] + }, + "Sraddha_Saham": { + "formula_day": ["Venus", "Mars", "Ascendant"], + "formula_night": ["Mars", "Venus", "Ascendant"] + }, + "Preeti_Saham": { + "formula_day": ["Sastra_Saham", "Punya_Saham", "Ascendant"], + "formula_night": ["Punya_Saham", "Sastra_Saham", "Ascendant"] + }, + "Jadya_Saham": { + "formula_day": ["Mars", "Saturn", "Mercury"], + "formula_night": ["Saturn", "Mars", "Mercury"] + }, + "Vyapara_Saham": { + "formula_day": ["Mars", "Saturn", "Ascendant"], + "formula_night": ["Mars", "Saturn", "Ascendant"] + }, + "Jalapatana_Saham": { + "formula_day": ["Cancer15", "Saturn", "Ascendant"], + "formula_night": ["Saturn", "Cancer15", "Ascendant"] + }, + "Bandhana_Saham": { + "formula_day": ["Punya_Saham", "Saturn", "Ascendant"], + "formula_night": ["Saturn", "Punya_Saham", "Ascendant"] + }, + "Apamrityu_Saham": { + "formula_day": ["House8Cusp", "Mars", "Ascendant"], + "formula_night": ["Mars", "House8Cusp", "Ascendant"] + }, "Karma_Saham": { "name_en": "Action / Profession", "formula_day": ["Mars", "Mercury", "Ascendant"], @@ -46,6 +167,6 @@ } }, "exception_rules": { - "add_30_degrees": "For A - B + C, add 30 degrees when C does not lie on the forward zodiacal arc from B to A. PVR Integrated Approach section 28.8.1, source page 394." + "add_30_degrees": "For A - B + C, add 30 degrees if C does not lie on the forward zodiacal arc from B to A. C is the actual third operand, not necessarily Ascendant." } } diff --git a/scripts/tajika.py b/scripts/tajika.py index 43acd73b..b00b52e4 100644 --- a/scripts/tajika.py +++ b/scripts/tajika.py @@ -76,6 +76,22 @@ _PANCHAVARGIYA_RULES = _load_panchavargiya_rules() def _resolve_saham_operand(operand: str, planet_lons: Dict[str, float], asc_lon: float, computed: Dict[str, float]) -> float: if operand == 'Ascendant': return float(asc_lon) % 360 + if operand == 'Cancer15': + return 105.0 + if operand in ('SunSignLord', 'MoonSignLord'): + luminary = operand.removesuffix('SignLord') + sign = SIGNS[int((float(planet_lons[luminary]) % 360) // 30)] + return float(planet_lons[SIGN_LORDS[sign]]) % 360 + if operand.startswith('House') and operand.endswith(('Cusp', 'Lord')): + suffix = 'Cusp' if operand.endswith('Cusp') else 'Lord' + house = int(operand[5:-len(suffix)]) + if not 1 <= house <= 12: + raise ValueError('Saham house must be 1..12') + # PVR Example 121: equal-house cusps keep the Ascendant's degree. + cusp = (float(asc_lon) + 30 * (house - 1)) % 360 + if suffix == 'Cusp': + return cusp + return float(planet_lons[SIGN_LORDS[SIGNS[int(cusp // 30)]]]) % 360 if operand in computed: return float(computed[operand]) % 360 if operand in planet_lons: @@ -83,6 +99,15 @@ def _resolve_saham_operand(operand: str, planet_lons: Dict[str, float], asc_lon: raise KeyError(f"Unsupported saham operand: {operand}") +def _select_saham_formula(rule_name: str, asc_lon: float, is_day: bool) -> List: + rule = _SAHAM_RULES['sahams'][rule_name] + variant = 'day' if is_day else 'night' + if (rule_name == 'Samartha_Saham' + and SIGN_LORDS[SIGNS[int((float(asc_lon) % 360) // 30)]] == 'Mars'): + return rule['mars_lagna_formula_' + variant] + return rule['formula_' + variant] + + def _calc_formula_saham( rule_name: str, planet_lons: Dict[str, float], @@ -90,13 +115,13 @@ def _calc_formula_saham( is_day: bool, computed: Dict[str, float], ) -> float: - rule = _SAHAM_RULES['sahams'][rule_name] - formula = rule['formula_day'] if is_day else rule['formula_night'] + formula = _select_saham_formula(rule_name, asc_lon, is_day) first = _resolve_saham_operand(formula[0], planet_lons, asc_lon, computed) second = _resolve_saham_operand(formula[1], planet_lons, asc_lon, computed) third = _resolve_saham_operand(formula[2], planet_lons, asc_lon, computed) result = (third + (first - second)) % 360 # PVR §28.8.1, p394: in A-B+C, C must lie on the forward arc B -> A. + # Direction matches BUG-1387 (ce9fed15): do not flip it back. third_between_points = (third - second) % 360 <= (first - second) % 360 return result if third_between_points else (result + 30.0) % 360 @@ -109,15 +134,14 @@ def _formula_saham_trace( computed: Dict[str, float], ) -> Dict: """Calculate a rule-backed Saham while preserving its sensitive convention.""" - rule = _SAHAM_RULES['sahams'][rule_name] - formula = rule['formula_day'] if is_day else rule['formula_night'] + formula = _select_saham_formula(rule_name, asc_lon, is_day) first = _resolve_saham_operand(formula[0], planet_lons, asc_lon, computed) second = _resolve_saham_operand(formula[1], planet_lons, asc_lon, computed) third = _resolve_saham_operand(formula[2], planet_lons, asc_lon, computed) base_longitude = (third + (first - second)) % 360 third_between_points = (third - second) % 360 <= (first - second) % 360 exception_applied = not third_between_points - return { + trace = { 'longitude': (base_longitude + 30.0) % 360 if exception_applied else base_longitude, 'base_longitude': base_longitude, 'rule_name': rule_name, @@ -126,6 +150,9 @@ def _formula_saham_trace( 'zodiacal_order_exception_applied': exception_applied, 'rule_source': 'references/saham_rules.json', } + if any(str(operand).startswith('House') for operand in formula): + trace['house_operand_profile'] = _SAHAM_RULES['house_operand_profile'] + return trace def calc_muntha(birth_asc_idx: int, age: int) -> Dict: @@ -1627,199 +1654,147 @@ def calc_all_sahams(planet_lons: Dict[str, float], is_day = daynight['is_daytime'] - results = {'status': 'partial', 'daynight_evidence': daynight} + results = { + 'status': 'partial', + 'daynight_evidence': daynight, + 'formula_rule_source': 'references/saham_rules.json', + 'formula_rule_boundary': ( + 'All 36 rows of PVR Table 74 are calculated under its declared day/night ' + 'and directed-arc rules. Additional legacy rows remain separate candidates, ' + 'not established classical formulas or vendor equivalents.' + ), + } computed_formula_sahams: Dict[str, float] = {} + formula_meta: Dict[str, Dict] = {} + # Table 74 rows. Legacy approximations below must not overwrite these keys. + for rule_name, result_key, label, interpretation in ( + ('Punya_Saham', 'punya_saham', 'Punya Saham', '福德点——善业、精神成长、父系福德'), + ('Vidya_Saham', 'vidya_saham', 'Vidya Saham', '学识点——学习、智力、教育'), + ('Yashas_Saham', 'yasha_saham', 'Yasha Saham', '名声点——声誉、社会地位'), + ('Mitra_Saham', 'mitra_saham', 'Mitra Saham', '友伴点——朋友、社交圈'), + ('Mahatmya_Saham', 'mahatmya_saham', 'Mahatmya Saham', '卓越点——成就、影响力'), + ('Gaurava_Saham', 'gaurava_saham', 'Gaurava Saham', '尊荣点——尊重、社会声望'), + ('Karma_Saham', 'karma_saham', 'Karma Saham', '行动点——职业、行动与责任'), + ('Vivaha_Saham', 'vivah_saham', 'Vivaha Saham', '婚姻点'), + ('Rajya_Saham', 'rajya_saham', 'Rajya Saham', '王权点'), + ('Satru_Saham', 'shatru_saham', 'Satru Saham', '敌人点'), + ('Putra_Saham', 'putra_saham', 'Putra Saham', '子女点'), + ('Bandhu_Saham', 'bandhu_saham', 'Bandhu Saham', '亲属点'), + ('Matri_Saham', 'matri_saham', 'Matri Saham', '母亲点'), + ('Pitri_Saham', 'pitri_saham', 'Pitri Saham', '父亲点'), + ('Bhratri_Saham', 'bhraatri_saham', 'Bhratri Saham', '兄弟姐妹点'), + ('Roga_Saham', 'rogha_saham', 'Roga Saham', '疾病点'), + ('Mrityu_Saham', 'mrityu_saham', 'Mrityu Saham', '终结点'), + ('Paradesa_Saham', 'paradesa_saham', 'Paradesa Saham', '海外点'), + ('Artha_Saham', 'artha_saham', 'Artha Saham', '财富点'), + ('Labha_Saham', 'labha_saham', 'Labha Saham', '收益点'), + ('Asha_Saham', 'asha_saham', 'Asha Saham', '愿望点'), + ('Samartha_Saham', 'samartha_saham', 'Samartha Saham', '能力与事业开拓点'), + ('Jeeva_Saham', 'jeeva_saham', 'Jeeva Saham', '生命点'), + ('Kali_Saham', 'kali_saham', 'Kali Saham', '传统逆境点'), + ('Sastra_Saham', 'sastra_saham', 'Sastra Saham', '学科点;不同于旧候选Shastra武器点'), + ('Paradara_Saham', 'paradara_saham', 'Paradara Saham', '传统婚外关系象征点'), + ('Vanik_Saham', 'vanik_saham', 'Vanik Saham', '商业贸易点'), + ('Karyasiddhi_Saham', 'karyasiddhi_saham', 'Karyasiddhi Saham', '事务达成点'), + ('Santapa_Saham', 'santapa_saham', 'Santapa Saham', '忧虑点'), + ('Sraddha_Saham', 'sraddha_saham', 'Sraddha Saham', '虔诚与诚意点'), + ('Preeti_Saham', 'preeti_saham', 'Preeti Saham', '情感依恋点'), + ('Jadya_Saham', 'jadya_saham', 'Jadya Saham', '传统慢性病象征点'), + ('Vyapara_Saham', 'vyapara_saham', 'Vyapara Saham', '经营点'), + ('Jalapatana_Saham', 'jalapatana_saham', 'Jalapatana Saham', '渡海点'), + ('Bandhana_Saham', 'bandhana_saham', 'Bandhana Saham', '束缚点'), + ('Apamrityu_Saham', 'apamrityu_saham', 'Apamrityu Saham', '传统意外终结象征点'), + ): + trace = _formula_saham_trace(rule_name, planet_lons, asc_lon, is_day, computed_formula_sahams) + computed_formula_sahams[rule_name] = trace['longitude'] + formula_meta[result_key] = { + 'rule_name': trace.get('rule_name'), + 'formula_operands': trace.get('formula_operands'), + 'formula_variant': trace.get('formula_variant'), + 'zodiacal_order_exception_applied': trace.get('zodiacal_order_exception_applied'), + } + results[result_key] = _saham_dict(trace['longitude'], label, interpretation, trace=trace) - # 1. Punya Saham(福德点):Moon - Sun + Asc - p_lon = _calc_formula_saham('Punya_Saham', planet_lons, asc_lon, is_day, computed_formula_sahams) - computed_formula_sahams['Punya_Saham'] = p_lon - results['punya_saham'] = _saham_dict(p_lon, 'Punya Saham', '福德点——善业、精神成长、父系福德') - - # 2. Karya Saham(事业点): - # 日间:Mars - Moon + Asc;夜间:Moon - Mars + Asc + # Legacy candidates that are not Table 74 names. Shastra stays distinct from Sastra. if is_day: k_lon = _saham(moon_lon, mars_lon) else: k_lon = _saham(mars_lon, moon_lon) results['karya_saham'] = _saham_dict(k_lon, 'Karya Saham', '事业点——行动、努力、工作成果') - # 3. Vivah Saham(婚姻点): - # 日间:Venus - Mars + Asc(男性)或 Mars - Venus + Asc(女性) - # 简化:用Venus - Mars - v_lon = _saham(mars_lon, venus_lon) - results['vivah_saham'] = _saham_dict(v_lon, 'Vivah Saham', '婚姻点——婚姻、伴侣、情感关系') - - # 4. Rajya Saham(王权点):Sun - Jupiter + Asc - r_lon = _saham(jupiter_lon, sun_lon) - results['rajya_saham'] = _saham_dict(r_lon, 'Rajya Saham', '王权点——地位、权威、社会认可') - - # 5. Shatru Saham(敌人点):Mars - Saturn + Asc - sh_lon = _saham(saturn_lon, mars_lon) - results['shatru_saham'] = _saham_dict(sh_lon, 'Shatru Saham', '敌人点——竞争对手、障碍、冲突来源') - - # 6. Labha Saham(收益点):Jupiter - Moon + Asc - l_lon = _saham(moon_lon, jupiter_lon) - results['labha_saham'] = _saham_dict(l_lon, 'Labha Saham', '收益点——收获、增益、扩张') - - # 7. Parakrama Saham(勇气点):Mars - Sun + Asc pa_lon = _saham(sun_lon, mars_lon) results['parakrama_saham'] = _saham_dict(pa_lon, 'Parakrama Saham', '勇气点——勇气、竞争力、克服困难') - # 8-36. 扩展Sahams(Tajika系统36种) - # 8. Putra Saham(子女点):Jupiter - Moon + Asc - putra_lon = _saham(moon_lon, jupiter_lon) - results['putra_saham'] = _saham_dict(putra_lon, 'Putra Saham', '子女点——子女、后代、创造力') - - # 9. Jnana Saham(教育点):Jupiter - Mercury + Asc mercury_lon = planet_lons.get('Mercury', 0) jnana_lon = _saham(mercury_lon, jupiter_lon) results['jnana_saham'] = _saham_dict(jnana_lon, 'Jnana Saham', '教育点——学习、知识、智慧') - # 10. Raja Saham(王权):Sun - Moon + Asc raja_lon = _saham(moon_lon, sun_lon) results['raja_saham'] = _saham_dict(raja_lon, 'Raja Saham', '王权点——领导力、权威') - # 11. Yasha Saham(名声点):Jupiter - Sun + Asc - try: - yasha_lon = _calc_formula_saham('Yashas_Saham', planet_lons, asc_lon, is_day, computed_formula_sahams) - computed_formula_sahams['Yashas_Saham'] = yasha_lon - except KeyError: - yasha_lon = _saham(sun_lon, jupiter_lon) - results['yasha_saham'] = _saham_dict(yasha_lon, 'Yasha Saham', '名声点——声誉、社会地位') - - # 12. Karma Saham(业力点):Saturn - Mars + Asc - try: - karma_lon = _calc_formula_saham('Karma_Saham', planet_lons, asc_lon, is_day, computed_formula_sahams) - computed_formula_sahams['Karma_Saham'] = karma_lon - except KeyError: - karma_lon = _saham(mars_lon, saturn_lon) - results['karma_saham'] = _saham_dict(karma_lon, 'Karma Saham', '业力点——前世因果、责任') - - # 13. Bandhu Saham(兄弟点):Mars - Mercury + Asc - bandhu_lon = _saham(mercury_lon, mars_lon) - results['bandhu_saham'] = _saham_dict(bandhu_lon, 'Bandhu Saham', '兄弟点——兄弟姐妹、同辈') - - # 14. Matri Saham(母亲点):Moon - Venus + Asc - matri_lon = _saham(venus_lon, moon_lon) - results['matri_saham'] = _saham_dict(matri_lon, 'Matri Saham', '母亲点——母亲、养育、情感滋养') - - # 15. Pitri Saham(父亲点):Sun - Saturn + Asc - pitri_lon = _saham(saturn_lon, sun_lon) - results['pitri_saham'] = _saham_dict(pitri_lon, 'Pitri Saham', '父亲点——父亲、权威、传承') - - # 16. Janma Saham(出生点):出生时刻敏感点 - j_lon = _saham(sun_lon, asc_lon) # Special: Asc based + j_lon = _saham(sun_lon, asc_lon) results['janma_saham'] = _saham_dict(j_lon, 'Janma Saham', '出生点——生命起点、先天禀赋') - # 17. Mrityu Saham(死亡点):Saturn出生 - 8宫主 + Asc - m_lon = _saham(mars_lon, saturn_lon) # 近似 - results['mrityu_saham'] = _saham_dict(m_lon, 'Mrityu Saham', '死亡点——寿命、终结、转变') - - # 18. Rogha Saham(疾病点):Mercury - Saturn + Asc - rogha_lon = _saham(saturn_lon, mercury_lon) - results['rogha_saham'] = _saham_dict(rogha_lon, 'Rogha Saham', '疾病点——健康、疾病倾向') - - # 19. Aarogya Saham(健康点):Jupiter - Saturn + Asc aarogya_lon = _saham(saturn_lon, jupiter_lon) results['aarogya_saham'] = _saham_dict(aarogya_lon, 'Aarogya Saham', '健康点——康复、旺盛精力') - # 20. Bhraatri Saham(同辈点):Mars - Moon + Asc - bhratr_lon = _saham(moon_lon, mars_lon) - results['bhraatri_saham'] = _saham_dict(bhratr_lon, 'Bhraatri Saham', '同辈点——手足、社交圈') - - # 21. Ghataka Saham(冲突点):Rahu - Mars + Asc rahu_lon = planet_lons.get('Rahu', 0) ghataka_lon = _saham(mars_lon, rahu_lon) results['ghataka_saham'] = _saham_dict(ghataka_lon, 'Ghataka Saham', '冲突点——意外、冲击、突袭') - # 22. Paradesa Saham(海外点):Saturn - Moon + Asc - paradesa_lon = _saham(moon_lon, saturn_lon) - results['paradesa_saham'] = _saham_dict(paradesa_lon, 'Paradesa Saham', '海外点——出国、异域、远方') - - # 23. Parabharya Saham(配偶点):Venus - Mars + Asc(女性盘) parab_lon = _saham(mars_lon, venus_lon) results['parabharya_saham'] = _saham_dict(parab_lon, 'Parabharya Saham', '配偶点——伴侣特质') - # 24. Dhan Saham(财富点):Jupiter - Venus + Asc dhan_lon = _saham(venus_lon, jupiter_lon) results['dhan_saham'] = _saham_dict(dhan_lon, 'Dhan Saham', '财富点——金钱、资产、物质') - # 25. Maya Saham(幻象点):Rahu - Ketu + Asc ketu_lon = planet_lons.get('Ketu', 0) maya_lon = _saham(ketu_lon, rahu_lon) results['maya_saham'] = _saham_dict(maya_lon, 'Maya Saham', '幻象点——迷惑、欺骗、直觉') - # 26. Moksha Saham(解脱点):Ketu - Jupiter + Asc moksha_lon = _saham(jupiter_lon, ketu_lon) results['moksha_saham'] = _saham_dict(moksha_lon, 'Moksha Saham', '解脱点——灵性、开悟、超越') - # 27. Buddha Saham(智慧点):Mercury - Jupiter + Asc buddha_lon = _saham(jupiter_lon, mercury_lon) results['buddha_saham'] = _saham_dict(buddha_lon, 'Buddha Saham', '智慧点——智力、逻辑、语言') - # 28. Shastra Saham(武器点):Mars - Ketu + Asc shastra_lon = _saham(ketu_lon, mars_lon) results['shastra_saham'] = _saham_dict(shastra_lon, 'Shastra Saham', '武器点——攻击性、防御、技术') - # 29. Kala Saham(时间点):Saturn - Sun + Asc kala_lon = _saham(sun_lon, saturn_lon) results['kala_saham'] = _saham_dict(kala_lon, 'Kala Saham', '时间点——时机、节奏、耐心') - # 30. Shakti Saham(力量点):Venus - Moon + Asc shakti_lon = _saham(moon_lon, venus_lon) results['shakti_saham'] = _saham_dict(shakti_lon, 'Shakti Saham', '力量点——女性力量、魅力、创造力') - # 31. Bhrigu Saham(先知点):Venus - Jupiter + Asc bhrigu_lon = _saham(jupiter_lon, venus_lon) results['bhrigu_saham'] = _saham_dict(bhrigu_lon, 'Bhrigu Saham', '先知点——直觉、预见、智慧传承') - # 32. Sundara Saham(美点):Venus - Mercury + Asc sundara_lon = _saham(mercury_lon, venus_lon) results['sundara_saham'] = _saham_dict(sundara_lon, 'Sundara Saham', '美点——艺术、美感、和谐') - # 33. Jnati Saham(亲戚点):Mars - Saturn + Asc jnati_lon = _saham(saturn_lon, mars_lon) results['jnati_saham'] = _saham_dict(jnati_lon, 'Jnati Saham', '亲戚点——宗族、家族关系') - # 34. Artha Saham(财富点):Sun - Venus + Asc - artha_lon = _saham(venus_lon, sun_lon) - results['artha_saham'] = _saham_dict(artha_lon, 'Artha Saham', '财富点——物质繁荣、经济') - - # 35. Dharma Saham(正法点):Jupiter - Saturn + Asc dharma_lon = _saham(saturn_lon, jupiter_lon) results['dharma_saham'] = _saham_dict(dharma_lon, 'Dharma Saham', '正法点——正义、道德、人生使命') - # 36. Sangrama Saham(战斗点):Mars - Rahu + Asc sangrama_lon = _saham(rahu_lon, mars_lon) results['sangrama_saham'] = _saham_dict(sangrama_lon, 'Sangrama Saham', '战斗点——竞争、挑战、胜利') - formula_meta: Dict[str, Dict] = {} - for result_key, rule_name in ( - ('punya_saham', 'Punya_Saham'), - ('yasha_saham', 'Yashas_Saham'), - ('karma_saham', 'Karma_Saham'), - ): - try: - traced = _formula_saham_trace(rule_name, planet_lons, asc_lon, is_day, computed_formula_sahams) - except KeyError: - continue - formula_meta[result_key] = { - 'rule_name': traced.get('rule_name'), - 'formula_operands': traced.get('formula_operands'), - 'formula_variant': traced.get('formula_variant'), - 'zodiacal_order_exception_applied': traced.get('zodiacal_order_exception_applied'), - } try: from annual_native.saham_traces import attach_formula_traces except ImportError: from scripts.annual_native.saham_traces import attach_formula_traces + # Report chapters still read a list of {key, rule, longitude}. Table 74 + # numbers live on the named rows; this list keeps that export shape. attach_formula_traces(results, is_day=is_day, formula_meta=formula_meta) return results -def _saham_dict(lon: float, name: str, interp: str) -> Dict: +def _saham_dict(lon: float, name: str, interp: str, *, trace: Optional[Dict] = None) -> Dict: """构建Saham字典""" - return { + result = { 'longitude': round(lon, 4), 'sign': SIGNS[int(lon / 30) % 12], 'sign_cn': SIGNS_CN.get(SIGNS[int(lon / 30) % 12], ''), @@ -1827,6 +1802,21 @@ def _saham_dict(lon: float, name: str, interp: str) -> Dict: 'name': name, 'interpretation': interp, } + if trace: + result.update({ + 'formula_status': 'rule_backed_parameter_sensitive', + 'formula_rule': trace['rule_name'], + 'formula_variant': trace['formula_variant'], + 'formula_operands': list(trace['formula_operands']), + 'zodiacal_order_exception_applied': trace['zodiacal_order_exception_applied'], + 'base_longitude_before_exception': round(trace['base_longitude'], 4), + 'formula_source': trace['rule_source'], + }) + if 'house_operand_profile' in trace: + result['house_operand_profile'] = trace['house_operand_profile'] + else: + result['formula_status'] = 'legacy_candidate_unverified' + return result # 补充SIGNS_CN(中文星座名) diff --git a/tests/test_upstream_sync9_saham_pvr_directed_arc.py b/tests/test_upstream_sync9_saham_pvr_directed_arc.py new file mode 100644 index 00000000..d01788ca --- /dev/null +++ b/tests/test_upstream_sync9_saham_pvr_directed_arc.py @@ -0,0 +1,90 @@ +import pytest +from datetime import datetime +from scripts.tajika import _calc_formula_saham, _formula_saham_trace + + +@pytest.mark.parametrize('is_day,expected', [ + (True, [280, 295, 160, 100, 310, 280, 295, 250]), + (False, [130, 115, 250, 310, 100, 130, 115, 250]), +]) +def test_extended_table_74_rows_survive_full_producer(monkeypatch, is_day, expected): + import saham_daynight + from scripts.tajika import calc_all_sahams + monkeypatch.setattr(saham_daynight, 'determine_daytime', + lambda *a, **k: {'is_daytime': is_day}) + planets = {'Sun': 45., 'Moon': 120., 'Mars': 90., 'Mercury': 60., + 'Jupiter': 180., 'Venus': 210., 'Saturn': 300.} + result = calc_all_sahams(planets, 10., datetime(2000, 1, 1), lat=0, lon=0, tz=0) + keys = ['vivah', 'rajya', 'shatru', 'putra', 'bandhu', 'matri', 'pitri', 'bhraatri'] + for key, longitude in zip(keys, expected): + row = result[key + '_saham'] + assert row['longitude'] == longitude, key + assert row['formula_status'] == 'rule_backed_parameter_sensitive' + + +@pytest.mark.parametrize('sun,moon,asc,expected,exception', [ + (45, 120, 10, 115, True), # C outside Sun -> Moon. + (45, 120, 60, 135, False), + (200, 100, 50, 310, False), # Wrap through zero; C is inside. + (200, 100, 150, 80, True), +]) +def test_punya_arc_runs_from_subtracted_to_added_operand(sun, moon, asc, expected, exception): + points = {'Sun': sun, 'Moon': moon} + assert _calc_formula_saham('Punya_Saham', points, asc, True, {}) == expected + trace = _formula_saham_trace('Punya_Saham', points, asc, True, {}) + assert trace['longitude'] == expected + assert trace['zodiacal_order_exception_applied'] is exception + + +def test_mitra_uses_punya_and_venus_not_venus_and_ascendant(): + # Table 74: Jupiter - Punya + Venus = 200 - 50 + 100 = 250. + trace = _formula_saham_trace('Mitra_Saham', {'Jupiter': 200, 'Venus': 100}, + 300, True, {'Punya_Saham': 50}) + assert trace['formula_operands'] == ['Jupiter', 'Punya_Saham', 'Venus'] + assert trace['longitude'] == 250 + assert not trace['zodiacal_order_exception_applied'] + + +def test_gaurava_keeps_sun_as_third_operand_at_night(): + trace = _formula_saham_trace('Gaurava_Saham', {'Jupiter': 200, 'Moon': 50, 'Sun': 100}, + 300, False, {}) + assert trace['formula_operands'] == ['Moon', 'Jupiter', 'Sun'] + assert trace['longitude'] == 340 + assert trace['zodiacal_order_exception_applied'] + + +def test_pvr_printed_example_121_artha_arithmetic(monkeypatch): + import scripts.tajika as producer + monkeypatch.setitem(producer._SAHAM_RULES['sahams'], 'Printed_Artha', { + 'formula_day': ['SecondHouse', 'Saturn', 'Ascendant'], + 'formula_night': ['SecondHouse', 'Saturn', 'Ascendant']}) + actual = producer._calc_formula_saham('Printed_Artha', + {'SecondHouse': 310 + 50/60, 'Saturn': 19 + 10/60}, 280 + 50/60, False, {}) + assert actual == pytest.approx(212.5) + + +def test_pvr_artha_uses_actual_second_house_and_lord_day_and_night(): + # Printed example 121: Asc 10Cp50, 2nd house 10Aq50, lord Saturn 19Ar10. + for is_day in (True, False): + trace = _formula_saham_trace('Artha_Saham', {'Saturn': 19 + 10/60}, + 280 + 50/60, is_day, {}) + assert trace['longitude'] == pytest.approx(212.5) + assert trace['house_operand_profile'] == 'pvr_equal_30_degrees_from_ascendant' + + +@pytest.mark.parametrize('day,expected', [(True, 260.), (False, 120.)]) +def test_roga_uses_lagna_and_moon_not_mercury_and_saturn(day, expected): + trace = _formula_saham_trace('Roga_Saham', {'Moon': 120}, 10, day, {}) + assert trace['longitude'] == expected + + +def test_source_house_formulas_are_not_overwritten_by_legacy_approximations(monkeypatch): + import saham_daynight + from scripts.tajika import calc_all_sahams + monkeypatch.setattr(saham_daynight, 'determine_daytime', lambda *a, **k: {'is_daytime': True}) + planets = {'Sun':45.,'Moon':120.,'Mars':90.,'Mercury':60.,'Jupiter':180.,'Venus':210.,'Saturn':300.} + result = calc_all_sahams(planets, 10., datetime(2000, 1, 1), lat=0, lon=0, tz=0) + # House8=220, House9=250 (Jupiter), House2=40 (Venus), House11=310 (Saturn). + for name, expected in [('mrityu',140.),('paradesa',110.),('artha',200.),('labha',50.)]: + assert result[name+'_saham']['longitude'] == expected + assert result[name+'_saham']['formula_status'] == 'rule_backed_parameter_sensitive' diff --git a/tests/test_upstream_sync9_saham_table74_completion.py b/tests/test_upstream_sync9_saham_table74_completion.py new file mode 100644 index 00000000..ad2f7ea7 --- /dev/null +++ b/tests/test_upstream_sync9_saham_table74_completion.py @@ -0,0 +1,72 @@ +"""PVR Table74 pp394–395 and printed Example121 p396; synthetic inputs.""" +import pytest +from scripts.tajika import _formula_saham_trace + + +def test_printed_example_121_samartha_and_vanik(): + planets={'Saturn':19+10/60,'Mars':354+58/60, + 'Moon':345+14/60,'Mercury':311+28/60} + asc=280+50/60 + assert _formula_saham_trace('Samartha_Saham',planets,asc,False,{})['longitude']==pytest.approx(335+2/60) + assert _formula_saham_trace('Vanik_Saham',planets,asc,False,{})['longitude']==pytest.approx(247+4/60) + + +@pytest.mark.parametrize('day,expected',[(True,130),(False,280)]) +def test_samartha_mars_lagna_exception(day,expected): + trace=_formula_saham_trace('Samartha_Saham',{'Mars':90,'Jupiter':180},10,day,{}) + assert trace['longitude']==expected + assert trace['formula_operands']==(['Jupiter','Mars','Ascendant'] if day else ['Mars','Jupiter','Ascendant']) + + +def test_karyasiddhi_night_changes_luminary_and_its_sign_lord_not_a_b_reversal(): + planets={'Sun':45,'Moon':120,'Saturn':300,'Venus':210} + day=_formula_saham_trace('Karyasiddhi_Saham',planets,10,True,{}) + night=_formula_saham_trace('Karyasiddhi_Saham',planets,10,False,{}) + assert day['longitude']==105 + assert night['longitude']==255 + assert night['formula_operands']==['Saturn','Moon','MoonSignLord'] + + +def test_all_36_book_rows_reach_full_producer_without_replacing_legacy_names(monkeypatch): + from datetime import datetime + import saham_daynight + from scripts.tajika import calc_all_sahams + monkeypatch.setattr(saham_daynight,'determine_daytime',lambda *a,**k:{'is_daytime':True}) + planets=dict(Sun=45.,Moon=120.,Mars=90.,Mercury=60.,Jupiter=180.,Venus=210.,Saturn=300.,Rahu=0.,Ketu=180.) + result=calc_all_sahams(planets,10.,datetime(2000,1,1),lat=0,lon=0,tz=0) + assert len(result['formula_traces'])==36 + assert result['sastra_saham']['formula_status']=='rule_backed_parameter_sensitive' + assert result['shastra_saham']['formula_status']=='legacy_candidate_unverified' + assert result['sastra_saham']['longitude']!=result['shastra_saham']['longitude'] + from scripts.pl9_full_data_export import _render_pl9_ai_density_raw_data_markdown_en + text=_render_pl9_ai_density_raw_data_markdown_en({ + 'advanced_systems':{'sahams':result}}) + delivered={c[0]:c for line in text.splitlines() if line.startswith('| ') + for c in [[v.strip() for v in line.strip().strip('|').split('|')]] + if len(c)==4} + for key,row in result.items(): + if not isinstance(row,dict) or row.get('formula_status')!='rule_backed_parameter_sensitive':continue + assert delivered[key][1]==row['sign'] + assert float(delivered[key][2])==row['degree_in_sign'] + assert float(delivered[key][3])==row['longitude'] + + +@pytest.mark.parametrize('day,column',[(True,0),(False,1)]) +def test_remaining_table74_day_night_golden_values(monkeypatch,day,column): + # Independent hand arithmetic of Table74 with integer longitudes. + # Includes the actual third operand, the Mars-owned-lagna exception, + # non-reversing Vyapara and the independently calculated Sastra/Punya. + from datetime import datetime + import saham_daynight + from scripts.tajika import calc_all_sahams + monkeypatch.setattr(saham_daynight,'determine_daytime',lambda *a,**k:{'is_daytime':day}) + planets=dict(Sun=45.,Moon=120.,Mars=90.,Mercury=60.,Jupiter=180.,Venus=210.,Saturn=300.,Rahu=0.,Ketu=180.) + result=calc_all_sahams(planets,10.,datetime(2000,1,1),lat=0,lon=0,tz=0) + expected={'asha':(250,160),'samartha':(130,280),'jeeva':(160,250), + 'kali':(130,280),'sastra':(300,210),'paradara':(205,205), + 'vanik':(100,310),'karyasiddhi':(105,255),'santapa':(340,10), + 'sraddha':(160,250),'preeti':(225,125),'jadya':(210,300), + 'vyapara':(160,160),'jalapatana':(175,235),'bandhana':(185,45), + 'apamrityu':(170,240)} + for name,values in expected.items(): + assert result[name+'_saham']['longitude']==values[column],name