feat: migrate precise Shadbala parity

This commit is contained in:
732642856
2026-07-17 19:39:45 +08:00
parent 064f985f3a
commit f2c4a0d915
16 changed files with 14364 additions and 173 deletions
@@ -197,6 +197,15 @@ def build_pyjhora_sample(sample, *, node_mode='mean'):
'sav': list(sav),
},
'shadbala': {name: float(shadbala[6][index]) for index, name in enumerate(['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn'])},
'shadbala_components': {
name: {
component: float(shadbala[row_index][index])
for component, row_index in {
'sthana': 0, 'kala': 1, 'dig': 2, 'chesta': 3, 'naisargika': 4, 'drik': 5,
}.items()
}
for index, name in enumerate(['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn'])
},
'dasha': dasha,
}
@@ -276,6 +285,13 @@ def compare_one(sample_id, local, pyjhora):
rows, sample_id, 'Shadbala', planet, 'total_virupas',
local.get('shadbala', {}).get(planet), pyjhora.get('shadbala', {}).get(planet), tolerance=0.5,
)
for component in ['sthana', 'kala', 'dig', 'chesta', 'naisargika', 'drik']:
compare_scalar(
rows, sample_id, 'Shadbala_Component', planet, component,
local.get('shadbala_components', {}).get(planet, {}).get(component),
pyjhora.get('shadbala_components', {}).get(planet, {}).get(component),
tolerance=0.5,
)
# PyJHora dasha is useful as external signal, but currently has different default starting convention/seed in some cases.
# Keep fields in matrix, with generous date tolerance; differences are classified below in report.
for field in ['mahadasha_lord', 'antardasha_lord']:
@@ -111,6 +111,17 @@ def canonicalize(sample, data):
'sav': [safe_get(modules, 'ashtakavarga', 'sav', 'scores', sign) for sign in ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', 'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces']],
},
'shadbala': {name: safe_get(modules, 'shadbala', 'planets', name, 'total_virupas') for name in PLANETS[:7]},
'shadbala_components': {
name: {
'sthana': safe_get(modules, 'shadbala', 'planets', name, 'sthana_bala', 'total'),
'kala': safe_get(modules, 'shadbala', 'planets', name, 'kala_bala', 'total'),
'dig': safe_get(modules, 'shadbala', 'planets', name, 'dig_bala'),
'chesta': safe_get(modules, 'shadbala', 'planets', name, 'chesta_bala'),
'naisargika': safe_get(modules, 'shadbala', 'planets', name, 'naisargika_bala'),
'drik': safe_get(modules, 'shadbala', 'planets', name, 'drik_bala'),
}
for name in PLANETS[:7]
},
'dasha': {
'mahadasha_lord': current.get('lord'),
'mahadasha_start': current.get('start'),
@@ -0,0 +1,38 @@
# Shadbala High-Rigor Migration Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Port the reviewed `f3bae51` Shadbala component-formula correction and its public high-rigor parity evidence from research to the commercial backend without importing research worktree residue.
**Architecture:** Use the committed research tree at `f3bae51` as the only source. First add the precise-context regression to commercial and observe failure; then port the smallest calculation/API/engine changes needed for it. Public Steve Jobs artifacts and the parity manifest move only after privacy and hash checks.
**Tech Stack:** Python 3, pytest, existing Swiss Ephemeris/Jyotish engine, JSON evidence manifests.
### Task 1: Port formula regression first
**Files:**
- Create: `tests/test_shadbala_precise_context.py`
- Modify: `scripts/shadbala.py`
- Modify: `scripts/jyotish_engine.py`
- Modify: `scripts/jyotish_api_server.py`
- [ ] Copy the committed regression test from `f3bae51`; run it in commercial and observe failure.
- [ ] Diff the source commit against its parent, then port only paths exercised by the failing test.
- [ ] Run the precise-context test plus existing Shadbala tests.
### Task 2: Port public high-rigor evidence
**Files:**
- Create: `references/oracle/artifacts/jyotishganit_steve_jobs_high_rigor_raw.json`
- Create: `references/oracle/artifacts/local_steve_jobs_high_rigor_raw.json`
- Modify: `references/oracle/three_engine_parity_replay_manifest.json`
- [ ] Verify artifacts have no secret/private input and are referenced by the committed parity manifest.
- [ ] Copy exact committed public files, then run parity artifact-contract and replay-validator tests.
### Task 3: Verify and publish
**Files:**
- Modify: `references/evidence_manifests/commercial_external_validation_release.v1.json` when public evidence is added to release scope
- [ ] Run focused Shadbala, parity, API, privacy, and release-hash gates.
- [ ] Commit only reviewed migration files and push `codex/cross-project-contract`.
@@ -93,6 +93,36 @@
"path": "references/oracle/artifacts/vedastro_steve_jobs_contract_probe_run3.json",
"sha256": "fb0e05704df9be5bece7bef3b42fc14e0cc4f254685f039b855b359da453c650",
"scope": "third independent redacted VedAstro contract probe"
},
{
"id": "high_rigor_local_raw",
"path": "references/oracle/artifacts/local_steve_jobs_high_rigor_raw.json",
"sha256": "8361cac2831404a005e6be5c927b9cd610cfc0787ca17a547e81ae9a4d601746",
"scope": "public same-chart local high-rigor raw output"
},
{
"id": "high_rigor_pyjhora_raw",
"path": "references/oracle/artifacts/pyjhora_steve_jobs_high_rigor_raw.json",
"sha256": "a0374c77f7d4c435a473871ca8bbd0dff45f314eb2d68f5e9b3106478b986508",
"scope": "public same-chart PyJHora high-rigor raw output"
},
{
"id": "high_rigor_jyotishganit_raw",
"path": "references/oracle/artifacts/jyotishganit_steve_jobs_high_rigor_raw.json",
"sha256": "5cbb85f6cd9210558d496c350b45aa1dc2785ab3f49a800a884b45c76c49e6a1",
"scope": "public same-chart jyotishganit high-rigor raw output"
},
{
"id": "high_rigor_vedastro_divisional_raw",
"path": "references/oracle/artifacts/vedastro_steve_jobs_public_aa_divisional_raw.json",
"sha256": "75da3fa79463e22801b4602ec96d55267fc1dd0ef134ca0c96249317b0db9ef9",
"scope": "public VedAstro divisional and Shadbala raw evidence"
},
{
"id": "pyjhora_extended_public_smoke",
"path": "references/oracle/pyjhora_extended_parity_public_smoke_manifest.json",
"sha256": "c86315d8e16338608409b9277f835f647f8b739c207e4bcca177fd5ea0e2c0c4",
"scope": "public PyJHora extended parity smoke manifest"
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,792 @@
{
"advanced": {
"A10_Karma_Pada": {
"degree": 117.9412,
"distance_from_source": 7,
"exception_applied": false,
"formula": "A10: from house lord count the same distance from source house; apply 1/7 exception",
"house": 12,
"label": "A10_Karma_Pada",
"meaning": "A10/Karma Pada/Rajya Pada:事业名声、职业可见度、社会身份与公众层面的职业结果",
"sign": "Cancer",
"sign_degree": 27.9412,
"source_house": 10,
"source_lord_sign": "Sagittarius",
"source_sign": "Taurus"
},
"dasha_sandhi": {
"orb_days": 90,
"reference_date": "2026-07-17",
"sandhi_windows": []
},
"pushkara_true": {
"Jupiter": {
"degree_in_sign": 27.2772,
"pushkara_bhaga": false,
"pushkara_bhaga_meta": {
"delta": 3.2772,
"element": "air",
"orb": 1.0,
"target_degree": 24
},
"pushkara_navamsa": true,
"pushkara_navamsa_meta": {
"element": "air",
"range": [
26.6667,
30
]
},
"sign": "Gemini",
"sign_cn": "双子座"
},
"Rahu": {
"degree_in_sign": 9.2743,
"pushkara_bhaga": false,
"pushkara_bhaga_meta": {
"delta": 11.7257,
"element": "fire",
"orb": 1.0,
"target_degree": 21
},
"pushkara_navamsa": true,
"pushkara_navamsa_meta": {
"element": "fire",
"range": [
6.6667,
10
]
},
"sign": "Sagittarius",
"sign_cn": "射手座"
},
"Saturn": {
"degree_in_sign": 27.932,
"pushkara_bhaga": false,
"pushkara_bhaga_meta": {
"delta": 3.932,
"element": "air",
"orb": 1.0,
"target_degree": 24
},
"pushkara_navamsa": true,
"pushkara_navamsa_meta": {
"element": "air",
"range": [
26.6667,
30
]
},
"sign": "Libra",
"sign_cn": "天秤座"
}
},
"vargottama_true": {
"Jupiter": {
"d1_sign": "Gemini",
"d9_sign": "Gemini",
"is_vargottama": true
},
"Venus": {
"d1_sign": "Sagittarius",
"d9_sign": "Sagittarius",
"is_vargottama": true
}
}
},
"ascendant": {
"degree": 29.0606,
"degree_in_sign": 29.0606,
"degree_in_sign_raw": 149.06059920521156,
"degree_raw": 149.0606,
"lon": 149.0606,
"lord": "Sun",
"sign": "Leo",
"sign_cn": "狮子座"
},
"ashtakavarga": {
"bav": {
"Jupiter": [
7,
5,
3,
3,
4,
7,
4,
5,
4,
6,
5,
3
],
"Lagna": [
5,
3,
4,
4,
4,
2,
4,
3,
5,
7,
5,
3
],
"Mars": [
4,
7,
4,
4,
3,
0,
4,
5,
1,
4,
1,
2
],
"Mercury": [
5,
5,
5,
4,
4,
2,
6,
5,
5,
8,
2,
3
],
"Moon": [
4,
4,
4,
3,
6,
5,
3,
3,
4,
5,
3,
5
],
"Saturn": [
1,
5,
3,
0,
6,
3,
4,
5,
3,
3,
3,
3
],
"Sun": [
3,
7,
4,
3,
3,
2,
6,
7,
4,
4,
3,
2
],
"Venus": [
4,
3,
5,
4,
3,
5,
4,
3,
5,
5,
5,
6
]
},
"sav": [
28,
36,
28,
21,
29,
24,
31,
33,
26,
35,
22,
24
]
},
"birth": {
"day": 24,
"hour": 19,
"lat": 37.7749,
"lon": -122.4194,
"minute": 15,
"month": 2,
"tz": -8.0,
"year": 1955
},
"category": "public",
"dasha": {
"antardasha_end": "2027-11-30",
"antardasha_lord": "Rahu",
"antardasha_start": "2025-03-19",
"mahadasha_end": "2043-03-20",
"mahadasha_lord": "Rahu",
"mahadasha_start": "2025-03-19"
},
"engine": "local_jyotish_skill_v6_0_4",
"label": "Steve Jobs public AA",
"module_health": {
"empty_modules": [],
"module_count": 55,
"validation": {
"checked": 11,
"failed": 0,
"passed": 11,
"results": [
{
"detail": "SAV total = 337, expected 337",
"name": "SAV总和=337",
"passed": true,
"rule": "R1"
},
{
"detail": "Sun=48/48; Moon=49/49; Mars=39/39; Mercury=54/54; Jupiter=56/56; Venus=52/52; Saturn=39/39; Lagna=49/49",
"name": "BAV行常数校验",
"passed": true,
"rule": "R2"
},
{
"detail": "所有12星座 BAV列和 与 SAV分数一致 ✅",
"name": "BAV列→SAV列校验",
"passed": true,
"rule": "R2b"
},
{
"detail": "Mercury延伸角 = 21.39°, 阈值 28°",
"name": "水星延伸角≤28°",
"passed": true,
"rule": "R3"
},
{
"detail": "Venus延伸角 = 44.58°, 阈值 47°",
"name": "金星延伸角≤47°",
"passed": true,
"rule": "R4"
},
{
"detail": "Rahu=249.2743°, Ketu=69.2743°, 偏差=0.0000°",
"name": "Rahu-Ketu 180°对冲",
"passed": true,
"rule": "R5"
},
{
"detail": "所有行星逆行状态合法",
"name": "逆行合法性",
"passed": true,
"rule": "R6"
},
{
"detail": "Dasha year chain sum = 120",
"name": "Dasha年限总和=120",
"passed": true,
"rule": "R7"
},
{
"detail": "完整:9行星 + Ascendant",
"name": "行星完整性",
"passed": true,
"rule": "R8"
},
{
"detail": "所有行星度数在合法范围",
"name": "星座度数范围",
"passed": true,
"rule": "R9"
},
{
"detail": "1-12宫完整(12宫)",
"name": "宫位连续性",
"passed": true,
"rule": "R10"
}
],
"skipped": 0,
"total_checks": 11,
"valid": true
}
},
"parameters": {
"ayanamsa": "lahiri_assumed_by_skill",
"house": "whole_sign_for_planet_house",
"today": "2026-07-17",
"zodiac": "sidereal"
},
"planets": {
"Jupiter": {
"degree_in_sign": 27.2772,
"house": 11,
"nakshatra": "Punarvasu",
"nakshatra_pada": 3,
"retrograde": true,
"sign": "Gemini"
},
"Ketu": {
"degree_in_sign": 9.2743,
"house": 11,
"nakshatra": "Ardra",
"nakshatra_pada": 1,
"retrograde": true,
"sign": "Gemini"
},
"Mars": {
"degree_in_sign": 5.8598,
"house": 9,
"nakshatra": "Ashwini",
"nakshatra_pada": 2,
"retrograde": false,
"sign": "Aries"
},
"Mercury": {
"degree_in_sign": 21.1311,
"house": 6,
"nakshatra": "Shravana",
"nakshatra_pada": 4,
"retrograde": true,
"sign": "Capricorn"
},
"Moon": {
"degree_in_sign": 14.5165,
"house": 8,
"nakshatra": "Uttara Bhadrapada",
"nakshatra_pada": 4,
"retrograde": false,
"sign": "Pisces"
},
"Rahu": {
"degree_in_sign": 9.2743,
"house": 5,
"nakshatra": "Mula",
"nakshatra_pada": 3,
"retrograde": true,
"sign": "Sagittarius"
},
"Saturn": {
"degree_in_sign": 27.932,
"house": 3,
"nakshatra": "Vishakha",
"nakshatra_pada": 3,
"retrograde": false,
"sign": "Libra"
},
"Sun": {
"degree_in_sign": 12.5175,
"house": 7,
"nakshatra": "Shatabhisha",
"nakshatra_pada": 2,
"retrograde": false,
"sign": "Aquarius"
},
"Venus": {
"degree_in_sign": 27.9412,
"house": 5,
"nakshatra": "Uttara Ashadha",
"nakshatra_pada": 1,
"retrograde": false,
"sign": "Sagittarius"
}
},
"privacy": "public",
"sample_id": "steve_jobs_public_aa",
"shadbala": {
"Jupiter": null,
"Mars": null,
"Mercury": null,
"Moon": null,
"Saturn": null,
"Sun": null,
"Venus": null
},
"shadbala_components": {
"Jupiter": {
"chesta": null,
"dig": null,
"drik": null,
"kala": null,
"naisargika": null,
"sthana": null
},
"Mars": {
"chesta": null,
"dig": null,
"drik": null,
"kala": null,
"naisargika": null,
"sthana": null
},
"Mercury": {
"chesta": null,
"dig": null,
"drik": null,
"kala": null,
"naisargika": null,
"sthana": null
},
"Moon": {
"chesta": null,
"dig": null,
"drik": null,
"kala": null,
"naisargika": null,
"sthana": null
},
"Saturn": {
"chesta": null,
"dig": null,
"drik": null,
"kala": null,
"naisargika": null,
"sthana": null
},
"Sun": {
"chesta": null,
"dig": null,
"drik": null,
"kala": null,
"naisargika": null,
"sthana": null
},
"Venus": {
"chesta": null,
"dig": null,
"drik": null,
"kala": null,
"naisargika": null,
"sthana": null
}
},
"shadbala_method_variants": {
"chesta": "bphs_bounded_surya_mean_motion_seeghrochcha",
"chesta_source": "MIT jyotishganit structure plus public Surya Siddhanta revolution constants",
"kala": "bphs_local_solar_events_ahargana_declination"
},
"varga": {
"D10": {
"Ascendant": {
"degree_in_sign": 20.606,
"lord": "Venus",
"part_index": 9,
"sign": "Taurus",
"sign_idx": 1
},
"Jupiter": {
"degree_in_sign": 2.7724,
"lord": "Jupiter",
"part_index": 9,
"sign": "Pisces",
"sign_idx": 11
},
"Ketu": {
"degree_in_sign": 2.7429,
"lord": "Mercury",
"part_index": 3,
"sign": "Virgo",
"sign_idx": 5
},
"Mars": {
"degree_in_sign": 28.5982,
"lord": "Venus",
"part_index": 1,
"sign": "Taurus",
"sign_idx": 1
},
"Mercury": {
"degree_in_sign": 1.3107,
"lord": "Mars",
"part_index": 7,
"sign": "Aries",
"sign_idx": 0
},
"Moon": {
"degree_in_sign": 25.1652,
"lord": "Jupiter",
"part_index": 4,
"sign": "Pisces",
"sign_idx": 11
},
"Rahu": {
"degree_in_sign": 2.7429,
"lord": "Jupiter",
"part_index": 3,
"sign": "Pisces",
"sign_idx": 11
},
"Saturn": {
"degree_in_sign": 9.3204,
"lord": "Moon",
"part_index": 9,
"sign": "Cancer",
"sign_idx": 3
},
"Sun": {
"degree_in_sign": 5.1749,
"lord": "Mercury",
"part_index": 4,
"sign": "Gemini",
"sign_idx": 2
},
"Venus": {
"degree_in_sign": 9.4123,
"lord": "Mercury",
"part_index": 9,
"sign": "Virgo",
"sign_idx": 5
}
},
"D2": {
"Ascendant": {
"degree_in_sign": 28.1212,
"lord": "Moon",
"part_index": 1,
"sign": "Cancer",
"sign_idx": 3
},
"Jupiter": {
"degree_in_sign": 24.5545,
"lord": "Moon",
"part_index": 1,
"sign": "Cancer",
"sign_idx": 3
},
"Ketu": {
"degree_in_sign": 18.5486,
"lord": "Sun",
"part_index": 0,
"sign": "Leo",
"sign_idx": 4
},
"Mars": {
"degree_in_sign": 11.7196,
"lord": "Sun",
"part_index": 0,
"sign": "Leo",
"sign_idx": 4
},
"Mercury": {
"degree_in_sign": 12.2621,
"lord": "Sun",
"part_index": 1,
"sign": "Leo",
"sign_idx": 4
},
"Moon": {
"degree_in_sign": 29.033,
"lord": "Moon",
"part_index": 0,
"sign": "Cancer",
"sign_idx": 3
},
"Rahu": {
"degree_in_sign": 18.5486,
"lord": "Sun",
"part_index": 0,
"sign": "Leo",
"sign_idx": 4
},
"Saturn": {
"degree_in_sign": 25.8641,
"lord": "Moon",
"part_index": 1,
"sign": "Cancer",
"sign_idx": 3
},
"Sun": {
"degree_in_sign": 25.035,
"lord": "Sun",
"part_index": 0,
"sign": "Leo",
"sign_idx": 4
},
"Venus": {
"degree_in_sign": 25.8825,
"lord": "Moon",
"part_index": 1,
"sign": "Cancer",
"sign_idx": 3
}
},
"D4": {
"Ascendant": {
"degree_in_sign": 26.2424,
"lord": "Venus",
"part_index": 3,
"sign": "Taurus",
"sign_idx": 1
},
"Jupiter": {
"degree_in_sign": 19.1089,
"lord": "Jupiter",
"part_index": 3,
"sign": "Pisces",
"sign_idx": 11
},
"Ketu": {
"degree_in_sign": 7.0972,
"lord": "Mercury",
"part_index": 1,
"sign": "Virgo",
"sign_idx": 5
},
"Mars": {
"degree_in_sign": 23.4393,
"lord": "Mars",
"part_index": 0,
"sign": "Aries",
"sign_idx": 0
},
"Mercury": {
"degree_in_sign": 24.5243,
"lord": "Moon",
"part_index": 2,
"sign": "Cancer",
"sign_idx": 3
},
"Moon": {
"degree_in_sign": 28.0661,
"lord": "Mercury",
"part_index": 1,
"sign": "Gemini",
"sign_idx": 2
},
"Rahu": {
"degree_in_sign": 7.0972,
"lord": "Jupiter",
"part_index": 1,
"sign": "Pisces",
"sign_idx": 11
},
"Saturn": {
"degree_in_sign": 21.7282,
"lord": "Moon",
"part_index": 3,
"sign": "Cancer",
"sign_idx": 3
},
"Sun": {
"degree_in_sign": 20.0699,
"lord": "Venus",
"part_index": 1,
"sign": "Taurus",
"sign_idx": 1
},
"Venus": {
"degree_in_sign": 21.7649,
"lord": "Mercury",
"part_index": 3,
"sign": "Virgo",
"sign_idx": 5
}
},
"D9": {
"Ascendant": {
"degree_in_sign": 21.5454,
"lord": "Jupiter",
"pada": 9,
"part_index": 8,
"sign": "Sagittarius",
"sign_idx": 8
},
"Jupiter": {
"degree_in_sign": 5.4951,
"lord": "Mercury",
"pada": 9,
"part_index": 8,
"sign": "Gemini",
"sign_idx": 2
},
"Ketu": {
"degree_in_sign": 23.4686,
"lord": "Jupiter",
"pada": 3,
"part_index": 2,
"sign": "Sagittarius",
"sign_idx": 8
},
"Mars": {
"degree_in_sign": 22.7384,
"lord": "Venus",
"pada": 2,
"part_index": 1,
"sign": "Taurus",
"sign_idx": 1
},
"Mercury": {
"degree_in_sign": 10.1797,
"lord": "Moon",
"pada": 7,
"part_index": 6,
"sign": "Cancer",
"sign_idx": 3
},
"Moon": {
"degree_in_sign": 10.6487,
"lord": "Mars",
"pada": 5,
"part_index": 4,
"sign": "Scorpio",
"sign_idx": 7
},
"Rahu": {
"degree_in_sign": 23.4686,
"lord": "Mercury",
"pada": 3,
"part_index": 2,
"sign": "Gemini",
"sign_idx": 2
},
"Saturn": {
"degree_in_sign": 11.3884,
"lord": "Mercury",
"pada": 9,
"part_index": 8,
"sign": "Gemini",
"sign_idx": 2
},
"Sun": {
"degree_in_sign": 22.6574,
"lord": "Saturn",
"pada": 4,
"part_index": 3,
"sign": "Capricorn",
"sign_idx": 9
},
"Venus": {
"degree_in_sign": 11.4711,
"lord": "Jupiter",
"pada": 9,
"part_index": 8,
"sign": "Sagittarius",
"sign_idx": 8
}
}
}
}
@@ -0,0 +1,494 @@
{
"ascendant": {
"degree_in_sign": 29.0556,
"sign": "Leo"
},
"ashtakavarga": {
"bav": {
"Jupiter": [
7,
5,
3,
3,
4,
7,
4,
5,
4,
6,
5,
3
],
"Lagna": [
5,
3,
4,
4,
4,
2,
4,
3,
5,
7,
5,
3
],
"Mars": [
4,
7,
4,
4,
3,
0,
4,
5,
1,
4,
1,
2
],
"Mercury": [
5,
5,
5,
4,
4,
2,
6,
5,
5,
8,
2,
3
],
"Moon": [
4,
4,
4,
3,
6,
5,
3,
3,
4,
5,
3,
5
],
"Saturn": [
1,
5,
3,
0,
6,
3,
4,
5,
3,
3,
3,
3
],
"Sun": [
3,
7,
4,
3,
3,
2,
6,
7,
4,
4,
3,
2
],
"Venus": [
4,
3,
5,
4,
3,
5,
4,
3,
5,
5,
5,
6
]
},
"sav": [
28,
36,
28,
21,
29,
24,
31,
33,
26,
35,
22,
24
]
},
"dasha": {
"antardasha_end": "2027-12-03",
"antardasha_lord": "Rahu",
"antardasha_start": "2025-03-22",
"mahadasha_end": "2043-03-23",
"mahadasha_lord": "Rahu",
"mahadasha_start": "2025-03-22"
},
"engine": "PyJHora_4_8_6_lahiri_patched",
"parameters": {
"ayanamsa": "LAHIRI",
"compat": "monkeypatch swisseph keyword API + missing constants; dummy timezonefinder only for import",
"d10_method": "PyJHora divisional_chart chart_method=1",
"d2_method": "PyJHora hora_chart chart_method=2 traditional_parasara",
"d4_method": "PyJHora chaturthamsa_chart chart_method=1 traditional_parasara",
"d9_method": "PyJHora divisional_chart chart_method=1",
"dasha_year": "mean sidereal year",
"license_note": "PyJHora is AGPL-3.0; used only as external benchmark, not vendored into skill.",
"zodiac": "sidereal"
},
"planets": {
"Jupiter": {
"degree_in_sign": 27.2706,
"nakshatra": "Punarvasu",
"nakshatra_pada": 3,
"sign": "Gemini"
},
"Ketu": {
"degree_in_sign": 9.2693,
"nakshatra": "Ardra",
"nakshatra_pada": 1,
"sign": "Gemini"
},
"Mars": {
"degree_in_sign": 5.8623,
"nakshatra": "Ashwini",
"nakshatra_pada": 2,
"sign": "Aries"
},
"Mercury": {
"degree_in_sign": 21.1259,
"nakshatra": "Shravana",
"nakshatra_pada": 4,
"sign": "Capricorn"
},
"Moon": {
"degree_in_sign": 14.5117,
"nakshatra": "Uttara Bhadrapada",
"nakshatra_pada": 4,
"sign": "Pisces"
},
"Rahu": {
"degree_in_sign": 9.2693,
"nakshatra": "Mula",
"nakshatra_pada": 3,
"sign": "Sagittarius"
},
"Saturn": {
"degree_in_sign": 27.9274,
"nakshatra": "Vishakha",
"nakshatra_pada": 3,
"sign": "Libra"
},
"Sun": {
"degree_in_sign": 12.5182,
"nakshatra": "Shatabhisha",
"nakshatra_pada": 2,
"sign": "Aquarius"
},
"Venus": {
"degree_in_sign": 27.9421,
"nakshatra": "Uttara Ashadha",
"nakshatra_pada": 1,
"sign": "Sagittarius"
}
},
"sample_id": "steve_jobs_public_aa",
"settings": {
"ayanamsa": "lahiri",
"node_mode": "mean"
},
"shadbala": {
"Jupiter": 596.89,
"Mars": 533.18,
"Mercury": 363.24,
"Moon": 562.89,
"Saturn": 429.42,
"Sun": 444.63,
"Venus": 408.64
},
"shadbala_components": {
"Jupiter": {
"chesta": 74.0,
"dig": 80.6,
"drik": 6.89,
"kala": 199.31,
"naisargika": 34.29,
"sthana": 201.8
},
"Mars": {
"chesta": 98.58,
"dig": 77.41,
"drik": 13.49,
"kala": 127.93,
"naisargika": 17.14,
"sthana": 198.63
},
"Mercury": {
"chesta": 45.78,
"dig": 12.64,
"drik": -10.63,
"kala": 121.78,
"naisargika": 25.71,
"sthana": 167.96
},
"Moon": {
"chesta": 0.0,
"dig": 95.48,
"drik": 20.06,
"kala": 149.58,
"naisargika": 51.43,
"sthana": 246.34
},
"Saturn": {
"chesta": 36.36,
"dig": 19.62,
"drik": 9.49,
"kala": 136.77,
"naisargika": 8.57,
"sthana": 218.61
},
"Sun": {
"chesta": 0.0,
"dig": 24.81,
"drik": 1.74,
"kala": 140.99,
"naisargika": 60.0,
"sthana": 217.09
},
"Venus": {
"chesta": 31.08,
"dig": 69.95,
"drik": -2.75,
"kala": 113.44,
"naisargika": 42.86,
"sthana": 154.06
}
},
"varga": {
"D10": {
"Ascendant": {
"degree_in_sign": 20.5556,
"sign": "Taurus",
"sign_idx": 1
},
"Jupiter": {
"degree_in_sign": 2.7055,
"sign": "Pisces",
"sign_idx": 11
},
"Ketu": {
"degree_in_sign": 2.6925,
"sign": "Virgo",
"sign_idx": 5
},
"Mars": {
"degree_in_sign": 28.6233,
"sign": "Taurus",
"sign_idx": 1
},
"Mercury": {
"degree_in_sign": 1.2589,
"sign": "Aries",
"sign_idx": 0
},
"Moon": {
"degree_in_sign": 25.1168,
"sign": "Pisces",
"sign_idx": 11
},
"Rahu": {
"degree_in_sign": 2.6925,
"sign": "Pisces",
"sign_idx": 11
},
"Saturn": {
"degree_in_sign": 9.2738,
"sign": "Cancer",
"sign_idx": 3
},
"Sun": {
"degree_in_sign": 5.182,
"sign": "Gemini",
"sign_idx": 2
},
"Venus": {
"degree_in_sign": 9.4212,
"sign": "Virgo",
"sign_idx": 5
}
},
"D2": {
"Ascendant": {
"degree_in_sign": 28.1111,
"sign": "Cancer",
"sign_idx": 3
},
"Jupiter": {
"degree_in_sign": 24.5411,
"sign": "Cancer",
"sign_idx": 3
},
"Ketu": {
"degree_in_sign": 18.5385,
"sign": "Leo",
"sign_idx": 4
},
"Mars": {
"degree_in_sign": 11.7247,
"sign": "Leo",
"sign_idx": 4
},
"Mercury": {
"degree_in_sign": 12.2518,
"sign": "Leo",
"sign_idx": 4
},
"Moon": {
"degree_in_sign": 29.0234,
"sign": "Cancer",
"sign_idx": 3
},
"Rahu": {
"degree_in_sign": 18.5385,
"sign": "Leo",
"sign_idx": 4
},
"Saturn": {
"degree_in_sign": 25.8548,
"sign": "Cancer",
"sign_idx": 3
},
"Sun": {
"degree_in_sign": 25.0364,
"sign": "Leo",
"sign_idx": 4
},
"Venus": {
"degree_in_sign": 25.8842,
"sign": "Cancer",
"sign_idx": 3
}
},
"D4": {
"Ascendant": {
"degree_in_sign": 26.2222,
"sign": "Taurus",
"sign_idx": 1
},
"Jupiter": {
"degree_in_sign": 19.0822,
"sign": "Pisces",
"sign_idx": 11
},
"Ketu": {
"degree_in_sign": 7.077,
"sign": "Virgo",
"sign_idx": 5
},
"Mars": {
"degree_in_sign": 23.4493,
"sign": "Aries",
"sign_idx": 0
},
"Mercury": {
"degree_in_sign": 24.5036,
"sign": "Cancer",
"sign_idx": 3
},
"Moon": {
"degree_in_sign": 28.0467,
"sign": "Gemini",
"sign_idx": 2
},
"Rahu": {
"degree_in_sign": 7.077,
"sign": "Pisces",
"sign_idx": 11
},
"Saturn": {
"degree_in_sign": 21.7095,
"sign": "Cancer",
"sign_idx": 3
},
"Sun": {
"degree_in_sign": 20.0728,
"sign": "Taurus",
"sign_idx": 1
},
"Venus": {
"degree_in_sign": 21.7685,
"sign": "Virgo",
"sign_idx": 5
}
},
"D9": {
"Ascendant": {
"degree_in_sign": 21.5,
"sign": "Sagittarius",
"sign_idx": 8
},
"Jupiter": {
"degree_in_sign": 5.435,
"sign": "Gemini",
"sign_idx": 2
},
"Ketu": {
"degree_in_sign": 23.4233,
"sign": "Sagittarius",
"sign_idx": 8
},
"Mars": {
"degree_in_sign": 22.761,
"sign": "Taurus",
"sign_idx": 1
},
"Mercury": {
"degree_in_sign": 10.133,
"sign": "Cancer",
"sign_idx": 3
},
"Moon": {
"degree_in_sign": 10.6051,
"sign": "Scorpio",
"sign_idx": 7
},
"Rahu": {
"degree_in_sign": 23.4233,
"sign": "Gemini",
"sign_idx": 2
},
"Saturn": {
"degree_in_sign": 11.3465,
"sign": "Gemini",
"sign_idx": 2
},
"Sun": {
"degree_in_sign": 22.6638,
"sign": "Capricorn",
"sign_idx": 9
},
"Venus": {
"degree_in_sign": 11.4791,
"sign": "Sagittarius",
"sign_idx": 8
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,31 @@
{
"scope": "pyjhora_extended_parity_public_smoke",
"status": "partial_verified",
"tested": true,
"birth_data_policy": "public_synthetic_smoke_only",
"settings": {
"ayanamsa": "lahiri",
"node_mode": "mean"
},
"sample_count": 1,
"sample_ids": [
"smoke_beijing_1990_noon"
],
"comparison_rows": {
"total": 239,
"match": 232,
"mismatch": 7
},
"covered_outputs": [
"D2",
"D4",
"D9",
"D10",
"Ashtakavarga"
],
"partial_outputs": [
"Shadbala"
],
"source_report": "docs/research/pyjhora_d2_d4_ashtakavarga_shadbala_parity_2026_07_15.md",
"boundary": "D2, D4, BAV and SAV match for one public synthetic chart. Shadbala total virupas mismatch for all seven planets and remains outside verified coverage."
}
File diff suppressed because it is too large Load Diff
+14 -1
View File
@@ -6281,12 +6281,25 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
birth_minute = self._get_float(body, 'birth_minute', body.get('minute', 0), 0, 59)
birth_second = self._get_birth_second(body)
birth_hour_decimal = self._birth_hour_decimal(birth_hour, birth_minute, birth_second)
result = _load_local_module('shadbala').calc_shadbala(
shadbala_module = _load_local_module('shadbala')
context = None
if all(key in body for key in ('year', 'month', 'day', 'lat', 'lon')):
year = self._get_int(body, 'year', 1990, 1800, 2400)
month = self._get_int(body, 'month', 6, 1, 12)
day = self._get_int(body, 'day', 15, 1, 31)
lat = self._get_float(body, 'lat', 0, -90, 90)
lon = self._get_float(body, 'lon', 0, -180, 180)
tz = self._parse_timezone(body, lat, lon, year, month, day, birth_hour, birth_minute, birth_second)
jd = swe.julday(year, month, day, birth_hour_decimal - tz)
context = shadbala_module.build_shadbala_context(jd, lat, lon, body.get('ayanamsa', 'lahiri'))
result = shadbala_module.calc_shadbala(
planets,
SIGNS[asc_sign_idx],
birth_hour_decimal,
planet_lons['Sun'],
planet_lons['Moon'],
birth_minute,
context=context,
)
advanced = self._compute_shadbala_advanced_layer(body, planets, result)
return {
+7 -5
View File
@@ -3356,7 +3356,7 @@ def cmd_shadbala(args):
return {"error": "swisseph未安装"}
try:
sys.path.insert(0, SCRIPT_DIR)
from shadbala import calc_shadbala
from shadbala import build_shadbala_context, calc_shadbala
except ImportError as e:
return {"error": f"shadbala模块导入失败: {e}"}
planets = chart.get("planets", {})
@@ -3364,7 +3364,8 @@ def cmd_shadbala(args):
birth_hour = _birth_hour_decimal(args.hour, args.minute, _arg_second(args))
sun_lon = planets.get("Sun", {}).get("degree", 0)
moon_lon = planets.get("Moon", {}).get("degree", 0)
return calc_shadbala(planets, asc_sign, birth_hour, sun_lon, moon_lon)
context = build_shadbala_context(jd, args.lat, args.lon, _current_ayanamsa_name(args))
return calc_shadbala(planets, asc_sign, birth_hour, sun_lon, moon_lon, context=context)
# ============================================================================
@@ -3752,11 +3753,12 @@ def cmd_audit(args):
# P9 Shadbala(六重力量)
try:
sys.path.insert(0, SCRIPT_DIR)
from shadbala import calc_shadbala
from shadbala import build_shadbala_context, calc_shadbala
birth_hour = _birth_hour_decimal(args.hour, args.minute, _arg_second(args))
sun_lon = planets.get('Sun', {}).get('degree', 0)
moon_lon = planets.get('Moon', {}).get('degree', 0)
shadbala = calc_shadbala(planets, asc_sign, birth_hour, sun_lon, moon_lon)
context = build_shadbala_context(jd, args.lat, args.lon, _current_ayanamsa_name(args))
shadbala = calc_shadbala(planets, asc_sign, birth_hour, sun_lon, moon_lon, context=context)
report['audit']['P9_shadbala'] = {
'summary': shadbala.get('summary', {}),
'ishta_bala_ranking': shadbala.get('ishta_bala_ranking', []),
@@ -5524,7 +5526,7 @@ def cmd_full_reading(args):
birth_hour = _birth_hour_decimal(args.hour, args.minute, _arg_second(args))
sun_lon = planet_lons.get('Sun', 0)
moon_lon = planet_lons.get('Moon', 0)
shadbala_result = calc_shadbala(planets, asc_sign, birth_hour, sun_lon, moon_lon)
shadbala_result = calc_shadbala(planets, asc_sign, birth_hour, sun_lon, moon_lon, context=context)
# 添加顶层汇总
if isinstance(shadbala_result, dict):
sb_planets = shadbala_result.get('planets', {})
+186 -51
View File
@@ -20,6 +20,7 @@ v6.9.12 修复:
"""
import math
import swisseph as swe
import sys
import os
import json
@@ -118,11 +119,189 @@ SPECIAL_ASPECTS = {
# Virupas → Rupas 转换(60 Virupas = 1 Rupa)
VIRUPAS_PER_RUPA = 60.0
_DIG_POWERLESS_HOUSE = {"Sun": 3, "Moon": 9, "Mars": 3, "Mercury": 6, "Jupiter": 6, "Venus": 9, "Saturn": 0}
_MOOLATRIKONA = {
"Sun": (4, 0.0, 20.0), "Moon": (1, 4.0, 30.0), "Mars": (0, 0.0, 12.0),
"Mercury": (5, 16.0, 20.0), "Jupiter": (8, 0.0, 10.0),
"Venus": (6, 0.0, 15.0), "Saturn": (10, 0.0, 20.0),
}
def build_shadbala_context(jd_ut: float, lat: float, lon: float, ayanamsa: str = "lahiri") -> Dict:
"""Build precise sidereal house context from standard birth inputs."""
sid_mode = swe.SIDM_LAHIRI
if str(ayanamsa).lower() in {"raman"}:
sid_mode = swe.SIDM_RAMAN
elif str(ayanamsa).lower() in {"kp", "krishnamurti"}:
sid_mode = swe.SIDM_KRISHNAMURTI
swe.set_sid_mode(sid_mode)
cusps, _ = swe.houses_ex(float(jd_ut), float(lat), float(lon), b"P", swe.FLG_SIDEREAL)
return {"jd_ut": float(jd_ut), "lat": float(lat), "lon": float(lon), "ayanamsa": str(ayanamsa), "house_midpoints": [float(value) % 360 for value in cusps]}
def calc_dig_bala_precise(pname: str, planet_lon: float, house_midpoints: list[float]) -> float:
"""Classical directional strength from the powerless bhava midpoint."""
powerless = house_midpoints[_DIG_POWERLESS_HOUSE[pname]]
return round(abs(float(powerless) - (float(planet_lon) % 360)) / 3.0, 2)
def _planet_longitude(name: str, planets: Dict) -> float:
data = planets[name]
degree = float(data.get("degree", 0.0))
if degree >= 30.0 or data.get("sign") not in SIGNS:
return degree % 360.0
return (SIGNS.index(data["sign"]) * 30.0 + degree) % 360.0
def classify_drik_planets(planets: Dict) -> tuple[set[str], set[str]]:
"""Classify the seven planets for Drik Bala from lunar phase and Mercury's company."""
benefics = {name for name in ("Jupiter", "Venus") if name in planets}
malefics = {name for name in ("Sun", "Mars", "Saturn") if name in planets}
if "Moon" in planets and "Sun" in planets:
target = benefics if (_planet_longitude("Moon", planets) - _planet_longitude("Sun", planets)) % 360.0 <= 180.0 else malefics
target.add("Moon")
if "Mercury" in planets:
mercury_lon = _planet_longitude("Mercury", planets)
mercury_sign = int(mercury_lon // 30)
companions = [
name for name in benefics | malefics
if name != "Mercury" and int(_planet_longitude(name, planets) // 30) == mercury_sign
]
benefic_count = sum(name in benefics for name in companions)
malefic_count = sum(name in malefics for name in companions)
if benefic_count >= malefic_count and (benefic_count != malefic_count or not companions):
benefics.add("Mercury")
elif malefic_count > benefic_count:
malefics.add("Mercury")
else:
nearest = min(companions, key=lambda name: abs(_planet_longitude(name, planets) - mercury_lon))
(benefics if nearest in benefics else malefics).add("Mercury")
return benefics, malefics
def _sphuta_drishti_virupas(angle: float, aspecting_planet: str) -> float:
angle = round(float(angle) % 360.0, 2)
if angle < 30.0:
strength = 0.0
elif angle < 60.0:
strength = (angle - 30.0) / 2.0
elif angle < 90.0:
strength = angle - 45.0 + (45.0 if aspecting_planet == "Saturn" else 0.0)
elif angle < 120.0:
strength = (120.0 - angle) / 2.0 + 30.0 + (15.0 if aspecting_planet == "Mars" else 0.0)
elif angle < 150.0:
strength = 150.0 - angle + (30.0 if aspecting_planet == "Jupiter" else 0.0)
elif angle < 180.0:
strength = 2.0 * (angle - 150.0)
elif angle < 300.0:
strength = (300.0 - angle) / 2.0
if aspecting_planet == "Mars" and 210.0 <= angle < 240.0:
strength += 15.0
elif aspecting_planet == "Jupiter" and 240.0 <= angle < 270.0:
strength += 30.0
elif aspecting_planet == "Saturn" and 270.0 <= angle < 300.0:
strength += 45.0
else:
strength = 0.0
return round(strength, 2)
def calc_drik_bala_precise(pname: str, all_planets: Dict) -> float:
"""Continuous Sphuta Drishti, quarter-weighted by natural benefic/malefic status."""
if pname not in all_planets:
return 0.0
benefics, malefics = classify_drik_planets(all_planets)
target_lon = _planet_longitude(pname, all_planets)
total = 0.0
for other_name in benefics | malefics:
if other_name == pname:
continue
strength = _sphuta_drishti_virupas(target_lon - _planet_longitude(other_name, all_planets), other_name)
total += strength if other_name in benefics else -strength
return round(total / 4.0, 2)
def _d30_sign(sign_idx: int, degree: float) -> int:
ranges = (
((5, 0), (10, 10), (18, 8), (25, 2), (30, 6))
if sign_idx % 2 == 0 else
((5, 1), (12, 5), (20, 11), (25, 9), (30, 7))
)
return next(sign for upper, sign in ranges if degree <= upper)
def _saptavarga_signs(planets: Dict) -> Dict[int, Dict[str, int]]:
result = {division: {} for division in (1, 2, 3, 7, 9, 12, 30)}
for name in set(planets) & set(_MOOLATRIKONA):
longitude = _planet_longitude(name, planets)
sign_idx, degree = int(longitude // 30), longitude % 30.0
result[1][name] = sign_idx
result[2][name] = (4 if sign_idx % 2 == 0 else 3) if degree < 15.0 else (3 if sign_idx % 2 == 0 else 4)
for division in (3, 7, 9, 12):
result[division][name] = varga_map(sign_idx, min(division - 1, int(degree / (30.0 / division))), division)
result[30][name] = _d30_sign(sign_idx, degree)
return result
def _compound_relation_score(planet: str, owner: str, d1_signs: Dict[str, int]) -> float:
natural = "friend" if owner in FRIENDSHIP[planet]["friend"] else "enemy" if owner in FRIENDSHIP[planet]["enemy"] else "neutral"
separation = (d1_signs[owner] - d1_signs[planet]) % 12
temporary = "friend" if separation in {1, 2, 3, 9, 10, 11} else "enemy"
return {
("friend", "friend"): 22.5,
("neutral", "friend"): 15.0,
("enemy", "friend"): 7.5,
("friend", "enemy"): 7.5,
("neutral", "enemy"): 3.75,
("enemy", "enemy"): 1.875,
}[(natural, temporary)]
def calc_sthana_bala_precise(pname: str, all_planets: Dict, house: int) -> Dict:
longitude = _planet_longitude(pname, all_planets)
degree = longitude % 30.0
vargas = _saptavarga_signs(all_planets)
scores = {}
for division, positions in vargas.items():
sign_idx = positions[pname]
owner = SIGN_LORDS[SIGNS[sign_idx]]
mt_sign, mt_start, mt_end = _MOOLATRIKONA[pname]
if division == 1 and sign_idx == mt_sign and mt_start <= degree < mt_end:
score = 45.0
elif owner == pname:
score = 30.0
else:
score = _compound_relation_score(pname, owner, vargas[1])
scores[f"sapta_d{division}"] = score
offset = (longitude - DEBILITATION_DEG[pname]) % 360.0
ucha_bala = round(min(offset, 360.0 - offset) / 3.0, 2)
wants_even = pname in {"Moon", "Venus"}
ojayugma = 15.0 * sum((vargas[division][pname] % 2 == 1) == wants_even for division in (1, 9))
kendra_bala = 60.0 if house in (1, 4, 7, 10) else 30.0 if house in (2, 5, 8, 11) else 15.0
drekkana_bala = 15.0 if (
(pname in {"Sun", "Mars", "Jupiter"} and degree < 10.0)
or (pname in {"Mercury", "Saturn"} and 10.0 <= degree < 20.0)
or (pname in {"Moon", "Venus"} and degree >= 20.0)
) else 0.0
sapta_score = round(sum(scores.values()), 2)
return {
"ucha_bala": ucha_bala,
**scores,
"sapta_score": sapta_score,
"ojayugma_bala": ojayugma,
"kendra_bala": kendra_bala,
"drekkana_bala": drekkana_bala,
"total": round(ucha_bala + sapta_score + ojayugma + kendra_bala + drekkana_bala, 2),
}
def calc_shadbala(planets: Dict, asc_sign: str, birth_hour: float,
sun_lon: float, moon_lon: float,
birth_minute: float = 0.0) -> Dict:
birth_minute: float = 0.0, context: Dict | None = None) -> Dict:
"""
计算 Shadbala 相对强弱参考(covered;外部绝对值校准前保留置信度上限)
@@ -152,8 +331,8 @@ def calc_shadbala(planets: Dict, asc_sign: str, birth_hour: float,
retro = p.get('retrograde', False)
speed = p.get('speed', 1.0)
sthana = calc_sthana_bala(pname, lon, sign, house)
dig = calc_dig_bala(pname, house)
sthana = calc_sthana_bala_precise(pname, planets, house)
dig = calc_dig_bala_precise(pname, lon, context["house_midpoints"]) if context and context.get("house_midpoints") else calc_dig_bala(pname, house)
kala = calc_kala_bala(pname, is_night, sun_northern, sun_lon, moon_lon, birth_hour, birth_minute)
chesta = calc_chesta_bala(pname, retro, speed, sun_lon, moon_lon)
naisargika = NAISARGIKA_BALA.get(pname, 30.0)
@@ -176,8 +355,8 @@ def calc_shadbala(planets: Dict, asc_sign: str, birth_hour: float,
retro = p.get('retrograde', False)
speed = p.get('speed', 1.0)
sthana = calc_sthana_bala(pname, lon, sign, house)
dig = calc_dig_bala(pname, house)
sthana = calc_sthana_bala_precise(pname, planets, house)
dig = calc_dig_bala_precise(pname, lon, context["house_midpoints"]) if context and context.get("house_midpoints") else calc_dig_bala(pname, house)
kala = calc_kala_bala(pname, is_night, sun_northern, sun_lon, moon_lon, birth_hour, birth_minute)
chesta = calc_chesta_bala(pname, retro, speed, sun_lon, moon_lon)
naisargika = NAISARGIKA_BALA.get(pname, 30.0)
@@ -737,52 +916,8 @@ def calc_yuddha_bala(planets: Dict) -> Dict:
def calc_drik_bala(pname: str, sign: str, house: int,
all_planets: Dict) -> float:
"""Drik Bala(相位力量),基于精确度数差(Sputa Drishti)计算。
v6.1.13: 升级为基于精确角距离的Sputa Drishti(替代简化宫位差)"""
drik = 0.0
p_sign_idx = SIGNS.index(sign) if sign in SIGNS else 0
p_degree_in_sign = (all_planets[pname].get('degree', 0) if pname in all_planets else 0) % 30
p_lon = p_sign_idx * 30 + p_degree_in_sign
for other_name, other_data in all_planets.items():
if other_name == pname or other_name == 'Rahu' or other_name == 'Ketu':
continue
other_sign = other_data.get('sign', '')
if other_sign not in SIGNS:
continue
other_sign_idx = SIGNS.index(other_sign)
other_deg_in_sign = other_data.get('degree', 0) % 30
other_lon = other_sign_idx * 30 + other_deg_in_sign
# 计算精确角度差
diff = abs(p_lon - other_lon)
if diff > 180:
diff = 360 - diff
# 检查传统相位规则(7宫=180°冲,特殊相位=Mars4/8, Jupiter5/9, Saturn3/10)
has_aspect = False
house_diff = (p_sign_idx - other_sign_idx) % 12 + 1
if house_diff == 7 or house_diff == 1:
has_aspect = True
if other_name in SPECIAL_ASPECTS:
if house_diff in SPECIAL_ASPECTS[other_name]:
has_aspect = True
if not has_aspect:
continue
# 使用Sputa Drishti计算相位力量 (0-1) → 缩放为0-60 Virupas
sputa_value = _get_sputa_drishti_value(diff, other_name)
if other_name in BENEFICS:
drik += sputa_value * 60.0
elif other_name in MALEFICS:
drik -= sputa_value * 60.0
else:
drik += sputa_value * 30.0 # 中性行星
return max(-60.0, min(60.0, drik))
"""Compatibility wrapper for the continuous Sphuta Drishti calculation."""
return calc_drik_bala_precise(pname, all_planets)
# ============================================================================
+170
View File
@@ -0,0 +1,170 @@
#!/usr/bin/env python3
"""Build same-case D2/D4/D9/D10/AV/Shadbala parity artifacts."""
from __future__ import annotations
import hashlib
import json
import sys
import tempfile
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
SCRIPTS = ROOT / "scripts"
if str(SCRIPTS) not in sys.path:
sys.path.insert(0, str(SCRIPTS))
from benchmarks.jyotish.scripts.run_pyjhora_compare import build_pyjhora_sample
from benchmarks.jyotish.scripts.run_skill_baseline import run_sample
from scripts.three_engine_parity_runner import _capture_jyotishganit_raw
ORACLE = ROOT / "references" / "oracle"
ARTIFACTS = ORACLE / "artifacts"
PLANETS = ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn"]
SIGNS = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"]
VED_VARGA_FIELDS = {"D2": "PlanetHoraD2Signs", "D4": "PlanetChaturthamshaD4Sign", "D9": "PlanetNavamshaD9Sign", "D10": "PlanetDashamamshaD10Sign"}
VED_COMPONENT_FIELDS = {
"sthana": "PlanetSthanaBala", "kala": "PlanetKalaBala", "dig": "PlanetDigBala",
"chesta": "PlanetChestaBala", "naisargika": "PlanetNaisargikaBala", "drik": "PlanetDrikBala",
}
SAMPLE = {
"id": "steve_jobs_public_aa", "label": "Steve Jobs public AA", "category": "public", "privacy": "public",
"birth": {"year": 1955, "month": 2, "day": 24, "hour": 19, "minute": 15, "lat": 37.7749, "lon": -122.4194, "tz": -8.0},
"today": "2026-07-17",
}
def _write(path: Path, value: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True), encoding="utf-8")
def _sha(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _jyotish_planet_signs(chart: dict[str, Any]) -> dict[str, str]:
return {str(p["celestialBody"]): str(p["sign"]) for h in chart.get("houses", []) for p in h.get("occupants", []) if p.get("celestialBody") in PLANETS}
def _jyotish_shadbala(raw: dict[str, Any]) -> tuple[dict[str, float], dict[str, dict[str, float]]]:
totals: dict[str, float] = {}; components: dict[str, dict[str, float]] = {}
for house in raw["d1Chart"]["houses"]:
for p in house.get("occupants", []):
name = p.get("celestialBody")
if name not in PLANETS or "shadbala" not in p:
continue
s = p["shadbala"]
totals[name] = float(s["Shadbala"]["Total"])
components[name] = {"sthana": float(s["Sthanabala"]["Total"]), "kala": float(s["Kaalabala"]["Total"]), "dig": float(s["Digbala"]), "chesta": float(s["Cheshtabala"]), "naisargika": float(s["Naisargikabala"]), "drik": float(s["Drikbala"])}
return totals, components
def _ved_payload(raw: dict[str, Any]) -> dict[str, Any]:
return raw["Payload"]["AllPlanetData"]
def _ved_components(raw: dict[str, Any], chesta: Any | None = None) -> dict[str, float]:
payload = _ved_payload(raw)
values = {name: float(payload[field]) for name, field in VED_COMPONENT_FIELDS.items() if field in payload}
if chesta is not None:
values["chesta"] = float(chesta)
return values
def _status(values: dict[str, Any], tolerance: float | None = None) -> str:
data = list(values.values())
if tolerance is not None and all(isinstance(v, (int, float)) for v in data):
return "match" if max(data) - min(data) <= tolerance else "mismatch"
return "match" if len({json.dumps(v, sort_keys=True) for v in data}) == 1 else "mismatch"
def build() -> dict[str, Any]:
local_result = run_sample(SAMPLE)
if not local_result.get("ok"):
raise RuntimeError(local_result.get("error"))
local = local_result["canonical"]
local["shadbala_method_variants"] = {
"kala": "bphs_local_solar_events_ahargana_declination",
"chesta": "bphs_bounded_surya_mean_motion_seeghrochcha",
"chesta_source": "MIT jyotishganit structure plus public Surya Siddhanta revolution constants",
}
pyjhora = build_pyjhora_sample(SAMPLE)
with tempfile.TemporaryDirectory() as tmp:
jyotish, _ = _capture_jyotishganit_raw(Path(tmp))
ved_path = ARTIFACTS / "vedastro_steve_jobs_public_aa_divisional_raw.json"
ved = json.loads(ved_path.read_text(encoding="utf-8"))
local_path = ARTIFACTS / "local_steve_jobs_high_rigor_raw.json"
py_path = ARTIFACTS / "pyjhora_steve_jobs_high_rigor_raw.json"
jy_path = ARTIFACTS / "jyotishganit_steve_jobs_high_rigor_raw.json"
_write(local_path, local); _write(py_path, pyjhora); _write(jy_path, jyotish)
rows: list[dict[str, Any]] = []
jy_d1 = _jyotish_planet_signs(jyotish["d1Chart"])
for planet in PLANETS:
values = {
"local": local["planets"][planet]["sign"],
"PyJHora_JHora": pyjhora["planets"][planet]["sign"],
"VedAstro": _ved_payload(ved["raw_responses"][planet])["PlanetRasiD1Sign"]["Name"],
"jyotishganit": jy_d1[planet],
}
rows.append({"section": "D1", "field": f"{planet}.sign", "local_value": values.pop("local"), "oracle_values": values, "status": _status({"local": local["planets"][planet]["sign"], **values})})
for section, ved_field in VED_VARGA_FIELDS.items():
jy_signs = _jyotish_planet_signs(jyotish["divisionalCharts"][section.lower()])
for planet in PLANETS:
values = {
"local": local["varga"][section][planet]["sign"],
"PyJHora_JHora": pyjhora["varga"][section][planet]["sign"],
"VedAstro": _ved_payload(ved["raw_responses"][planet])[ved_field]["Name"],
"jyotishganit": jy_signs[planet],
}
rows.append({"section": section, "field": f"{planet}.sign", "local_value": values.pop("local"), "oracle_values": values, "status": _status({"local": local["varga"][section][planet]["sign"], **values})})
ved_bav = ved["scalar_responses"]["ashtakavarga_bav"]["Payload"]["BhinnashtakavargaChart"]
ved_sav = ved["scalar_responses"]["ashtakavarga_sav_chart"]["Payload"]["SarvashtakavargaChart"]["Sarvashtakavarga"]["Rows"]
jy_av = jyotish["ashtakavarga"]
for planet in PLANETS:
values = {"local": local["ashtakavarga"]["bav"][planet], "PyJHora_JHora": pyjhora["ashtakavarga"]["bav"][planet], "VedAstro": ved_bav[planet]["Rows"], "jyotishganit": [jy_av[f"{planet.lower()}Bhav"][s] for s in SIGNS]}
rows.append({"section": "ashtakavarga_bav", "field": planet, "local_value": values.pop("local"), "oracle_values": values, "status": _status({"local": local["ashtakavarga"]["bav"][planet], **values})})
sav_values = {"local": local["ashtakavarga"]["sav"], "PyJHora_JHora": pyjhora["ashtakavarga"]["sav"], "VedAstro": ved_sav, "jyotishganit": [jy_av["sav"][s] for s in SIGNS]}
rows.append({"section": "ashtakavarga_sav", "field": "12_sign_scores", "local_value": sav_values.pop("local"), "oracle_values": sav_values, "status": _status({"local": local["ashtakavarga"]["sav"], **sav_values})})
jy_totals, jy_components = _jyotish_shadbala(jyotish)
for planet in PLANETS:
total_values = {"local": local["shadbala"][planet], "PyJHora_JHora": pyjhora["shadbala"][planet], "VedAstro": float(_ved_payload(ved["raw_responses"][planet])["PlanetShadbalaPinda"]), "jyotishganit": jy_totals[planet]}
rows.append({"section": "shadbala_total", "field": planet, "local_value": total_values.pop("local"), "oracle_values": total_values, "status": _status({"local": local["shadbala"][planet], **total_values}, 0.5)})
ved_chesta = ved["component_responses"][f"{planet}.chesta"]["Payload"]["PlanetChestaBala"]
ved_components = _ved_components(ved["raw_responses"][planet], ved_chesta)
for component in VED_COMPONENT_FIELDS:
values = {"local": local["shadbala_components"][planet][component], "PyJHora_JHora": pyjhora["shadbala_components"][planet][component], "VedAstro": ved_components[component], "jyotishganit": jy_components[planet][component]}
row = {"section": "shadbala_components", "field": f"{planet}.{component}", "local_value": values.pop("local"), "oracle_values": values, "status": _status({"local": local["shadbala_components"][planet][component], **values}, 0.5)}
if component == "chesta":
row["method_conflict"] = "PyJHora unbounded; VedAstro/jyotishganit bounded; Sun/Moon treatment differs across engines."
rows.append(row)
manifest = {
"case_id": "steve_jobs_public_1955_lahiri", "birth_data_policy": "public_case_only", "blocked_reason": "none",
"engines": {
"VedAstro": {"status": "official_verified", "official_raw_response_path": "artifacts/" + ved_path.name, "artifact_hash": _sha(ved_path), "settings": ved["settings"]},
"PyJHora_JHora": {"status": "imported", "raw_output_path": "artifacts/" + py_path.name, "artifact_hash": _sha(py_path), "settings": pyjhora["settings"]},
"jyotishganit": {"status": "imported", "raw_output_path": "artifacts/" + jy_path.name, "artifact_hash": _sha(jy_path), "settings": {"ayanamsa": jyotish["ayanamsa"]}},
},
"comparison_rows": rows,
"method_arbitration": {
"kala": "local_vs_PyJHora_7_of_7_within_0.05_virupa",
"chesta": "blocked_cross_engine_method_conflict",
"chesta_local_variant": "bphs_bounded_surya_mean_motion_seeghrochcha",
},
"runtime_boundary": "Full same-case raw coverage; mismatches remain formula/method differences, not missing artifacts.",
}
_write(ORACLE / "three_engine_parity_replay_manifest.json", manifest)
return manifest
if __name__ == "__main__":
result = build()
print(json.dumps({"status": "built", "rows": len(result["comparison_rows"])}, indent=2))
+56
View File
@@ -0,0 +1,56 @@
from scripts.shadbala import (
build_shadbala_context,
calc_dig_bala_precise,
calc_drik_bala_precise,
calc_sthana_bala_precise,
classify_drik_planets,
)
PLANET_LONGITUDES = {
"Sun": 312.51819523181405,
"Moon": 344.51168181221095,
"Mars": 5.862334338243072,
"Mercury": 291.12589362817465,
"Jupiter": 87.27055150216494,
"Venus": 267.94211928370015,
"Saturn": 207.92738390514674,
}
PYJHORA_DIG = {"Sun": 24.81, "Moon": 95.48, "Mars": 77.41, "Mercury": 12.64, "Jupiter": 80.60, "Venus": 69.95, "Saturn": 19.62}
PYJHORA_DRIK = {"Sun": 1.74, "Moon": 20.06, "Mars": 13.49, "Mercury": -10.63, "Jupiter": 6.89, "Venus": -2.75, "Saturn": 9.49}
PYJHORA_STHANA = {"Sun": 217.09, "Moon": 246.34, "Mars": 198.63, "Mercury": 167.96, "Jupiter": 201.80, "Venus": 154.06, "Saturn": 218.61}
def _planets() -> dict:
return {
planet: {
"degree": longitude,
"sign": ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"][int(longitude // 30)],
}
for planet, longitude in PLANET_LONGITUDES.items()
}
def test_precise_dig_bala_matches_pyjhora_same_chart() -> None:
context = build_shadbala_context(2435163.6354166665, 37.7749, -122.4194, "lahiri")
for planet, longitude in PLANET_LONGITUDES.items():
assert calc_dig_bala_precise(planet, longitude, context["house_midpoints"]) == PYJHORA_DIG[planet]
def test_drik_classification_matches_pyjhora_same_chart() -> None:
benefics, malefics = classify_drik_planets(_planets())
assert benefics == {"Moon", "Mercury", "Jupiter", "Venus"}
assert malefics == {"Sun", "Mars", "Saturn"}
def test_precise_drik_bala_matches_pyjhora_same_chart() -> None:
planets = _planets()
for planet, expected in PYJHORA_DRIK.items():
assert abs(calc_drik_bala_precise(planet, planets) - expected) <= 0.02
def test_precise_sthana_bala_matches_pyjhora_same_chart() -> None:
planets = _planets()
houses = {"Sun": 7, "Moon": 8, "Mars": 9, "Mercury": 6, "Jupiter": 11, "Venus": 5, "Saturn": 3}
for planet, expected in PYJHORA_STHANA.items():
assert calc_sthana_bala_precise(planet, planets, houses[planet])["total"] == expected
@@ -0,0 +1,56 @@
import os
from pathlib import Path
import subprocess
import sys
from benchmarks.jyotish.scripts.run_skill_baseline import run_sample
from benchmarks.jyotish.scripts.run_pyjhora_compare import build_pyjhora_sample, compare_one
from scripts import three_engine_high_rigor_parity as parity
ROOT = Path(__file__).resolve().parents[1]
def test_runner_imports_from_commercial_root() -> None:
env = os.environ.copy()
env["PYTHONPATH"] = str(ROOT)
result = subprocess.run(
[sys.executable, "-c", "import scripts.three_engine_high_rigor_parity"],
cwd=ROOT,
env=env,
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, result.stderr
def test_public_sample_exposes_shadbala_components() -> None:
result = run_sample(parity.SAMPLE)
assert result["ok"] is True
components = result["canonical"]["shadbala_components"]
assert set(components["Sun"]) == set(parity.VED_COMPONENT_FIELDS)
def test_pyjhora_sample_exposes_shadbala_components() -> None:
sample = build_pyjhora_sample(parity.SAMPLE)
assert set(sample["shadbala_components"]["Sun"]) == set(parity.VED_COMPONENT_FIELDS)
def test_pyjhora_matrix_includes_shadbala_components() -> None:
baseline = {
"ascendant": {}, "planets": {}, "varga": {}, "ashtakavarga": {}, "shadbala": {}, "dasha": {},
"shadbala_components": {"Sun": {name: 1.0 for name in parity.VED_COMPONENT_FIELDS}},
}
rows = compare_one("sample", baseline, baseline)
assert {row["field"] for row in rows if row["section"] == "Shadbala_Component" and row["body"] == "Sun"} == set(parity.VED_COMPONENT_FIELDS)
def test_jyotishganit_planet_map_reads_divisional_occupants() -> None:
chart = {"houses": [{"occupants": [{"celestialBody": "Sun", "sign": "Leo"}]}]}
assert parity._jyotish_planet_signs(chart) == {"Sun": "Leo"}
def test_vedastro_components_read_six_raw_fields() -> None:
raw = {"Payload": {"AllPlanetData": {field: index for index, field in enumerate(parity.VED_COMPONENT_FIELDS.values(), 1)}}}
assert parity._ved_components(raw) == {name: index for index, name in enumerate(parity.VED_COMPONENT_FIELDS, 1)}