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)
This commit is contained in:
jesse-ux
2026-10-28 09:45:06 +08:00
co-authored by Grok (agent loop)
parent 9d3f59a872
commit 16bae00615
4 changed files with 400 additions and 127 deletions
+126 -5
View File
@@ -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."
}
}
+112 -122
View File
@@ -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(中文星座名)
@@ -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'
@@ -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