merge: data audit wave 2 H (Chara co-lord/antardasha, gulika night, natal vara, BUG-1565..1569)
Freeze contract, FREEZE/REPORT constants and gate tests keep the G state here; the merged tree is re-frozen once as audit_wave2_2026_11_04 at the end. H's audit_h records are kept byte-for-byte as history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DbBXoyeZrYn6ryNcSe5Y2Y
This commit is contained in:
@@ -6,6 +6,14 @@
|
||||
- 星盘页与对话分盘谱的 D30、D40,以及完整解读里的 D30 专项(`trimshamsa_d30`)也按 BPHS 改正(BUG-1561、BUG-1562)。完整解读分盘表里的 D30 偶数星座仍是旧表:它进生时校正打分,改了会动校正分数,待产品决定(BUG-1560)。
|
||||
- 这几张分盘里行星和上升的星座、宫位会变;D3、D7、D9、D10、D12、D24、D60 不变;对话数据卡上的 D9 / D10 / D12、Chara Karaka 与 Arudha 不变;生时校正 20 条密封案例和 900 个报时窗口回放逐条不变。
|
||||
|
||||
## 2026-11-04 — 数据核对 H:Chara Dasha 双主星与小运、时段点夜段、日出前出生的星期
|
||||
|
||||
- K.N. Rao Chara Dasha 的天蝎 / 水瓶宫主以前与盘面无关(天蝎总是计都、水瓶总是土星),现在一主在本宫取另一主,两主都在外按所在星座比强弱;天蝎、水瓶两段的年数与其后各段起止可能变化(例:布什公开盘天蝎 13 → 9 年,乔布斯水瓶 5 → 1 年),与 PyJHora 的年数一致率 93% → 99%(BUG-1566)。
|
||||
- K.N. Rao Chara 的小运按 K.N. Rao 原书:每个大运按自己的顺逆,从下一星座起,本运星座排最后;以前除第一运外都不对(BUG-1567)。
|
||||
- 星盘页特殊点层的 Maandi、Kaala、Mrityu、Artha Praharaka、Yama Ghantaka:周三到周六夜里出生的盘以前落在错的时段,已改正;Gulika 不变(BUG-1565)。
|
||||
- 完整解读 / 报告里的出生星期(vara)和时主:日出前出生按前一天算(BUG-1568)。
|
||||
- 对照脚本两边都显式传岁差(BUG-1569)。Narayana、行运、日出与外部引擎核对后未发现缺陷,流派差异列给占星师。生时校正结果不变。Skill 版本不变;没有数据库变更。
|
||||
|
||||
## 2026-11-04 — Shadbala:月亮的盈亏力不再重复计入
|
||||
|
||||
- 六力里月亮的盈亏力(Paksha)以前被算了四倍(时力里乘 2,动力又取乘 2 后的值),现按 BPHS(Santhanam 译本)27.10–11 + 27.18:时力里不乘 2,动力等于盈亏力,合计两倍。月亮的六力总分、Ishta / Kashta 和「最强星」可能变化(例:奥巴马公开盘最强星由月亮变为太阳),其他六颗星不变;结果里 `method_variants.moon_paksha` 写明口径(BUG-1557)。生时校正结果不变。Skill 版本不变;没有数据库变更。
|
||||
|
||||
@@ -24,7 +24,7 @@ OUT = ROOT / 'outputs'
|
||||
CANON = OUT / 'canonical'
|
||||
REPORT = OUT / 'jyotish_benchmark_round7_chara_dasha_compare.md'
|
||||
MATRIX = OUT / 'chara_dasha_comparison_matrix.csv'
|
||||
SKILL_SCRIPTS = Path(__file__).resolve().parents[3] / 'scripts' # repo scripts/ (parents[2] was benchmarks/)
|
||||
SKILL_SCRIPTS = Path(__file__).resolve().parents[3] / 'scripts'
|
||||
PYJHORA_SITE = Path(__import__('os').environ.get('PYJHORA_SITE', ''))
|
||||
PYJHORA_COMPAT = ROOT / 'scripts/pyjhora_compat'
|
||||
|
||||
@@ -81,7 +81,8 @@ def pyjhora_chara(sample, *, ayanamsa='lahiri', node_mode='mean'):
|
||||
from jhora import const
|
||||
from jhora.panchanga import drik
|
||||
from jhora.horoscope.dhasa.raasi import chara
|
||||
# Same ayanamsa and node mode as the local baseline (pre_work_error_ledger 2026-11-04).
|
||||
# Both sides use the requested ayanamsa and node mode; the engine default
|
||||
# is Raman and PyJHora's is TRUE_PUSHYA (2026-11-04 audit H).
|
||||
const._DEFAULT_AYANAMSA_MODE = ayanamsa.upper()
|
||||
drik.set_ayanamsa_mode(ayanamsa.upper())
|
||||
const.set_node_mode(node_mode == 'true')
|
||||
@@ -124,7 +125,7 @@ def summarize(rows, prefix=None):
|
||||
return total, match, total - match, match / total if total else 0.0
|
||||
|
||||
|
||||
def write_report(rows, per_sample):
|
||||
def write_report(rows, per_sample, *, ayanamsa='lahiri', sample_count=None):
|
||||
lines = []
|
||||
lines.append('# Jyotish benchmark 第七轮:Chara Dasha / Jaimini 时间线对标')
|
||||
lines.append('')
|
||||
@@ -134,7 +135,8 @@ def write_report(rows, per_sample):
|
||||
lines.append('')
|
||||
lines.append('- 验证当前 skill `scripts/jaimini.py` 的 Chara Dasha 是否可作为正式计算模块使用。')
|
||||
lines.append('- 对标对象:PyJHora `raasi/chara.py` 的 KN Rao method(PyJHora 默认 `CHARA_TYPE_DEFAULT = KN_RAO`)。')
|
||||
lines.append('- 样本仍为10个公开/虚构 smoke case,不包含用户个人资料。')
|
||||
lines.append(f'- 样本:{sample_count if sample_count is not None else len(per_sample)} 个公开 / 虚构样本,不包含用户个人资料。')
|
||||
lines.append(f'- 口径:两边都用 {ayanamsa.capitalize()}、平均交点;本地行星位置取自同口径的 canonical 基线。')
|
||||
lines.append('')
|
||||
lines.append('## 2. 总体结果')
|
||||
lines.append('')
|
||||
@@ -166,9 +168,10 @@ def write_report(rows, per_sample):
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description='Compare the local Chara Dasha against PyJHora KN Rao.')
|
||||
parser.add_argument('--ayanamsa', choices=['lahiri', 'raman'], default='lahiri', help='Ayanamsa for PyJHora; the local baseline must have been built with the same one.')
|
||||
parser.add_argument('--node-mode', choices=['mean', 'true'], default='mean', help='Node mode for PyJHora; must match the local baseline.')
|
||||
parser = argparse.ArgumentParser(description='Compare the local K.N. Rao Chara Dasha against PyJHora.')
|
||||
parser.add_argument('--ayanamsa', choices=['lahiri', 'raman'], default='lahiri',
|
||||
help='Ayanamsa for both sides; the canonical baseline must have been built with the same one.')
|
||||
parser.add_argument('--node-mode', choices=['mean', 'true'], default='mean')
|
||||
args = parser.parse_args(argv)
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from run_pyjhora_compare import baseline_settings_error
|
||||
@@ -180,7 +183,7 @@ def main(argv=None):
|
||||
c = canon(sid)
|
||||
settings_error = baseline_settings_error(c, ayanamsa=args.ayanamsa, node_mode=args.node_mode)
|
||||
if settings_error:
|
||||
parser.error(f'{sid}: {settings_error}; rebuild with run_skill_baseline.py')
|
||||
parser.error(f'{sid}: {settings_error}; rebuild with run_skill_baseline.py --ayanamsa {args.ayanamsa}')
|
||||
local = local_chara(sample, c)
|
||||
pyj = pyjhora_chara(sample, ayanamsa=args.ayanamsa, node_mode=args.node_mode)
|
||||
sign_match = 0
|
||||
@@ -204,7 +207,7 @@ def main(argv=None):
|
||||
writer = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
REPORT.write_text(write_report(rows, per_sample))
|
||||
REPORT.write_text(write_report(rows, per_sample, ayanamsa=args.ayanamsa, sample_count=len(samples)))
|
||||
print(json.dumps({'report': str(REPORT), 'matrix': str(MATRIX), 'rows': len(rows)}, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ Chara Dasha KN Rao Benchmark v1.1
|
||||
|
||||
依赖: pip install jhora numpy
|
||||
"""
|
||||
import argparse
|
||||
import sys, json, os
|
||||
|
||||
# 添加scripts路径
|
||||
@@ -44,7 +45,15 @@ PLANET_NAMES = {
|
||||
}
|
||||
|
||||
|
||||
def run_benchmark():
|
||||
def run_benchmark(ayanamsa='lahiri', node_mode='mean'):
|
||||
# Shared positions come from PyJHora, so the ayanamsa does not change the
|
||||
# algorithm comparison, but it decides which sign each planet is in; set
|
||||
# it explicitly instead of inheriting PyJHora's TRUE_PUSHYA default
|
||||
# (2026-11-04 audit H).
|
||||
const._DEFAULT_AYANAMSA_MODE = ayanamsa.upper()
|
||||
drik.set_ayanamsa_mode(ayanamsa.upper())
|
||||
const.set_node_mode(node_mode == 'true')
|
||||
drik.set_planet_list(set_rahu_ketu_as_true_nodes=(node_mode == 'true'))
|
||||
total_sign = 0; sign_match = 0
|
||||
total_dur = 0; dur_match = 0
|
||||
detail_lines = []
|
||||
@@ -154,6 +163,8 @@ def run_benchmark():
|
||||
result = {
|
||||
"version": "v6.1.11",
|
||||
"benchmark": "chara_dasha_kn_rao",
|
||||
"ayanamsa": ayanamsa,
|
||||
"node_mode": node_mode,
|
||||
"total_sign_match": sign_match,
|
||||
"total_dur_match": dur_match,
|
||||
"total_sign": total_sign,
|
||||
@@ -169,4 +180,8 @@ def run_benchmark():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_benchmark()
|
||||
_parser = argparse.ArgumentParser(description="K.N. Rao Chara Dasha: local helpers vs PyJHora on shared PyJHora positions.")
|
||||
_parser.add_argument("--ayanamsa", choices=["lahiri", "raman"], default="lahiri")
|
||||
_parser.add_argument("--node-mode", choices=["mean", "true"], default="mean")
|
||||
_args = _parser.parse_args()
|
||||
run_benchmark(_args.ayanamsa, _args.node_mode)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
Compares local Jyotish skill canonical output against Swiss Ephemeris Mean/True Node
|
||||
and PyJHora's default rasi_chart node mode. Samples are fictional/public smoke cases.
|
||||
"""
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import math
|
||||
@@ -33,6 +34,9 @@ NAKSHATRAS = [
|
||||
'Mula','Purva Ashadha','Uttara Ashadha','Shravana','Dhanishta','Shatabhisha','Purva Bhadrapada','Uttara Bhadrapada','Revati'
|
||||
]
|
||||
PYJHORA_PLANET_ID = {7: 'Rahu', 8: 'Ketu'}
|
||||
# Both sides get the ayanamsa explicitly; the canonical baseline must have been
|
||||
# built with the same one (2026-11-04 audit H).
|
||||
SIDEREAL_MODES = {'lahiri': swe.SIDM_LAHIRI, 'raman': swe.SIDM_RAMAN}
|
||||
|
||||
|
||||
def norm(deg):
|
||||
@@ -73,9 +77,9 @@ def point_from_lon(lon):
|
||||
}
|
||||
|
||||
|
||||
def swiss_nodes(sample, node_pid):
|
||||
def swiss_nodes(sample, node_pid, ayanamsa='lahiri'):
|
||||
jd = julian_day_utc(sample['birth'])
|
||||
swe.set_sid_mode(swe.SIDM_LAHIRI, 0, 0)
|
||||
swe.set_sid_mode(SIDEREAL_MODES[ayanamsa], 0, 0)
|
||||
flags = swe.FLG_SWIEPH | swe.FLG_SIDEREAL | swe.FLG_SPEED
|
||||
res, _ = swe.calc_ut(jd, node_pid, flags)
|
||||
rahu = point_from_lon(res[0])
|
||||
@@ -104,7 +108,7 @@ def patch_swisseph_for_pyjhora():
|
||||
return swe
|
||||
|
||||
|
||||
def pyjhora_default_nodes(sample):
|
||||
def pyjhora_default_nodes(sample, ayanamsa='lahiri'):
|
||||
# PyJHora is used only as an external benchmark. Do not vendor/copy its code into the skill.
|
||||
if str(PYJHORA_COMPAT) not in sys.path:
|
||||
sys.path.insert(0, str(PYJHORA_COMPAT))
|
||||
@@ -114,8 +118,8 @@ def pyjhora_default_nodes(sample):
|
||||
from jhora import utils, const
|
||||
from jhora.panchanga import drik
|
||||
from jhora.horoscope.chart import charts
|
||||
const._DEFAULT_AYANAMSA_MODE = 'LAHIRI'
|
||||
drik.set_ayanamsa_mode('LAHIRI')
|
||||
const._DEFAULT_AYANAMSA_MODE = ayanamsa.upper()
|
||||
drik.set_ayanamsa_mode(ayanamsa.upper())
|
||||
# Note: charts.rasi_chart -> drik.dhasavarga() defaults to set_rahu_ketu_as_true_nodes=True.
|
||||
b = sample['birth']
|
||||
jd = utils.julian_day_number((b['year'], b['month'], b['day']), (b['hour'], b['minute'], 0))
|
||||
@@ -186,7 +190,7 @@ def write_report(rows):
|
||||
lines.append('')
|
||||
lines.append('- 解释第三轮 PyJHora 对比中 Rahu/Ketu 大量差异的根因。')
|
||||
lines.append('- 对比当前 skill canonical baseline 与 Swiss Ephemeris Mean Node、Swiss Ephemeris True Node、PyJHora rasi_chart 默认输出。')
|
||||
lines.append('- 样本仍为10个公开/虚构 smoke case,不包含用户个人资料。')
|
||||
lines.append('- 样本:公开 / 虚构样本,不包含用户个人资料;岁差两边显式传入。')
|
||||
lines.append('')
|
||||
lines.append('## 2. 总体结果')
|
||||
lines.append('')
|
||||
@@ -223,16 +227,26 @@ def write_report(rows):
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description='Arbitrate the local Rahu / Ketu against Swiss mean / true node and PyJHora.')
|
||||
parser.add_argument('--ayanamsa', choices=sorted(SIDEREAL_MODES), default='lahiri',
|
||||
help='Ayanamsa for every side; the canonical baseline must have been built with the same one.')
|
||||
args = parser.parse_args(argv)
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from run_pyjhora_compare import baseline_settings_error
|
||||
samples = json.loads(DATA.read_text())
|
||||
rows = []
|
||||
for sample in samples:
|
||||
sample_id = sample['id']
|
||||
canonical = json.loads((CANON / f'{sample_id}.canonical.json').read_text())
|
||||
settings_error = baseline_settings_error(canonical, ayanamsa=args.ayanamsa, node_mode='mean')
|
||||
if settings_error:
|
||||
parser.error(f'{sample_id}: {settings_error}; rebuild with run_skill_baseline.py --ayanamsa {args.ayanamsa}')
|
||||
local = local_nodes(sample_id)
|
||||
targets = {
|
||||
'swiss_mean_node': swiss_nodes(sample, swe.MEAN_NODE),
|
||||
'swiss_true_node': swiss_nodes(sample, swe.TRUE_NODE),
|
||||
'pyjhora_default_rasi': pyjhora_default_nodes(sample),
|
||||
'swiss_mean_node': swiss_nodes(sample, swe.MEAN_NODE, args.ayanamsa),
|
||||
'swiss_true_node': swiss_nodes(sample, swe.TRUE_NODE, args.ayanamsa),
|
||||
'pyjhora_default_rasi': pyjhora_default_nodes(sample, args.ayanamsa),
|
||||
}
|
||||
for target_name, target_nodes in targets.items():
|
||||
for body in ['Rahu', 'Ketu']:
|
||||
|
||||
@@ -235,12 +235,17 @@ def write_report(samples, results):
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
def main(argv=None):
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description='Build the local canonical baselines for the benchmark samples.')
|
||||
parser.add_argument('--ayanamsa', choices=['lahiri', 'raman'], default=DEFAULT_AYANAMSA)
|
||||
parser.add_argument('--node-mode', choices=['mean', 'true'], default=DEFAULT_NODE_MODE)
|
||||
args = parser.parse_args(argv)
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
RAW.mkdir(parents=True, exist_ok=True)
|
||||
CANON.mkdir(parents=True, exist_ok=True)
|
||||
samples = json.loads(DATA.read_text())
|
||||
results = [run_sample(sample) for sample in samples]
|
||||
results = [run_sample(sample, ayanamsa=args.ayanamsa, node_mode=args.node_mode) for sample in samples]
|
||||
summary = {'total': len(results), 'ok': sum(1 for r in results if r.get('ok')), 'results': [{'id': r['id'], 'ok': r.get('ok'), 'error': r.get('error')} for r in results]}
|
||||
(OUT / 'run_summary.json').write_text(json.dumps(summary, ensure_ascii=False, indent=2))
|
||||
rows = flatten_rows(results)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# or JYOTISH_SKILL_SCRIPT is provided. Raw output directories are generated locally
|
||||
# and are intentionally not committed.
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import subprocess
|
||||
@@ -13,7 +14,7 @@ from pathlib import Path
|
||||
import swisseph as swe
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SKILL_SCRIPT = Path(__file__).resolve().parents[2] / 'scripts' / 'jyotish_engine.py'
|
||||
SKILL_SCRIPT = Path(__file__).resolve().parents[3] / 'scripts' / 'jyotish_engine.py'
|
||||
PYTHON = Path(sys.executable)
|
||||
DATA = ROOT / 'data/benchmark_samples.json'
|
||||
OUT = ROOT / 'outputs'
|
||||
@@ -31,6 +32,10 @@ PLANETS = {
|
||||
'Rahu': swe.MEAN_NODE,
|
||||
}
|
||||
SIGNS = ['Aries','Taurus','Gemini','Cancer','Leo','Virgo','Libra','Scorpio','Sagittarius','Capricorn','Aquarius','Pisces']
|
||||
# Both sides get the ayanamsa explicitly: the engine default is Raman, and this
|
||||
# script used to compare an engine-default full reading with Lahiri Swiss
|
||||
# positions (2026-11-04 audit H).
|
||||
SIDEREAL_MODES = {'lahiri': swe.SIDM_LAHIRI, 'raman': swe.SIDM_RAMAN}
|
||||
|
||||
|
||||
def normalize(deg):
|
||||
@@ -53,9 +58,9 @@ def julian_day_for_transit(date_str, tz):
|
||||
return swe.julday(utc_dt.year, utc_dt.month, utc_dt.day, hour, swe.GREG_CAL)
|
||||
|
||||
|
||||
def calc_swiss_transit(date_str, tz):
|
||||
def calc_swiss_transit(date_str, tz, ayanamsa='lahiri'):
|
||||
jd = julian_day_for_transit(date_str, tz)
|
||||
swe.set_sid_mode(swe.SIDM_LAHIRI, 0, 0)
|
||||
swe.set_sid_mode(SIDEREAL_MODES[ayanamsa], 0, 0)
|
||||
flags = swe.FLG_SWIEPH | swe.FLG_SIDEREAL | swe.FLG_SPEED
|
||||
planets = {}
|
||||
for name, pid in PLANETS.items():
|
||||
@@ -75,10 +80,10 @@ def calc_swiss_transit(date_str, tz):
|
||||
'degree_in_sign': round(degree_in_sign(ketu_lon), 6),
|
||||
'retrograde': planets['Rahu']['retrograde'],
|
||||
}
|
||||
return {'julian_day_ut': jd, 'planets': planets, 'parameters': {'ayanamsa': 'Lahiri', 'node': 'Mean Node'}}
|
||||
return {'julian_day_ut': jd, 'planets': planets, 'parameters': {'ayanamsa': ayanamsa, 'node': 'Mean Node'}}
|
||||
|
||||
|
||||
def run_full_reading(sample):
|
||||
def run_full_reading(sample, ayanamsa='lahiri'):
|
||||
birth = sample['birth']
|
||||
transit_date = sample.get('today', '2026-06-03')
|
||||
cmd = [
|
||||
@@ -94,6 +99,7 @@ def run_full_reading(sample):
|
||||
'--today', transit_date,
|
||||
'--transit-date', transit_date,
|
||||
'--node-mode', 'mean',
|
||||
'--ayanamsa', ayanamsa,
|
||||
]
|
||||
proc = subprocess.run(cmd, text=True, capture_output=True)
|
||||
raw_path = RAW / f"{sample['id']}.transit_full_reading.json"
|
||||
@@ -105,13 +111,16 @@ def run_full_reading(sample):
|
||||
return data
|
||||
|
||||
|
||||
def compare_sample(sample):
|
||||
def compare_sample(sample, ayanamsa='lahiri'):
|
||||
transit_date = sample.get('today', '2026-06-03')
|
||||
data = run_full_reading(sample)
|
||||
data = run_full_reading(sample, ayanamsa)
|
||||
reported = str((data.get('birth_info') or {}).get('ayanamsa_name') or '').lower()
|
||||
if reported and reported != ayanamsa:
|
||||
raise RuntimeError(f"{sample['id']}: engine reported ayanamsa {reported}, comparison asked for {ayanamsa}")
|
||||
modules = data.get('modules', {})
|
||||
local_transit = modules.get('transit_positions', {})
|
||||
local_multi = modules.get('transit_multi_reference', {})
|
||||
swiss = calc_swiss_transit(transit_date, sample['birth']['tz'])
|
||||
swiss = calc_swiss_transit(transit_date, sample['birth']['tz'], ayanamsa)
|
||||
(TRANSIT_OUT / f"{sample['id']}.swiss_transit.json").write_text(json.dumps(swiss, ensure_ascii=False, indent=2))
|
||||
|
||||
rows = []
|
||||
@@ -184,7 +193,7 @@ def compare_sample(sample):
|
||||
return rows
|
||||
|
||||
|
||||
def write_report(rows):
|
||||
def write_report(rows, *, ayanamsa='lahiri', sample_count=None):
|
||||
total = len(rows)
|
||||
matches = sum(1 for r in rows if r['status'] == 'match')
|
||||
mismatches = [r for r in rows if r['status'] == 'mismatch']
|
||||
@@ -202,8 +211,8 @@ def write_report(rows):
|
||||
lines.append('## 1. 范围')
|
||||
lines.append('')
|
||||
lines.append('- 对比对象:full-reading.modules.transit_positions / transit_multi_reference vs 直接调用 Swiss Ephemeris。')
|
||||
lines.append('- 样本:10个公开/虚构 smoke case,不包含真实用户个人资料。')
|
||||
lines.append('- 配置:Sidereal Lahiri,Mean Node,transit date 使用样本 today 字段。')
|
||||
lines.append(f'- 样本:{sample_count} 个公开 / 虚构样本,不包含真实用户个人资料。')
|
||||
lines.append(f'- 配置:Sidereal {ayanamsa.capitalize()}(两边显式传入),Mean Node,transit date 使用样本 today 字段。')
|
||||
lines.append('- 重点:确认 full-reading 的多参考点 Transit 不再使用 natal positions fallback,而是使用真实过境行星位置。')
|
||||
lines.append('')
|
||||
lines.append('## 2. 总体结果')
|
||||
@@ -240,20 +249,24 @@ def write_report(rows):
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description='Compare full-reading transit positions with Swiss Ephemeris.')
|
||||
parser.add_argument('--ayanamsa', choices=sorted(SIDEREAL_MODES), default='lahiri',
|
||||
help='Ayanamsa passed to both the engine and the Swiss Ephemeris side.')
|
||||
args = parser.parse_args(argv)
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
RAW.mkdir(parents=True, exist_ok=True)
|
||||
TRANSIT_OUT.mkdir(parents=True, exist_ok=True)
|
||||
samples = json.loads(DATA.read_text())
|
||||
all_rows = []
|
||||
for sample in samples:
|
||||
all_rows.extend(compare_sample(sample))
|
||||
all_rows.extend(compare_sample(sample, args.ayanamsa))
|
||||
csv_path = OUT / 'transit_true_comparison_matrix.csv'
|
||||
with csv_path.open('w', newline='') as f:
|
||||
writer = csv.DictWriter(f, fieldnames=['sample_id','body','field','local_skill','swiss_direct','delta','status'])
|
||||
writer.writeheader()
|
||||
writer.writerows(all_rows)
|
||||
report = write_report(all_rows)
|
||||
report = write_report(all_rows, ayanamsa=args.ayanamsa, sample_count=len(samples))
|
||||
print(json.dumps({
|
||||
'rows': len(all_rows),
|
||||
'matches': sum(1 for r in all_rows if r['status'] == 'match'),
|
||||
|
||||
@@ -21410,3 +21410,80 @@
|
||||
- 关联记录:BUG-1560、BUG-1561。
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/audit-vargas-20261104`(未推送)。
|
||||
|
||||
## BUG-1565 | 时段点夜段分配表周三到周六整体错一格:Maandi 和 Gulika 不在同一个土星段,Kaala 等四点落在别的星段
|
||||
|
||||
- 状态:resolved(引擎 + 回归测试;未部署)
|
||||
- 首次发现 / 最近更新:2026-11-04(数据核对 H:19 张对照盘逐张用 Swiss Ephemeris 独立重算八分段)/ 2026-11-04
|
||||
- 影响面:`scripts/gulika.py` `NIGHT_RULERS`,经 `_temporal_upagraha` 影响 `calculate_upagrahas` 的 Maandi、Kaala、Mrityu、Artha_Praharaka、Yama_Ghantaka:星盘页特殊点层(`chart_view_points/api.py`)、完整解读的 upagraha 包(`_build_natal_foundation_modules`)。Gulika 本身走 `SATURN_PART_START`,本来就对,不受影响。
|
||||
- 现象:19 张盘里 7 张夜生盘(乔布斯周四夜、奥巴马 / 泰勒 / 洛杉矶周五夜、琵雅芙 / 伦敦周六夜、齐达内周四夜)这五个点全部偏一段(几度到几十度);例:乔布斯(Raman)Maandi 射手 26.93°,应为射手 3.35°,与 Gulika(天蝎 22.80°,同一土星段的起点)不在同一段。
|
||||
- 触发条件:出生在周三、周四、周五、周六的夜段(日落到次日日出;按开启该段的日落所在日期算星期)。
|
||||
- 根因:夜段八分的第一段应由「当天星期主数起第五位」的行星主管(周六夜从水星起),表里周三到周六四行都从前一位起(周六夜从火星起)。白天表与 `SATURN_PART_START` 是对的,夜段表手写时错位,两张表没有互相校验。
|
||||
- 修复:改正 `NIGHT_RULERS` 的周三到周六四行,与 PyJHora `const.night_rulers` 同表;注释写明规则。
|
||||
- 验证:`tests/test_audit_h_dasha_sky_20261104.py`:七天白天 / 夜段第一段与「星期主 / 第五位」一致、土星所在段与 `SATURN_PART_START` 一致;乔布斯、泰勒、伦敦三张周四到周六夜生盘五个时段点与独立 Swiss 计算差 ≤ 0.01°,Maandi 的段位 = Gulika 段位 + 1/16。修复后 19 张盘 × 2 种岁差 114/114 与独立计算一致(修复前 79/114)。`test_gulika*.py` 通过。
|
||||
- 未改(口径,列给占星师):PyJHora 的八段循环把「无主段」放在土星段之后,BPHS 读法是第八段无主;Gulika / Maandi 两种读法相同,Kaala 等四点在 114 项里有 42 项不同。PyJHora 4.8.6 `upagraha_longitude` 夜段用次日日出作起点、以「日落与次日日出钟点差」作夜长,夜生盘 Gulika 与本仓差 128–177°,夜段不能作对照。
|
||||
- 防复发:上述测试锁两张表互相一致。
|
||||
- 关联记录:BUG-1388(夜段终点用真实次日日出)、BUG-513。
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/audit-dasha-sky-20261104`(未推送)。
|
||||
|
||||
## BUG-1566 | K.N. Rao Chara Dasha 天蝎 / 水瓶的宫主一律取计都 / 土星:按大运星座本身比尊贵,没有「一主在本宫取另一主」
|
||||
|
||||
- 状态:resolved(引擎 + 回归测试;未部署)
|
||||
- 首次发现 / 最近更新:2026-11-04(数据核对 H:19 张盘 × Raman / Lahiri 与 PyJHora 4.8.6 `chara` KN_RAO 逐段对照)/ 2026-11-04
|
||||
- 影响面:`scripts/jaimini.py` `_resolve_chara_dasha_lord`、`_jaimini_stronger_planet`(新增 `_co_lord_strength`、`_rasi_drishti`);连带 `calc_chara_dasha` / `calc_chara_dasha_with_antardasha` 天蝎、水瓶两段的宫主、年数与其后各段起止:咨询 `modules.chara_dasha`(数据卡 Chara 当前运)、`/api/dasha/chara` 与星盘页 Chara 宫主标注(issue #163)、完整解读 Jaimini、报告 Rashi 大运章 Chara 表。Narayana(两条时间轴)不用这两个函数,不受影响。
|
||||
- 现象:19 张盘每种岁差 228 段里 16 段年数与 PyJHora 不同,全部在天蝎、水瓶。布什公开盘计都在天蝎、火星在狮子,天蝎给 13 年(PyJHora 9);泰勒水瓶 1 年(PyJHora 11);乔布斯水瓶 5 年(PyJHora 1)。
|
||||
- 触发条件:盘中有天蝎或水瓶大运(每张盘都有),且计都 / 土星不是按规则该取的那一颗。
|
||||
- 根因:`_jaimini_stronger_planet` 用大运星座(天蝎 / 水瓶)本身判断两颗主星的尊贵,与它们实际所在星座无关:天蝎里计都「入旺」永远胜火星,水瓶里土星「本宫」永远胜罗睺,结果与盘面无关;也没有「一主在本宫、另一主在外,取在外者」这条(K.N. Rao《Predicting through Jaimini's Chara Dasha》;Rath《Narayana Dasa》p.43 (c);PyJHora basic rule;`references/prashna-complete-guide.md` 双主星第 1 条)。函数注释说「基于 PyJHora _stronger_planet_new」,实际没有照它的次序。
|
||||
- 修复:一主在本宫、另一主在外,取在外者;两主都在外,按各自所在星座比:① 同宫星数多者;② 木星 / 水星 / 定位星同宫或星座相照者多;③ 所在星座尊贵(旺 > 本宫 > 其他 > 落);④ 双体 > 固定 > 活动;⑤ 星座内度数大者;最后天然力量。次序与 PyJHora、Rath p.43 (d)、prashna 指南一致。两主都在本宫仍按比较结果(年数都是 12),未改。
|
||||
- 验证:`tests/test_audit_h_dasha_sky_20261104.py`(布什、泰勒、乔布斯三张公开盘两种岁差的天蝎 / 水瓶年数等于 PyJHora 参照值,只存数字;一主在本宫取另一主;两主都在外时随盘面翻转);`tests/test_chara_dasha_board_lord.py` 一条按「原值 / 新值 / 原因」改写(计都在天蝎、火星在天秤:原值 计都 / 天蝎,新值 火星 / 天秤),另加一例火星在天蝎时取计都。修复后与 PyJHora 年数 225/228(Raman)、226/228(Lahiri),剩余差异均为 PyJHora 自己的取法:把上升算作同宫星(东京盘天蝎、齐达内 Raman 水瓶),尊贵一步只认入旺、第二颗星的入旺判断写成和自己比(伦敦盘天蝎)。K.N. Rao 书中越南女士例(1958,Lahiri)前 5 段星座与年数(含水瓶 3 年)与原书一致。前端金样 `chart-view-golden.json`、`chara-zero-year-1985-08-03-beijing.golden.json` 的 Chara 段按同一张盘的经度用修复后的引擎重采(前者天蝎宫主计都 8 年 → 火星 4 年,两份的小运次序随 BUG-1567 变),先用改前代码重算与原金样逐字节相同,确认输入一致;读它们的前端测试 256 条通过。咨询数据卡两份大金样(`consult-evidence-card-golden.json`、`consult-biography-backtest-golden.json`)里的 `chara_dasha` 仍是旧引擎采样(乔布斯天蝎宫主计都、水瓶 5 年),未重采,下次整体重采时更新。`tests/test_rashi_dashas_native.py` 的 PyJHora 4.8.7 对照(乔布斯、奥巴马、爱因斯坦)三盘 Chara 年数改前各差一段,改后与 PyJHora 逐段相同,fixture 的 `native.chara.years` 与 `diff_steps` 按「原值 / 新值 / 原因」更新(PyJHora 行未动)。
|
||||
- 冻结身份(ERR-110):`jaimini.py` 在生时校正冻结身份里(校正只用 `calc_arudha_padas`,未改)。与 BUG-1567 一起按 ERR-110 改 FREEZE / REPORT 常量后用各脚本 `--freeze` 新建记录并回放,标签 `audit_h_2026_11_04`(`docs/research/sealed_holdout_rerun_audit_h_2026_11_04.{freeze.json,json}`、`docs/research/reported_offset_audit_h_2026_11_04.{freeze.json,json}`),合同 `references/rectification_sealed_holdout.v1.json` 按 moon_paksha 先例轮换,旧记录逐字节保留。与 `moon_paksha_2026_11_04` 逐条逐字段比:密封 20 条全部相同、`metrics` 不变;报时窗口 900 条全部相同、`summary` 不变(只有冻结身份哈希与路径不同);相对 09-20 历史报告的 changed_trial_count 仍为 8 / 581。符合预期:校正打分只调 `jaimini.calc_arudha_padas`,Chara Dasha 不进打分。
|
||||
- 防复发:上述测试;同名「动态判定」函数要用盘面上的位置,不能用被判定的星座代入。
|
||||
- 关联记录:BUG-1567(同轮 Chara 小运)、BUG-1218 / BUG-1227(Narayana Rath 版双主星,p.43)、issue #163。
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/audit-dasha-sky-20261104`(未推送)。
|
||||
|
||||
## BUG-1567 | K.N. Rao Chara Dasha 小运:每个大运都沿用上升序右移一位,只有第一运对
|
||||
|
||||
- 状态:resolved(引擎 + 回归测试;未部署)
|
||||
- 首次发现 / 最近更新:2026-10-08 已在 `docs/tasks/PROGRESS-rashi-dashas-native-20261008.md` 记为与书不符、未改 `jaimini.py`;2026-11-04 数据核对 H 按书逐运核对后修复。
|
||||
- 影响面:`scripts/jaimini.py` `calc_chara_dasha_with_antardasha`(`profile="kn_rao"`)的 `antardashas` 与其下 `pratyantar_dashas` 次序:报告 Rashi 大运章 Chara 表的子运(`rashi_dashas_native._chara`)、`/api/dasha/chara`、完整解读 Jaimini 的 Chara 子运。Goel 三个档案不变。
|
||||
- 现象:19 张盘 × 12 运,只有每张盘的第一运(19/228)符合书中规则。K.N. Rao 书中越南女士例(pdf pp.51–52)摩羯大运第一小运是射手,本仓给白羊。
|
||||
- 触发条件:任何盘的第二运起。
|
||||
- 根因:子运序列写成「全盘大运序右移一位」(注释称 PyJHora method=2;PyJHora 实为不移位、并注明「逻辑未实现」)。K.N. Rao《Predicting through Jaimini's Chara Dasha》印页 35–36:每个大运按它自己的顺逆(其第 9 宫若为偶足则逆行),从下一星座起,本运星座排最后。
|
||||
- 修复:`kn_rao` 档案每运单独起序:第 9 宫偶足则逆行,否则顺行,从下一星座起,第 12 段为本运星座;`antardasha_method` 写明口径。其余档案保持原序并注明「未找到出处,沿用」。
|
||||
- 验证:`tests/test_audit_h_dasha_sky_20261104.py`:每运末段为本运星座、方向按其第 9 宫;天蝎大运次序等于原书 pdf pp.49–50 金样(天秤 … 天蝎);越南女士例 5 段大运与原书一致、摩羯首个小运为射手;Goel 档案次序不变。修复后 228/228 符合书中规则(修复前 19/228)。PyJHora 的 KN_RAO 子运是全盘大运序不移位(0/57),不作参照。
|
||||
- 冻结身份(ERR-110):与 BUG-1566 同一次重冻,见 BUG-1566。
|
||||
- 防复发:上述测试与书中金样。
|
||||
- 关联记录:BUG-1566。
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/audit-dasha-sky-20261104`(未推送)。
|
||||
|
||||
## BUG-1568 | 出生 Panchanga 的星期(vara)和时主(hora)按公历日:日出前出生算成当天
|
||||
|
||||
- 状态:resolved(引擎 + 回归测试;未部署)
|
||||
- 首次发现 / 最近更新:2026-11-04(数据核对 H:19 张盘与 PyJHora `drik.vaara`、本仓 Shadbala 的星期对照)/ 2026-11-04
|
||||
- 影响面:`scripts/jyotish_engine.py` `_build_natal_foundation_modules` 的 `panchanga.raw.vara` 与 `hora`:完整解读 `modules.panchanga`、报告「基础资料表」与 parity 章出生 Panchanga、读这两项的地方。Shadbala 的星期 / 时主本来就按日出算,不受影响;星盘页出生历法的「星期」是公历星期,未改(列给产品)。
|
||||
- 现象:19 张盘 4 张日出前出生(德里测试盘、泰勒、琵雅芙、齐达内)vara 写成公历当天,时主随之错:泰勒(1932-02-27 周六 02:30 伦敦)写周六,应为周五,时主应为火星。
|
||||
- 触发条件:出生在午夜后、当天日出前。
|
||||
- 根因:时主小时数从最近一次日出算(前一天的日出),星期却取出生的公历日期。吠陀日从日出到日出(PyJHora `vedic_weekday`、本仓 `shadbala._calendar_lords` 都是这样),两者混用。
|
||||
- 修复:星期取「开始计时的那次日出」所在的公历日期。日出口径(apparent)不变。
|
||||
- 验证:`tests/test_audit_h_dasha_sky_20261104.py`:泰勒 vara 周五、第 20 个时、时主火星;布什(日出后)仍为周六。19 张盘 vara 与 PyJHora 吠陀星期 19/19(修复前 15/19);与 Shadbala 时主一致 16/19,余 3 张(卡罗、布什、齐达内)差在日出口径(Panchanga 用 apparent、Shadbala 用 Hindu,差 4–6 分钟,恰好跨整点),列给占星师。
|
||||
- 防复发:上述测试。
|
||||
- 关联记录:BUG-1388(日出口径分用)、BUG-1546(PyJHora 时主公式错,不作参照)。
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/audit-dasha-sky-20261104`(未推送)。
|
||||
|
||||
## BUG-1569 | 四个对照脚本一边写死 Lahiri、一边吃引擎默认(Raman)或不查基线口径;两个脚本引擎路径指错
|
||||
|
||||
- 状态:resolved(工具 + 回归测试;不涉及线上)
|
||||
- 首次发现 / 最近更新:2026-11-04(数据核对 H,按同日 74169e1b 的做法逐个检查)/ 2026-11-04
|
||||
- 影响面:`benchmarks/jyotish/scripts/run_chara_dasha_compare.py`、`run_chara_dasha_knrao.py`、`run_transit_true_compare.py`、`run_node_mode_compare.py`、`run_skill_baseline.py`(`main`)。只影响对照报告,不影响产品输出。
|
||||
- 现象:引擎默认岁差改为 Raman 后,这几份对照会把 1.446° 的岁差差当成算法差;`run_chara_dasha_compare.py` 与 `run_transit_true_compare.py` 的引擎路径指到不存在的 `benchmarks/scripts/`,直接跑不起来。
|
||||
- 根因:PyJHora / Swiss 一侧写死 `LAHIRI` / `SIDM_LAHIRI`,引擎一侧不传 `--ayanamsa`,canonical 基线的口径也不核对;`parents[2]` 应为 `parents[3]`。
|
||||
- 修复:四个脚本都加 `--ayanamsa`(Chara 两个另加 `--node-mode`),两边显式传入;读 canonical 基线的两个脚本用 `run_pyjhora_compare.baseline_settings_error` 拒绝口径不符的基线;Transit 脚本核对引擎回报的岁差;路径改正;`run_skill_baseline.py` 命令行加 `--ayanamsa / --node-mode`。
|
||||
- 验证:`tests/test_benchmark_ayanamsa_plumbing_20261104.py`;用 Raman 重建 19 张 canonical 基线后重跑:Chara 对照 MD 星座 228/228、年数 225/228(BUG-1566 后);K.N. Rao 共享位置对照 Raman 年数 117/120、Lahiri 118/120;Transit 646/646(含 19 张盘 171 项度数、逆行、多参考点星座);节点对照 Swiss 平均交点 152/152、真交点与 PyJHora 默认各 100/152(口径差,预期)。用 Lahiri 对 Raman 基线跑 Chara 对照,脚本拒绝并提示重建。
|
||||
- 防复发:上述测试;新写对照脚本两边都要显式传岁差与交点,并核对基线口径。
|
||||
- 关联记录:74169e1b(`run_skill_baseline.py`、`run_pyjhora_compare.py` 同类修正)。
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/audit-dasha-sky-20261104`(未推送)。
|
||||
|
||||
@@ -19,6 +19,7 @@ Purpose: read this file before substantial project work. It exists to stop repea
|
||||
- 数据卡宫行加 Ashtakavarga 强 / 中 / 弱(worktree `/workspace/wt-card-av`,`codex/card-ashtakavarga-20261104`):`pre_work_check.py` 第一次 exit 1(`fragment_scan` 90 s、`focused_tests` 45 s 超时,当时同机在建 worktree、跑卡片导出),机器空下来重跑 pass、六项全部 ok。分档沿用星盘页 `savBand`(≥ 28 强、25–27 中、≤ 24 弱),来源写在 `SAV_BAND_SOURCE`。注意:事业卡的 `sav_by_house`、财富 / 综合卡的 `ashtakavarga` 块本来就把原始与净化后的 SAV 点数给了模型,本单没动。
|
||||
- BUG-1557 月亮 Paksha 口径(worktree `/workspace/wt-moon`,`codex/moon-paksha-bphs-20261104`):`pre_work_check.py` exit 0、六项全部 ok。精确路径 Kala 里月亮 ×2(Raman)加 Chesta = Paksha(BPHS 27.18)同时用、Chesta 又取 ×2 后的值,总分 4P;改为 BPHS-Santhanam(不乘 2,Chesta = Paksha),`method_variants.moon_paksha` 标口径。`shadbala.py` 在冻结身份里,按 ERR-110 重冻为 `moon_paksha_2026_11_04`:密封 20 条、报时 900 条与 `data_audit_2026_11_04` 全同。教训:两家来源各自「×2」「Chesta = Paksha」都对,拼在一起就错;引进一条古典规则时先看同一来源怎么处理相邻分量。Shadbala 相关测试改前改后失败清单一致(同上 10 条 + 其他既有 12 条);Python quick 门一次通过。
|
||||
- 数据核对第二轮 G(worktree `/workspace/wt-g`,`codex/audit-vargas-20261104`):分盘 D3–D60、Arudha、Chara Karaka 对 PyJHora 与 jyotishganit,19 张盘、Raman / Lahiri、两边显式传岁差与交点。BUG-1560~1562:`varga.py` 的 D16/D20/D27/D45 起点、`divisional_charts_extended.py` 的 D16/D20/D27/D30/D40/D45、`trimshamsa_d30.py` 的偶数星座 D30 不符 BPHS,已改,同经度 190 / 190 与 PyJHora 一致。`varga.py` 的偶数星座 D30 同样错,但它进生时校正打分:先改了并按 ERR-110 重冻,回放变了(密封头名率 0.50 → 0.45,报时 166 / 900 条名次或分钟误差变),快速门 memoization / dated-transitions golden 9 条红,按先例要升 scoring-13,属产品决定,撤回,留 KNOWN DEFECT 注释与 strict xfail。最终重冻 `audit_g_2026_11_04`:密封 20 条、报时 900 条与 `moon_paksha_2026_11_04` 全同。教训:改分盘表前先查它是否进校正打分;进的话,除了 ERR-110 重冻,还要看 `tests/test_rectification_engine_memoization.py` 的 golden 与算法身份。`run_arudha_compare.py`、`run_chara_dasha_compare.py` 原来 PyJHora 那边写死 Lahiri,已改为 `--ayanamsa` / `--node-mode` 并拒绝口径不符的基线;`run_chara_dasha_compare.py` 还从 `benchmarks/scripts` 导入 jaimini(路径错),已改。`run_node_mode_compare.py`、`run_swiss_*_compare.py`、`run_transit_true_compare.py` 仍写死 Lahiri,未动。jyotishganit 的偶数星座 D30 表本身不符 BPHS,不能当 D30 对照;`tests/test_varga_vedastro_mode.py` 9 条在 origin/staging 上就失败(D2 上升),与本单无关。回放脚本不加 `--freeze` 时只把报告打到 stdout,要自己重定向成 REPORT;临时冻结记录要放在仓库里并传绝对路径(`reported_offset_sweep.py` 对冻结路径取 `relative_to(ROOT)`,放 scratchpad 或传相对路径会在跑完 900 条后才报错)。
|
||||
- 数据核对 H(worktree `/workspace/wt-h`,`codex/audit-dasha-sky-20261104`):`pre_work_check.py` exit 0、六项全部 ok。Narayana / Chara / 行运 / 日出 / Gulika 两种岁差各 19 张盘对 PyJHora 与 Swiss 直算:行运 190/190、Hindu 日出与 PyJHora 0 秒、Narayana Rath 版差异全是流派(起运宫、双主星、水星处女),第二轮规则与 PyJHora 同;修 BUG-1565(夜段时段点表错一格)、BUG-1566 / 1567(K.N. Rao Chara 双主星、小运次序)、BUG-1568(日出前出生的星期)、BUG-1569(四个对照脚本岁差)。`jaimini.py` 在冻结身份里,按 ERR-110 重冻为 `audit_h_2026_11_04`:密封 20 条、报时 900 条与 `moon_paksha_2026_11_04` 全同。教训:① PyJHora 4.8.6 夜生的 Gulika / 时段点算法本身错(以次日日出为起点、钟点差作夜长),夜生盘不能拿它作对照,要用 Swiss 独立重算;② 一个函数名叫「动态判定」不等于真按盘面判定,对照时先看结果是否随盘面变;③ 同机多会话同时跑报时回放与全量测试,相关测试 91 个文件跑了一个多小时,比对改前改后要用同一时段的两个 worktree 并行跑。`test_narayana_rath_book_tables.py::test_rath_profile_is_display_only` 在未改动的基线上就失败(`scripts/annual_service/package.py` 导入 `narayana_rath`)。
|
||||
|
||||
## 2026-11-03 · 输入框单行起步(方案 2,worktree `/workspace/wt-composer`)
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"record_version": "reported-offset-native-candidate-date-v3",
|
||||
"frozen_at_utc": "2026-11-04T08:34:36.741288+00:00",
|
||||
"ayanamsa": "raman",
|
||||
"node_mode": "mean",
|
||||
"radii_minutes": [
|
||||
15,
|
||||
30,
|
||||
60
|
||||
],
|
||||
"offsets_minutes": [
|
||||
-30,
|
||||
-20,
|
||||
-15,
|
||||
-10,
|
||||
-8,
|
||||
-5,
|
||||
-3,
|
||||
0,
|
||||
3,
|
||||
5,
|
||||
8,
|
||||
10,
|
||||
15,
|
||||
20,
|
||||
30
|
||||
],
|
||||
"minute_step": 1,
|
||||
"dataset": "references/real_case_calibration/minute_rectification_holdout_v3.json",
|
||||
"dataset_sha256": "45aa76ab79dfc6c6404b6252083e1aa5c7e2e2b7de811fc11042a8ad2da3f0ea",
|
||||
"dataset_benchmark_id": "minute_rectification_fact_ranker_v4_holdout_v3",
|
||||
"implementation_sha256": "a6a60f59f7405496453000abef6f9f3074aa8d31c2a4dc9a8f18c6d812837168",
|
||||
"implementation_sha256_prefix": "a6a60f59f7405496",
|
||||
"historical_artifacts_manifest_sha256": "6102a26a840be207b5858b3a4c0509274468ae9071e86d308cbbac7371d6864e",
|
||||
"production_scoring_files": [
|
||||
"scripts/active_rectification_event_engine.py",
|
||||
"scripts/active_rectification_events.py",
|
||||
"scripts/ashtakavarga.py",
|
||||
"scripts/dasha_analyzer.py",
|
||||
"scripts/divisional_charts_extended.py",
|
||||
"scripts/domain_calculation_service.py",
|
||||
"scripts/functional_benefics.py",
|
||||
"scripts/jaimini.py",
|
||||
"scripts/minute_rectification_fact_ranker_v4.py",
|
||||
"scripts/minute_rectification_feature_facts_v4.py",
|
||||
"scripts/narayana_dasha.py",
|
||||
"scripts/rectification/api_service.py",
|
||||
"scripts/rectification/candidate_contrast.py",
|
||||
"scripts/rectification/candidate_window.py",
|
||||
"scripts/rectification/case_holdout.py",
|
||||
"scripts/rectification/contracts.py",
|
||||
"scripts/rectification/dasha_transition_proximity.py",
|
||||
"scripts/rectification/decision_policy.py",
|
||||
"scripts/rectification/event_probes.py",
|
||||
"scripts/rectification/refinement_packet.py",
|
||||
"scripts/rectification/scoring_service.py",
|
||||
"scripts/shadbala.py",
|
||||
"scripts/varga.py"
|
||||
],
|
||||
"production_scoring_sha256": "e20ff44f33b57db60bb6661ac235bd9f81425ced3fa632e7ea4b31f48b41237e",
|
||||
"research_files": [
|
||||
"scripts/research/reported_offset_sweep.py",
|
||||
"scripts/research/cluster_width_lib.py",
|
||||
"scripts/research/probe_supply_after_six.py",
|
||||
"scripts/research/sealed_holdout_rerun.py",
|
||||
"scripts/minute_rectification_blind_eval.py",
|
||||
"scripts/minute_rectification_fact_blind_eval_v4.py",
|
||||
"scripts/minute_rectification_holdout_validator.py"
|
||||
],
|
||||
"research_implementation_sha256": "25d970c09cb4d6a71e90c05d9580be5b81a63cbc338cc7cd4bf768a8aa45efd9",
|
||||
"file_sha256": {
|
||||
"scripts/active_rectification_event_engine.py": "dced0b398ad90bdec59c743a55ffe01613306127c17a1e44c8c9cfb569c57841",
|
||||
"scripts/active_rectification_events.py": "03e333ccf1103caf685dfe50fe890ab1793e945037085f50ce834a4a3b232222",
|
||||
"scripts/ashtakavarga.py": "c524da3f54b2ed9c080b78b33854854a0ca3d5d79b7e536bb1c3bc2be3e02852",
|
||||
"scripts/dasha_analyzer.py": "3949b96813217fe624899856afec9f0b787c4cb0fea3a9a0d961990dbd11943a",
|
||||
"scripts/divisional_charts_extended.py": "e9820e80f14fddc86ab5495d11455d16c2d3c9502a9a242fcf9efc41186e0d11",
|
||||
"scripts/domain_calculation_service.py": "66923a2bc69daf65c965d2973c4b9234edbec5147909eaf3609b6b377ce3c7ca",
|
||||
"scripts/functional_benefics.py": "cbaac3b8c09b97f8724b5300e9a402cbf77089af6c7db8788550d40fd35ac57b",
|
||||
"scripts/jaimini.py": "706d20dcff89079063fd1857a36f19419604ef81c80227921c7a2ced60011ac9",
|
||||
"scripts/minute_rectification_blind_eval.py": "07aa30d8bad3278669495d5fea8be937aaf6367c8af980ff4f2fe9aa84ccc6ff",
|
||||
"scripts/minute_rectification_fact_blind_eval_v4.py": "bdf460e3467208e8b87345c5beb2128f3e22de7eeffd68f72b281b3ad9affc92",
|
||||
"scripts/minute_rectification_fact_ranker_v4.py": "f1a0d8d2482cd754d2e7f253a9606b0373519435a2bf58cfc3753e2b9ae0fb76",
|
||||
"scripts/minute_rectification_feature_facts_v4.py": "3d899a2cdd4825b8e32062443d487edcd2173a4ea55a36eb9f3abe587c0faf0f",
|
||||
"scripts/minute_rectification_holdout_validator.py": "b60b1c752e58672beaee8b36eb3b2348169726cda070ec969db0f13e7ed715af",
|
||||
"scripts/narayana_dasha.py": "9a847d3b8d05f90d4f275079c97af1f71e83a74f930e14bd8f3f37b293d91d1e",
|
||||
"scripts/rectification/api_service.py": "089730097d3872a4f7b5d7dd5574bb7e9ab4d2e246d4a380800bc824d7979b95",
|
||||
"scripts/rectification/candidate_contrast.py": "eee7da71c0574a08876404b78ca111153fa95fb83ace82b9fdca141d1992e273",
|
||||
"scripts/rectification/candidate_window.py": "2fbc5058b12049b15fca375cdc125b8283e19f9f3e4bedcc291e344501891bef",
|
||||
"scripts/rectification/case_holdout.py": "fe0e699b617d9e216acdfeff87af4221c1d6f4c101bf33e1dbf14e2bcd75a8b5",
|
||||
"scripts/rectification/contracts.py": "c6dc6bd230e38f9f2126c224aae9ddbd6415304c15340f8c7cd3f06f6ef3160f",
|
||||
"scripts/rectification/dasha_transition_proximity.py": "5ce4ed27b0f06c681abc34e5fd998fafb10c153a0070aa36c99ddfa869a2cbf2",
|
||||
"scripts/rectification/decision_policy.py": "80e8de4680d19a052213bb821d2a9c94143a2962235aeb27f6ad81374d2e60d5",
|
||||
"scripts/rectification/event_probes.py": "f5ca9d54501f67a1546fcf5b01da0c1277dec2e2c6df481884e8900d14909a7c",
|
||||
"scripts/rectification/refinement_packet.py": "dbeea5aa780889785ce850f3881e8b7f67bb44594e9bc382c65b2e730884a2f4",
|
||||
"scripts/rectification/scoring_service.py": "fa056fa30d9e6ffa4b75295fc49c793e35b26ebeca64ca843d26c8abef3050f2",
|
||||
"scripts/research/cluster_width_lib.py": "1d8f2ffa7795d62069deebb2de8e16749317037d4ed3f24a2c0092676e5cfcc6",
|
||||
"scripts/research/probe_supply_after_six.py": "098a5b4398d6f8d996917d809c2fc922ad64031d3b70660f8ef7c876aba92438",
|
||||
"scripts/research/reported_offset_sweep.py": "13c29205f5315383a4d4af04b4c69a244d8f175158beb20f3d9e47b62fd2683f",
|
||||
"scripts/research/sealed_holdout_rerun.py": "2a4dfd27bb8b78d90d81f2b534091a1f82f748bc873e9943609256774a4313bc",
|
||||
"scripts/shadbala.py": "352a9dd91c9be6dd7a294b1dc24c62173e8f08db5fc36c453dc5dcd7e0fd62d7",
|
||||
"scripts/varga.py": "1bca389e4f36d293a755de175163c3f39b42bab5831cb6f7828830899f9f177e"
|
||||
},
|
||||
"hash_scope": "explicit_identity_file_sets_not_a_transitive_dependency_lock",
|
||||
"scorer": "native_event_contribution_matrix_not_shadow_fact_ranker",
|
||||
"delivery": "initial_signature_clusters_peak_gap_lt_8_envelope_no_answers_no_elimination",
|
||||
"rank": "score_desc_then_sha256(benchmark_id:case_id:candidate_time)",
|
||||
"grid": "every_minute_inclusive_not_production_two_minute_sampling",
|
||||
"cross_midnight": "date_aware_candidates_distance_and_native_single_matrix",
|
||||
"evaluator_sha256": "13c29205f5315383a4d4af04b4c69a244d8f175158beb20f3d9e47b62fd2683f",
|
||||
"replay_revision": "native_candidate_date_v3",
|
||||
"supersedes": "candidate_date_grouped_v2_identity_and_path_not_assumed_numerically_wrong",
|
||||
"is_blind_evaluation": false,
|
||||
"truth_hidden_from_ranker": true,
|
||||
"official_valid_independent_blind": false,
|
||||
"official_blind_trial_count": 0,
|
||||
"results_previously_seen": true,
|
||||
"must_not_use_for_tuning": true
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"record_version": "exposed-v3-fixed-protocol-cross-midnight-rerun-v2",
|
||||
"extended_identity": {
|
||||
"historical_artifacts_manifest_sha256": "6102a26a840be207b5858b3a4c0509274468ae9071e86d308cbbac7371d6864e",
|
||||
"production_scoring_files": [
|
||||
"scripts/active_rectification_event_engine.py",
|
||||
"scripts/active_rectification_events.py",
|
||||
"scripts/ashtakavarga.py",
|
||||
"scripts/dasha_analyzer.py",
|
||||
"scripts/divisional_charts_extended.py",
|
||||
"scripts/domain_calculation_service.py",
|
||||
"scripts/functional_benefics.py",
|
||||
"scripts/jaimini.py",
|
||||
"scripts/minute_rectification_fact_ranker_v4.py",
|
||||
"scripts/minute_rectification_feature_facts_v4.py",
|
||||
"scripts/narayana_dasha.py",
|
||||
"scripts/rectification/api_service.py",
|
||||
"scripts/rectification/candidate_contrast.py",
|
||||
"scripts/rectification/candidate_window.py",
|
||||
"scripts/rectification/case_holdout.py",
|
||||
"scripts/rectification/contracts.py",
|
||||
"scripts/rectification/dasha_transition_proximity.py",
|
||||
"scripts/rectification/decision_policy.py",
|
||||
"scripts/rectification/event_probes.py",
|
||||
"scripts/rectification/refinement_packet.py",
|
||||
"scripts/rectification/scoring_service.py",
|
||||
"scripts/shadbala.py",
|
||||
"scripts/varga.py"
|
||||
],
|
||||
"production_scoring_sha256": "e20ff44f33b57db60bb6661ac235bd9f81425ced3fa632e7ea4b31f48b41237e",
|
||||
"research_files": [
|
||||
"scripts/research/reported_offset_sweep.py",
|
||||
"scripts/research/cluster_width_lib.py",
|
||||
"scripts/research/probe_supply_after_six.py",
|
||||
"scripts/research/sealed_holdout_rerun.py",
|
||||
"scripts/minute_rectification_blind_eval.py",
|
||||
"scripts/minute_rectification_fact_blind_eval_v4.py",
|
||||
"scripts/minute_rectification_holdout_validator.py"
|
||||
],
|
||||
"research_implementation_sha256": "25d970c09cb4d6a71e90c05d9580be5b81a63cbc338cc7cd4bf768a8aa45efd9",
|
||||
"file_sha256": {
|
||||
"scripts/active_rectification_event_engine.py": "dced0b398ad90bdec59c743a55ffe01613306127c17a1e44c8c9cfb569c57841",
|
||||
"scripts/active_rectification_events.py": "03e333ccf1103caf685dfe50fe890ab1793e945037085f50ce834a4a3b232222",
|
||||
"scripts/ashtakavarga.py": "c524da3f54b2ed9c080b78b33854854a0ca3d5d79b7e536bb1c3bc2be3e02852",
|
||||
"scripts/dasha_analyzer.py": "3949b96813217fe624899856afec9f0b787c4cb0fea3a9a0d961990dbd11943a",
|
||||
"scripts/divisional_charts_extended.py": "e9820e80f14fddc86ab5495d11455d16c2d3c9502a9a242fcf9efc41186e0d11",
|
||||
"scripts/domain_calculation_service.py": "66923a2bc69daf65c965d2973c4b9234edbec5147909eaf3609b6b377ce3c7ca",
|
||||
"scripts/functional_benefics.py": "cbaac3b8c09b97f8724b5300e9a402cbf77089af6c7db8788550d40fd35ac57b",
|
||||
"scripts/jaimini.py": "706d20dcff89079063fd1857a36f19419604ef81c80227921c7a2ced60011ac9",
|
||||
"scripts/minute_rectification_blind_eval.py": "07aa30d8bad3278669495d5fea8be937aaf6367c8af980ff4f2fe9aa84ccc6ff",
|
||||
"scripts/minute_rectification_fact_blind_eval_v4.py": "bdf460e3467208e8b87345c5beb2128f3e22de7eeffd68f72b281b3ad9affc92",
|
||||
"scripts/minute_rectification_fact_ranker_v4.py": "f1a0d8d2482cd754d2e7f253a9606b0373519435a2bf58cfc3753e2b9ae0fb76",
|
||||
"scripts/minute_rectification_feature_facts_v4.py": "3d899a2cdd4825b8e32062443d487edcd2173a4ea55a36eb9f3abe587c0faf0f",
|
||||
"scripts/minute_rectification_holdout_validator.py": "b60b1c752e58672beaee8b36eb3b2348169726cda070ec969db0f13e7ed715af",
|
||||
"scripts/narayana_dasha.py": "9a847d3b8d05f90d4f275079c97af1f71e83a74f930e14bd8f3f37b293d91d1e",
|
||||
"scripts/rectification/api_service.py": "089730097d3872a4f7b5d7dd5574bb7e9ab4d2e246d4a380800bc824d7979b95",
|
||||
"scripts/rectification/candidate_contrast.py": "eee7da71c0574a08876404b78ca111153fa95fb83ace82b9fdca141d1992e273",
|
||||
"scripts/rectification/candidate_window.py": "2fbc5058b12049b15fca375cdc125b8283e19f9f3e4bedcc291e344501891bef",
|
||||
"scripts/rectification/case_holdout.py": "fe0e699b617d9e216acdfeff87af4221c1d6f4c101bf33e1dbf14e2bcd75a8b5",
|
||||
"scripts/rectification/contracts.py": "c6dc6bd230e38f9f2126c224aae9ddbd6415304c15340f8c7cd3f06f6ef3160f",
|
||||
"scripts/rectification/dasha_transition_proximity.py": "5ce4ed27b0f06c681abc34e5fd998fafb10c153a0070aa36c99ddfa869a2cbf2",
|
||||
"scripts/rectification/decision_policy.py": "80e8de4680d19a052213bb821d2a9c94143a2962235aeb27f6ad81374d2e60d5",
|
||||
"scripts/rectification/event_probes.py": "f5ca9d54501f67a1546fcf5b01da0c1277dec2e2c6df481884e8900d14909a7c",
|
||||
"scripts/rectification/refinement_packet.py": "dbeea5aa780889785ce850f3881e8b7f67bb44594e9bc382c65b2e730884a2f4",
|
||||
"scripts/rectification/scoring_service.py": "fa056fa30d9e6ffa4b75295fc49c793e35b26ebeca64ca843d26c8abef3050f2",
|
||||
"scripts/research/cluster_width_lib.py": "1d8f2ffa7795d62069deebb2de8e16749317037d4ed3f24a2c0092676e5cfcc6",
|
||||
"scripts/research/probe_supply_after_six.py": "098a5b4398d6f8d996917d809c2fc922ad64031d3b70660f8ef7c876aba92438",
|
||||
"scripts/research/reported_offset_sweep.py": "13c29205f5315383a4d4af04b4c69a244d8f175158beb20f3d9e47b62fd2683f",
|
||||
"scripts/research/sealed_holdout_rerun.py": "2a4dfd27bb8b78d90d81f2b534091a1f82f748bc873e9943609256774a4313bc",
|
||||
"scripts/shadbala.py": "352a9dd91c9be6dd7a294b1dc24c62173e8f08db5fc36c453dc5dcd7e0fd62d7",
|
||||
"scripts/varga.py": "1bca389e4f36d293a755de175163c3f39b42bab5831cb6f7828830899f9f177e"
|
||||
},
|
||||
"hash_scope": "explicit_identity_file_sets_not_a_transitive_dependency_lock"
|
||||
},
|
||||
"production_identity_scope": "context_only_shadow_scorer_does_not_call_transition_proximity",
|
||||
"frozen_at_utc": "2026-11-04T08:34:34.223675+00:00",
|
||||
"dataset_path": "references/real_case_calibration/minute_rectification_holdout_v3.json",
|
||||
"dataset_sha256": "45aa76ab79dfc6c6404b6252083e1aa5c7e2e2b7de811fc11042a8ad2da3f0ea",
|
||||
"algorithm_version": "birth-time-event-fact-ranker-v4-shadow",
|
||||
"implementation_sha256": "a6a60f59f7405496453000abef6f9f3074aa8d31c2a4dc9a8f18c6d812837168",
|
||||
"files": [
|
||||
"scripts/active_rectification_event_engine.py",
|
||||
"scripts/active_rectification_events.py",
|
||||
"scripts/ashtakavarga.py",
|
||||
"scripts/dasha_analyzer.py",
|
||||
"scripts/divisional_charts_extended.py",
|
||||
"scripts/domain_calculation_service.py",
|
||||
"scripts/jaimini.py",
|
||||
"scripts/minute_rectification_fact_ranker_v4.py",
|
||||
"scripts/minute_rectification_feature_facts_v4.py",
|
||||
"scripts/narayana_dasha.py",
|
||||
"scripts/shadbala.py",
|
||||
"scripts/varga.py"
|
||||
],
|
||||
"historical_frozen_sha256": "f41c298dd6cdcebe7a93e632f7191954be7987f6012ca2a34cecb6e447fbf196",
|
||||
"evaluator_sha256": "2a4dfd27bb8b78d90d81f2b534091a1f82f748bc873e9943609256774a4313bc",
|
||||
"ayanamsa": "raman",
|
||||
"node_mode": "mean",
|
||||
"candidate_radius_minutes": [
|
||||
10
|
||||
],
|
||||
"minute_step": 1,
|
||||
"release_metrics": {
|
||||
"top_1_rate_minimum": 0.6,
|
||||
"top_3_rate_minimum": 0.85,
|
||||
"mean_absolute_minute_error_maximum": 2.0,
|
||||
"false_confirmation_rate_maximum": 0.05,
|
||||
"correct_insufficient_evidence_rejection_rate_minimum": 0.9
|
||||
},
|
||||
"results_previously_seen": true,
|
||||
"official_valid_independent_blind": false,
|
||||
"must_not_use_for_tuning": true,
|
||||
"tie_breaker": "sha256(benchmark_id:case_id:candidate_time); published minute is never passed to the ranker",
|
||||
"metric_rank_definition": "competition_rank_1_plus_strictly_higher_scores_legacy_protocol",
|
||||
"extra_metric_rank_definition": "score_desc_then_existing_opaque_sha256_total_order"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+504
-504
File diff suppressed because it is too large
Load Diff
+8
-2
@@ -43,7 +43,13 @@ SATURN_PART_START = {
|
||||
|
||||
# Planet indices follow the local seven-graha ordering: Sun through Saturn.
|
||||
# Each row assigns the eight daylight/nighttime shares for a weekday where
|
||||
# Python's datetime.weekday() is Monday=0.
|
||||
# Python's datetime.weekday() is Monday=0. The day starts with the weekday
|
||||
# lord, the night with the lord of the fifth weekday counted from it; the
|
||||
# shares then run round the eight-slot cycle Sun..Saturn, lordless (PyJHora
|
||||
# const.day_rulers / night_rulers). Saturn's share in every row equals
|
||||
# SATURN_PART_START. The Wednesday-Saturday night rows used to start one lord
|
||||
# early (Saturday night with Mars instead of Mercury), so Maandi and the four
|
||||
# other time-segment points sat in the wrong share on those nights (BUG-1565).
|
||||
DAY_RULERS = (
|
||||
(1, 2, 3, 4, 5, 6, -1, 0),
|
||||
(2, 3, 4, 5, 6, -1, 0, 1),
|
||||
@@ -56,10 +62,10 @@ DAY_RULERS = (
|
||||
NIGHT_RULERS = (
|
||||
(5, 6, -1, 0, 1, 2, 3, 4),
|
||||
(6, -1, 0, 1, 2, 3, 4, 5),
|
||||
(-1, 0, 1, 2, 3, 4, 5, 6),
|
||||
(0, 1, 2, 3, 4, 5, 6, -1),
|
||||
(1, 2, 3, 4, 5, 6, -1, 0),
|
||||
(2, 3, 4, 5, 6, -1, 0, 1),
|
||||
(3, 4, 5, 6, -1, 0, 1, 2),
|
||||
(4, 5, 6, -1, 0, 1, 2, 3),
|
||||
)
|
||||
TEMPORAL_UPAGRAHAS = {
|
||||
|
||||
+96
-48
@@ -361,38 +361,66 @@ def _jaimini_planet_dignity_level(planet: str, sign_idx: int) -> int:
|
||||
return 3
|
||||
|
||||
|
||||
_CO_LORD_GRAHAS = ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu')
|
||||
|
||||
|
||||
def _rasi_drishti(source_sign_idx: int, target_sign_idx: int) -> bool:
|
||||
"""Jaimini sign aspect: movable -> fixed (not the next), fixed -> movable
|
||||
(not the previous), dual -> the other duals."""
|
||||
if source_sign_idx in (0, 3, 6, 9):
|
||||
return target_sign_idx in (1, 4, 7, 10) and target_sign_idx != (source_sign_idx + 1) % 12
|
||||
if source_sign_idx in (1, 4, 7, 10):
|
||||
return target_sign_idx in (0, 3, 6, 9) and target_sign_idx != (source_sign_idx - 1) % 12
|
||||
return target_sign_idx in (2, 5, 8, 11) and target_sign_idx != source_sign_idx
|
||||
|
||||
|
||||
def _co_lord_strength(longitudes: Dict[str, float], planet: str) -> Tuple[int, int, int, int, float]:
|
||||
"""Strength key of one Scorpio / Aquarius co-lord where it stands (BUG-1566).
|
||||
|
||||
Same order as PyJHora `house._stronger_planet_new`, Rath (Narayana Dasa
|
||||
p.43 (d)) and references/prashna-complete-guide.md (dual lords):
|
||||
1. more other bodies in its sign;
|
||||
2. more of Jupiter, Mercury and its dispositor in or aspecting (Rasi
|
||||
Drishti) its sign;
|
||||
3. dignity in the sign it occupies (exalted > own > other > debilitated);
|
||||
4. dual > fixed > movable sign;
|
||||
5. more degrees in the sign.
|
||||
"""
|
||||
sign_idx = _get_planet_house(longitudes, planet)
|
||||
companions = sum(
|
||||
1 for other in _CO_LORD_GRAHAS
|
||||
if other != planet and other in longitudes and _get_planet_house(longitudes, other) == sign_idx
|
||||
)
|
||||
dispositor = SIGN_LORDS[SIGNS[sign_idx]]
|
||||
support = 0
|
||||
for role in ('Jupiter', 'Mercury', dispositor):
|
||||
if role not in longitudes:
|
||||
continue
|
||||
role_sign = _get_planet_house(longitudes, role)
|
||||
if role_sign == sign_idx or _rasi_drishti(role_sign, sign_idx):
|
||||
support += 1
|
||||
dignity = _jaimini_planet_dignity_level(planet, sign_idx)
|
||||
modality = 2 if sign_idx % 3 == 2 else 1 if sign_idx % 3 == 1 else 0
|
||||
return companions, support, dignity, modality, float(longitudes[planet]) % 30
|
||||
|
||||
|
||||
def _jaimini_stronger_planet(longitudes: Dict[str, float], planet_a: str,
|
||||
planet_b: str, sign_idx: int) -> str:
|
||||
"""
|
||||
动态比较两颗行星在指定星座的力量,返回较强者。
|
||||
基于PyJHora _stronger_planet_new 算法翻译。
|
||||
Stronger of two co-lords of ``sign_idx``, judged where each one stands.
|
||||
|
||||
比较层次:
|
||||
1. 尊严层级: exalted(6) > own(5) > friendly(4) > neutral(3) > enemy(2) > debilitated(1)
|
||||
2. 同层级:比较行星经度位置(度数更靠近星座中心更强)
|
||||
3. 仍相同:比较Naisargika Bala(天然力量)
|
||||
Before BUG-1566 this compared both planets' dignity *in ``sign_idx``*
|
||||
(the dasha sign) rather than in the signs they occupy, so Scorpio always
|
||||
went to Ketu (exalted there) and Aquarius always to Saturn (own sign),
|
||||
whatever the chart. Now: `_co_lord_strength` levels 1-5, then the
|
||||
natural-strength order as a last tie-break.
|
||||
"""
|
||||
level_a = _jaimini_planet_dignity_level(planet_a, sign_idx)
|
||||
level_b = _jaimini_planet_dignity_level(planet_b, sign_idx)
|
||||
|
||||
if level_a > level_b:
|
||||
key_a = _co_lord_strength(longitudes, planet_a)
|
||||
key_b = _co_lord_strength(longitudes, planet_b)
|
||||
if key_a > key_b:
|
||||
return planet_a
|
||||
if level_b > level_a:
|
||||
if key_b > key_a:
|
||||
return planet_b
|
||||
|
||||
# 同层级:比较行星在星座中的位置(更靠近星座中心度数=15°更强)
|
||||
deg_a = longitudes.get(planet_a, 0) % 30
|
||||
deg_b = longitudes.get(planet_b, 0) % 30
|
||||
|
||||
center_diff_a = abs(deg_a - 15)
|
||||
center_diff_b = abs(deg_b - 15)
|
||||
|
||||
if center_diff_a < center_diff_b:
|
||||
return planet_a
|
||||
if center_diff_b < center_diff_a:
|
||||
return planet_b
|
||||
|
||||
# 完全平局:使用Naisargika Bala决断
|
||||
naisargika_order = ['Sun', 'Moon', 'Venus', 'Jupiter', 'Mercury', 'Mars', 'Saturn']
|
||||
idx_a = naisargika_order.index(planet_a) if planet_a in naisargika_order else 99
|
||||
idx_b = naisargika_order.index(planet_b) if planet_b in naisargika_order else 99
|
||||
@@ -401,32 +429,33 @@ def _jaimini_stronger_planet(longitudes: Dict[str, float], planet_a: str,
|
||||
|
||||
def _resolve_chara_dasha_lord(longitudes, sign_idx):
|
||||
"""
|
||||
解析Chara Dasha宫主。v6.1.13: 实现动态共主判定。
|
||||
解析Chara Dasha宫主(天蝎:火星 / 计都;水瓶:土星 / 罗睺)。
|
||||
|
||||
Aquarius (sign 10): 比较 Saturn vs Rahu
|
||||
Scorpio (sign 7): 比较 Mars vs Ketu
|
||||
BUG-1566: one co-lord in the sign itself and the other elsewhere -> the
|
||||
one elsewhere (K.N. Rao, Predicting through Jaimini's Chara Dasha; Rath,
|
||||
Narayana Dasa p.43 (c); PyJHora basic rule; prashna guide rule 1).
|
||||
Otherwise the stronger by `_jaimini_stronger_planet`, judged where each
|
||||
planet stands. Both in the sign keep the stronger-planet result (the
|
||||
count is 12 either way).
|
||||
其他星座: 使用传统宫主
|
||||
"""
|
||||
sign_name = SIGNS[sign_idx]
|
||||
traditional_lord = SIGN_LORDS.get(sign_name, '')
|
||||
|
||||
# Aquarius: Saturn vs Rahu 共主判定
|
||||
if sign_idx == 10: # Aquarius
|
||||
saturn_present = 'Saturn' in longitudes and longitudes['Saturn'] >= 0
|
||||
rahu_present = 'Rahu' in longitudes and longitudes['Rahu'] >= 0
|
||||
if saturn_present and rahu_present:
|
||||
return _jaimini_stronger_planet(longitudes, 'Saturn', 'Rahu', sign_idx)
|
||||
return 'Saturn' if saturn_present else 'Rahu'
|
||||
|
||||
# Scorpio: Mars vs Ketu 共主判定
|
||||
if sign_idx == 7: # Scorpio
|
||||
mars_present = 'Mars' in longitudes and longitudes['Mars'] >= 0
|
||||
ketu_present = 'Ketu' in longitudes and longitudes['Ketu'] >= 0
|
||||
if mars_present and ketu_present:
|
||||
return _jaimini_stronger_planet(longitudes, 'Mars', 'Ketu', sign_idx)
|
||||
return 'Mars' if mars_present else 'Ketu'
|
||||
|
||||
return traditional_lord
|
||||
pair = {10: ('Saturn', 'Rahu'), 7: ('Mars', 'Ketu')}.get(sign_idx)
|
||||
if pair is None:
|
||||
return traditional_lord
|
||||
first, second = pair
|
||||
first_present = first in longitudes and longitudes[first] >= 0
|
||||
second_present = second in longitudes and longitudes[second] >= 0
|
||||
if not (first_present and second_present):
|
||||
return first if first_present else second
|
||||
first_in = _get_planet_house(longitudes, first) == sign_idx
|
||||
second_in = _get_planet_house(longitudes, second) == sign_idx
|
||||
if first_in and not second_in:
|
||||
return second
|
||||
if second_in and not first_in:
|
||||
return first
|
||||
return _jaimini_stronger_planet(longitudes, first, second, sign_idx)
|
||||
|
||||
|
||||
def _sign_is_even_footed(sign_idx: int) -> bool:
|
||||
@@ -707,8 +736,21 @@ def calc_chara_dasha_with_antardasha(asc_sign_idx: int,
|
||||
antar_sequence = [(md_sign_idx + 12 - i) % 12 for i in range(12)]
|
||||
else:
|
||||
antar_sequence = [(md_sign_idx + i) % 12 for i in range(12)]
|
||||
elif profile == "kn_rao":
|
||||
# K.N. Rao, Predicting through Jaimini's Chara Dasha, printed
|
||||
# pp.35-36: each mahadasha sign runs its own direction (reverse
|
||||
# when the 9th from it is even-footed) and its own sign comes
|
||||
# last. The book's Scorpio MD (pp.49-50) runs Libra .. Scorpio and
|
||||
# its Capricorn MD (pp.51-52) opens with Sagittarius (golden rows
|
||||
# in references/oracle/chara_narayana_book_golden_cases_2026_08_28.json).
|
||||
# Before BUG-1567 every MD reused the lagna order shifted by one,
|
||||
# which matches the book only for the first MD.
|
||||
if _sign_is_even_footed((md_sign_idx + 8) % 12):
|
||||
antar_sequence = [(md_sign_idx - i) % 12 for i in range(1, 13)]
|
||||
else:
|
||||
antar_sequence = [(md_sign_idx + i) % 12 for i in range(1, 13)]
|
||||
else:
|
||||
# Antardasha序列:Mahadasha序列偏移1(PyJHora method=2)
|
||||
# Antardasha序列:Mahadasha序列偏移1(Goel 两档未找到出处,沿用旧序)
|
||||
antar_sequence = progression[1:] + progression[:1]
|
||||
|
||||
antardasha_list = []
|
||||
@@ -744,7 +786,13 @@ def calc_chara_dasha_with_antardasha(asc_sign_idx: int,
|
||||
|
||||
base['has_antardasha'] = True
|
||||
base['has_pratyantar'] = True
|
||||
base['antardasha_method'] = 'equal_division_12 (PyJHora method=2)'
|
||||
base['antardasha_method'] = (
|
||||
'equal_division_12; kn_rao: own direction per mahadasha sign, mahadasha sign last (K.N. Rao pp.35-36)'
|
||||
if profile == 'kn_rao' else
|
||||
'equal_division_12; goel_shastri_method2: mahadasha sign first, its own direction'
|
||||
if profile == 'goel_shastri_method2' else
|
||||
'equal_division_12; lagna order shifted by one (unsourced, unchanged)'
|
||||
)
|
||||
return base
|
||||
|
||||
|
||||
|
||||
@@ -15283,8 +15283,13 @@ def _build_natal_foundation_modules(args, planet_lons, *, asc_lon=None, jd=None)
|
||||
hours_from_sunrise = (daynight["julian_day_ut"] - daynight["sunrise_jd_ut"]) * 24
|
||||
if hours_from_sunrise < 0:
|
||||
hours_from_sunrise += 24
|
||||
# The Vedic day (vara) runs sunrise to sunrise: a birth before sunrise
|
||||
# belongs to the previous weekday, and its hora counts from that
|
||||
# day's sunrise (BUG-1568). The weekday is the civil date of the
|
||||
# sunrise the hours are counted from, not the birth's civil date.
|
||||
# muhurta.py uses Sunday=0 while datetime.weekday() uses Monday=0.
|
||||
weekday = (birth_dt.weekday() + 1) % 7
|
||||
sy, sm, sd, _ = swe.revjul(daynight["sunrise_jd_ut"] + float(args.tz) / 24.0)
|
||||
weekday = (datetime(int(sy), int(sm), int(sd)).weekday() + 1) % 7
|
||||
panchanga = calc_panchanga(
|
||||
float(planet_lons["Sun"]),
|
||||
float(planet_lons["Moon"]),
|
||||
|
||||
+3
-6
@@ -619,7 +619,7 @@
|
||||
6,
|
||||
6,
|
||||
4,
|
||||
5,
|
||||
1,
|
||||
9,
|
||||
12,
|
||||
7,
|
||||
@@ -658,7 +658,6 @@
|
||||
"subperiod"
|
||||
],
|
||||
"chara": [
|
||||
"years",
|
||||
"subperiod"
|
||||
]
|
||||
}
|
||||
@@ -1274,7 +1273,7 @@
|
||||
"years": [
|
||||
12,
|
||||
0,
|
||||
3,
|
||||
9,
|
||||
8,
|
||||
2,
|
||||
1,
|
||||
@@ -1318,7 +1317,6 @@
|
||||
"subperiod"
|
||||
],
|
||||
"chara": [
|
||||
"years",
|
||||
"subperiod"
|
||||
]
|
||||
}
|
||||
@@ -1963,7 +1961,7 @@
|
||||
11,
|
||||
10,
|
||||
2,
|
||||
8,
|
||||
3,
|
||||
6,
|
||||
5,
|
||||
5,
|
||||
@@ -2001,7 +1999,6 @@
|
||||
"subperiod"
|
||||
],
|
||||
"chara": [
|
||||
"years",
|
||||
"subperiod"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
"""Data audit wave 2 (H), 2026-11-04: Chara Dasha, time-segment points and the
|
||||
birth weekday, checked against PyJHora 4.8.6, Swiss Ephemeris and the books.
|
||||
|
||||
External reference values are stored as plain numbers (PyJHora is AGPL and is
|
||||
not imported here). Public charts are Rodden AA figures already in
|
||||
benchmarks/jyotish/data/benchmark_samples.json.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import swisseph as swe
|
||||
|
||||
from scripts import gulika, jaimini
|
||||
from scripts.jyotish_engine import _build_natal_foundation_modules, compute_chart_data
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
GRAHAS = ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu")
|
||||
|
||||
|
||||
def _chart(year, month, day, hour, minute, lat, lon, tz, ayanamsa="raman"):
|
||||
chart, asc_idx, jd, _ = compute_chart_data(year, month, day, hour, minute, lat, lon, tz,
|
||||
node_mode="mean", ayanamsa_name=ayanamsa)
|
||||
return asc_idx, {name: chart["planets"][name]["degree_raw"] for name in GRAHAS}, jd
|
||||
|
||||
|
||||
BUSH = (1946, 7, 6, 7, 26, 41.3, -72.9333, -4.0)
|
||||
TAYLOR = (1932, 2, 27, 2, 30, 51.5, -0.1667, 0.0)
|
||||
JOBS = (1955, 2, 24, 19, 15, 37.7749, -122.4194, -8.0)
|
||||
TOKYO = (1964, 10, 10, 12, 0, 35.6762, 139.6503, 9.0)
|
||||
|
||||
|
||||
# ── BUG-1566: Scorpio / Aquarius co-lord in K.N. Rao Chara Dasha ─────────────
|
||||
|
||||
@pytest.mark.parametrize("ayanamsa", ["raman", "lahiri"])
|
||||
@pytest.mark.parametrize("birth, scorpio, aquarius", [
|
||||
# PyJHora 4.8.6 chara KN_RAO, same ayanamsa, mean node (numbers only).
|
||||
(BUSH, 9, 7), # Ketu in Scorpio, Mars in Leo -> Mars (was Ketu, 13 years)
|
||||
(TAYLOR, 3, 11), # both outside: Mars wins Scorpio, Rahu wins Aquarius (were 10 / 1)
|
||||
(JOBS, 6, 1), # Aquarius goes to Rahu (was Saturn, 5 years)
|
||||
])
|
||||
def test_co_lord_years_match_pyjhora_kn_rao(ayanamsa, birth, scorpio, aquarius):
|
||||
asc_idx, lons, _ = _chart(*birth, ayanamsa=ayanamsa)
|
||||
years = {row["sign"]: row["duration_years"] for row in
|
||||
jaimini.calc_chara_dasha(asc_idx, lons, birth[0], birth[1], birth[2])["dasha_sequence"]}
|
||||
assert years["Scorpio"] == scorpio
|
||||
assert years["Aquarius"] == aquarius
|
||||
|
||||
|
||||
def test_a_co_lord_sitting_in_the_sign_gives_way_to_the_one_outside():
|
||||
# K.N. Rao; Rath, Narayana Dasa p.43 (c); references/prashna-complete-guide.md.
|
||||
base = {"Sun": 15.0, "Moon": 75.0, "Mercury": 40.0, "Jupiter": 100.0, "Venus": 160.0}
|
||||
assert jaimini._resolve_chara_dasha_lord({**base, "Mars": 215.0, "Ketu": 45.0, "Rahu": 225.0, "Saturn": 5.0}, 7) == "Ketu"
|
||||
assert jaimini._resolve_chara_dasha_lord({**base, "Mars": 190.0, "Ketu": 225.0, "Rahu": 45.0, "Saturn": 5.0}, 7) == "Mars"
|
||||
assert jaimini._resolve_chara_dasha_lord({**base, "Mars": 0.0, "Ketu": 100.0, "Rahu": 280.0, "Saturn": 310.0}, 10) == "Rahu"
|
||||
asc_idx, lons, _ = _chart(*TOKYO)
|
||||
assert int(lons["Saturn"] // 30) == 10 # Saturn in Aquarius
|
||||
assert jaimini._resolve_chara_dasha_lord(lons, 10) == "Rahu"
|
||||
|
||||
|
||||
def test_co_lord_choice_depends_on_where_the_planets_stand():
|
||||
# Before BUG-1566 dignity was read in Scorpio itself, so Ketu won every
|
||||
# chart; with both lords outside, moving them must be able to flip it.
|
||||
base = {"Sun": 15.0, "Moon": 75.0, "Mercury": 40.0, "Jupiter": 100.0, "Venus": 160.0, "Saturn": 5.0}
|
||||
mars_exalted = {**base, "Mars": 285.0, "Ketu": 130.0, "Rahu": 310.0} # Mars in Capricorn, alone
|
||||
ketu_crowded = {**base, "Mars": 250.0, "Ketu": 105.0, "Rahu": 285.0} # Ketu in Cancer with Jupiter
|
||||
assert jaimini._resolve_chara_dasha_lord(mars_exalted, 7) == "Mars"
|
||||
assert jaimini._resolve_chara_dasha_lord(ketu_crowded, 7) == "Ketu"
|
||||
|
||||
|
||||
# ── BUG-1567: K.N. Rao Chara antardasha order ───────────────────────────────
|
||||
|
||||
def _golden(case_id):
|
||||
data = json.loads((ROOT / "references/oracle/chara_narayana_book_golden_cases_2026_08_28.json").read_text())
|
||||
return next(case for case in data["golden_cases"] if case["case_id"] == case_id)
|
||||
|
||||
|
||||
def test_every_mahadasha_runs_its_own_direction_and_ends_on_itself():
|
||||
asc_idx, lons, _ = _chart(*JOBS)
|
||||
result = jaimini.calc_chara_dasha_with_antardasha(asc_idx, lons, 1955, 2, 24)
|
||||
for md in result["dasha_sequence"]:
|
||||
order = [ad["sign_idx"] for ad in md["antardashas"]]
|
||||
assert order[-1] == md["sign_idx"]
|
||||
step = -1 if ((md["sign_idx"] + 8) % 12) in jaimini._EVEN_FOOTED_SIGNS else 1
|
||||
assert order == [(md["sign_idx"] + step * i) % 12 for i in range(1, 13)]
|
||||
|
||||
|
||||
def test_scorpio_mahadasha_matches_the_book_order():
|
||||
# K.N. Rao, Predicting through Jaimini's Chara Dasha, pdf pp.49-50.
|
||||
expected = _golden("rao_chara_male_1949_scorpio_ad")["profile_assertions"]["subperiod_order"]
|
||||
asc_idx, lons, _ = _chart(*JOBS)
|
||||
result = jaimini.calc_chara_dasha_with_antardasha(asc_idx, lons, 1955, 2, 24)
|
||||
scorpio = next(md for md in result["dasha_sequence"] if md["sign"] == "Scorpio")
|
||||
assert [ad["sign"] for ad in scorpio["antardashas"]] == expected
|
||||
|
||||
|
||||
def test_vietnamese_lady_replays_the_book_mahadashas_and_first_capricorn_antardasha():
|
||||
# K.N. Rao, pdf pp.51-52: 1958-01-19 14:45 Hanoi; Lahiri as in the book.
|
||||
asc_idx, lons, _ = _chart(1958, 1, 19, 14, 45, 21.03, 105.85, 7.0, ayanamsa="lahiri")
|
||||
result = jaimini.calc_chara_dasha_with_antardasha(asc_idx, lons, 1958, 1, 19)
|
||||
rows = [(md["sign"], md["duration_years"]) for md in result["dasha_sequence"][:5]]
|
||||
book = [(row["sign"], row["duration_years"]) for row in _golden("rao_chara_vietnamese_lady_1958_capricorn_ad")["period_rows"] if row["level"] == "MD"]
|
||||
assert rows == book
|
||||
capricorn = next(md for md in result["dasha_sequence"] if md["sign"] == "Capricorn")
|
||||
assert capricorn["antardashas"][0]["sign"] == "Sagittarius"
|
||||
|
||||
|
||||
def test_goel_profiles_keep_their_antardasha_order():
|
||||
asc_idx, lons, _ = _chart(*JOBS)
|
||||
progression = jaimini._chara_progression_profile(asc_idx, lons, profile="goel_lagna_start")
|
||||
result = jaimini.calc_chara_dasha_with_antardasha(asc_idx, lons, 1955, 2, 24, profile="goel_lagna_start")
|
||||
for md in result["dasha_sequence"]:
|
||||
assert [ad["sign_idx"] for ad in md["antardashas"]] == progression[1:] + progression[:1]
|
||||
|
||||
|
||||
# ── BUG-1565: night shares of the time-segment points ───────────────────────
|
||||
|
||||
def test_night_and_day_share_tables_follow_the_weekday_rule():
|
||||
for weekday in range(7): # Monday=0
|
||||
sunday_based = (weekday + 1) % 7
|
||||
assert gulika.DAY_RULERS[weekday][0] == sunday_based
|
||||
assert gulika.NIGHT_RULERS[weekday][0] == (sunday_based + 4) % 7 # lord of the 5th weekday
|
||||
assert gulika.DAY_RULERS[weekday].index(6) == gulika.SATURN_PART_START[weekday]["day"]
|
||||
assert gulika.NIGHT_RULERS[weekday].index(6) == gulika.SATURN_PART_START[weekday]["night"]
|
||||
|
||||
|
||||
def _independent_point(moment, lat, lon, tz, planet, middle, ayanamsa="raman"):
|
||||
flags = swe.FLG_SWIEPH | swe.FLG_TRUEPOS
|
||||
geo = (lon, lat, 0.0)
|
||||
|
||||
def event(after, kind):
|
||||
return swe.rise_trans(after, swe.SUN, kind | swe.BIT_HINDU_RISING, geo, flags=flags)[1][0]
|
||||
|
||||
jd = swe.julday(moment.year, moment.month, moment.day, moment.hour + moment.minute / 60 - tz)
|
||||
midnight = swe.julday(moment.year, moment.month, moment.day, -tz)
|
||||
rise = event(midnight, swe.CALC_RISE)
|
||||
if rise > jd:
|
||||
midnight -= 1.0
|
||||
rise = event(midnight, swe.CALC_RISE)
|
||||
sunset, next_rise = event(midnight, swe.CALC_SET), event(midnight + 1.0, swe.CALC_RISE)
|
||||
day = rise <= jd < sunset
|
||||
start, end = (rise, sunset) if day else (sunset, next_rise)
|
||||
y, m, d, _ = swe.revjul(start + tz / 24)
|
||||
sunday_based = (datetime(y, m, d).weekday() + 1) % 7
|
||||
first = sunday_based if day else (sunday_based + 4) % 7
|
||||
cycle = [0, 1, 2, 3, 4, 5, 6, -1]
|
||||
order = [cycle[(cycle.index(first) + i) % 8] for i in range(8)]
|
||||
fraction = (order.index(planet) + (0.5 if middle else 0.0)) / 8
|
||||
from scripts.ayanamsa_utils import temporary_ayanamsa
|
||||
with temporary_ayanamsa(ayanamsa, swe):
|
||||
return swe.houses_ex(start + (end - start) * fraction, lat, lon, b"P", swe.FLG_SWIEPH | swe.FLG_SIDEREAL)[1][0] % 360
|
||||
|
||||
|
||||
@pytest.mark.parametrize("birth", [JOBS, TAYLOR, (1970, 3, 21, 18, 15, 51.5074, -0.1278, 0.0)])
|
||||
def test_thursday_to_saturday_night_points_sit_in_the_right_share(birth):
|
||||
# Jobs: Thursday night; Taylor: Friday night (before Saturday's sunrise);
|
||||
# London smoke chart: Saturday night.
|
||||
moment = datetime(*birth[:5])
|
||||
lat, lon, tz = birth[5], birth[6], birth[7]
|
||||
sun = _chart(*birth)[1]["Sun"]
|
||||
raw = gulika.calculate_upagrahas(moment, lat=lat, lon=lon, tz=tz, sun_longitude=sun, ayanamsa="raman")["raw"]
|
||||
assert raw["Maandi"]["period"] == "night"
|
||||
for name, (planet, part) in gulika.TEMPORAL_UPAGRAHAS.items():
|
||||
expected = _independent_point(moment, lat, lon, tz, planet, part == "middle")
|
||||
assert raw[name]["longitude"] == pytest.approx(expected, abs=0.01), name
|
||||
# Maandi is the middle of the same Saturn share whose start is Gulika.
|
||||
single = gulika.calculate_gulika(moment, lat=lat, lon=lon, tz=tz, ayanamsa="raman")
|
||||
assert raw["Maandi"]["segment_fraction"] == pytest.approx(single["segment_fraction"] + 1 / 16)
|
||||
|
||||
|
||||
# ── BUG-1568: birth weekday (vara) and hora before sunrise ──────────────────
|
||||
|
||||
def _panchanga(birth):
|
||||
year, month, day, hour, minute, lat, lon, tz = birth
|
||||
_, lons, jd = _chart(*birth)
|
||||
args = SimpleNamespace(year=year, month=month, day=day, hour=hour, minute=minute, second=0,
|
||||
lat=lat, lon=lon, tz=tz, ayanamsa="raman", node_mode="mean", position_mode="legacy")
|
||||
return _build_natal_foundation_modules(args, lons, jd=jd)["panchanga"]["raw"]
|
||||
|
||||
|
||||
def test_a_birth_before_sunrise_belongs_to_the_previous_weekday():
|
||||
# Taylor: Saturday 1932-02-27 02:30 civil, before sunrise -> Friday
|
||||
# (PyJHora drik.vaara, the Shadbala path in this repo, Panchanga practice).
|
||||
raw = _panchanga(TAYLOR)
|
||||
assert raw["vara"]["vara"] == "Friday"
|
||||
# Hora counts from Friday's sunrise: about 19.5 hours in -> the 20th hora,
|
||||
# Friday's sequence Venus, Mercury, Moon, Saturn, Jupiter, Mars, Sun, ... -> Mars.
|
||||
assert raw["hora"]["hora_num"] == 20
|
||||
assert raw["hora"]["hora_lord"] == "Mars"
|
||||
|
||||
|
||||
def test_a_daytime_birth_keeps_its_civil_weekday():
|
||||
raw = _panchanga(BUSH) # Saturday morning after sunrise
|
||||
assert raw["vara"]["vara"] == "Saturday"
|
||||
@@ -0,0 +1,79 @@
|
||||
"""BUG-1569: the Chara, transit and node-mode benchmark scripts pass the ayanamsa
|
||||
to both sides and refuse a canonical baseline built with another one.
|
||||
|
||||
Same failure as 74169e1b: one side hard-coded Lahiri while the engine default
|
||||
is Raman, so a 1.446 degree ayanamsa gap read as an algorithm difference.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import swisseph as swe
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPTS = ROOT / "benchmarks" / "jyotish" / "scripts"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", [
|
||||
"run_chara_dasha_compare.py", "run_chara_dasha_knrao.py",
|
||||
"run_transit_true_compare.py", "run_node_mode_compare.py",
|
||||
])
|
||||
def test_no_script_hard_codes_lahiri_and_each_takes_the_ayanamsa(name):
|
||||
source = (SCRIPTS / name).read_text(encoding="utf-8")
|
||||
assert "set_ayanamsa_mode('LAHIRI')" not in source
|
||||
assert "SIDM_LAHIRI, 0, 0" not in source
|
||||
assert "--ayanamsa" in source
|
||||
|
||||
|
||||
def test_engine_paths_point_at_the_repository_scripts():
|
||||
from benchmarks.jyotish.scripts import run_chara_dasha_compare, run_transit_true_compare
|
||||
|
||||
assert (run_chara_dasha_compare.SKILL_SCRIPTS / "jaimini.py").is_file()
|
||||
assert run_transit_true_compare.SKILL_SCRIPT.is_file()
|
||||
|
||||
|
||||
def test_transit_compare_passes_the_ayanamsa_to_engine_and_swiss(monkeypatch, tmp_path):
|
||||
from benchmarks.jyotish.scripts import run_transit_true_compare as rtc
|
||||
|
||||
seen = {}
|
||||
|
||||
class Done:
|
||||
returncode = 0
|
||||
stdout = json.dumps({"birth_info": {"ayanamsa_name": "raman"}, "modules": {}})
|
||||
stderr = ""
|
||||
|
||||
def fake_run(cmd, **kwargs):
|
||||
seen["cmd"] = cmd
|
||||
return Done()
|
||||
|
||||
monkeypatch.setattr(rtc, "RAW", tmp_path)
|
||||
monkeypatch.setattr(rtc.subprocess, "run", fake_run)
|
||||
sample = {"id": "fixture", "today": "2026-11-04",
|
||||
"birth": {"year": 2000, "month": 1, "day": 1, "hour": 0, "minute": 0, "lat": 0, "lon": 0, "tz": 0}}
|
||||
rtc.run_full_reading(sample, "raman")
|
||||
assert seen["cmd"][seen["cmd"].index("--ayanamsa") + 1] == "raman"
|
||||
raman = rtc.calc_swiss_transit("2026-11-04", 0, "raman")["planets"]["Saturn"]["longitude"]
|
||||
lahiri = rtc.calc_swiss_transit("2026-11-04", 0, "lahiri")["planets"]["Saturn"]["longitude"]
|
||||
assert 1.3 < (raman - lahiri) % 360 < 1.6
|
||||
monkeypatch.setattr(rtc, "TRANSIT_OUT", tmp_path)
|
||||
with pytest.raises(RuntimeError, match="ayanamsa"):
|
||||
rtc.compare_sample(sample, "lahiri")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("module_name", ["run_chara_dasha_compare", "run_node_mode_compare"])
|
||||
def test_canonical_baseline_from_another_ayanamsa_is_refused(module_name, monkeypatch, tmp_path):
|
||||
import importlib
|
||||
|
||||
module = importlib.import_module(f"benchmarks.jyotish.scripts.{module_name}")
|
||||
canon = tmp_path / "canonical"
|
||||
canon.mkdir()
|
||||
(canon / "fixture.canonical.json").write_text(json.dumps({"parameters": {"ayanamsa": "raman", "node_mode": "mean"}}))
|
||||
data = tmp_path / "samples.json"
|
||||
data.write_text(json.dumps([{"id": "fixture", "label": "x", "birth": {}}]))
|
||||
monkeypatch.setattr(module, "CANON", canon)
|
||||
monkeypatch.setattr(module, "DATA", data)
|
||||
with pytest.raises(SystemExit):
|
||||
module.main(["--ayanamsa", "lahiri"])
|
||||
@@ -17,8 +17,9 @@ from jaimini import ( # noqa: E402
|
||||
calc_chara_dasha_with_antardasha,
|
||||
)
|
||||
|
||||
# Scorpio: Ketu (exalted there in this table) outranks Mars, so the engine picks Ketu.
|
||||
# Aquarius: Saturn owns it and outranks Rahu, so the engine keeps Saturn.
|
||||
# Scorpio: Ketu sits in Scorpio and Mars in Libra, so the lord outside, Mars,
|
||||
# counts (BUG-1566; before, Ketu always won Scorpio because its dignity was
|
||||
# read in Scorpio itself). Aquarius: Rahu sits in Aquarius, so Saturn counts.
|
||||
LONGITUDES = {
|
||||
"Sun": 15.0,
|
||||
"Moon": 75.0,
|
||||
@@ -47,8 +48,17 @@ def test_every_mahadasha_row_names_its_lord_and_the_sign_it_sits_in():
|
||||
|
||||
|
||||
def test_co_lord_signs_carry_the_engine_choice_not_the_traditional_lord():
|
||||
# Was Scorpio -> Ketu / Scorpio (原值); now Mars / Libra (新值): the co-lord
|
||||
# sitting in the sign gives way to the one outside it (BUG-1566).
|
||||
rows = _rows()
|
||||
assert rows["Scorpio"]["lord"] == "Ketu"
|
||||
assert rows["Scorpio"]["lord_in_sign"] == "Scorpio"
|
||||
assert rows["Scorpio"]["lord"] == "Mars"
|
||||
assert rows["Scorpio"]["lord_in_sign"] == "Libra"
|
||||
assert rows["Aquarius"]["lord"] == "Saturn"
|
||||
assert rows["Aquarius"]["lord_in_sign"] == "Aries"
|
||||
# Mars in Scorpio, Ketu in Taurus: the row carries Ketu, not the
|
||||
# traditional lord.
|
||||
moved = {**LONGITUDES, "Mars": 215.0, "Ketu": 45.0, "Rahu": 225.0}
|
||||
result = calc_chara_dasha_with_antardasha(0, moved, 1990, 6)
|
||||
scorpio = next(row for row in result["dasha_sequence"] if row["sign"] == "Scorpio")
|
||||
assert scorpio["lord"] == "Ketu"
|
||||
assert scorpio["lord_in_sign"] == "Taurus"
|
||||
|
||||
@@ -285,6 +285,9 @@ def test_pyjhora_fixture_diffs_stay_on_the_recorded_steps() -> None:
|
||||
# 原值:niryaana_shoola 的 native 行与 Shoola 一样,从上升或第七宫起、每段 9 年,并带子运差异。
|
||||
# 新值:同一键改为 PVR 主运的星座和年数;diff_steps 按新表相对 PyJHora 重记。
|
||||
# 原因:产品 2026-10-18 改口径。PyJHora 对照行没有重跑,只改我们自己的期望。
|
||||
# 原值:乔布斯 / 奥巴马 / 爱因斯坦三盘 native.chara 年数各有一段(水瓶或天蝎)与 PyJHora 不同,diff_steps 含 years。
|
||||
# 新值:三盘 Chara 年数与 PyJHora 4.8.7 对照行逐段相同,diff_steps 只剩 subperiod。
|
||||
# 原因:BUG-1566 天蝎 / 水瓶双主星按盘面判定(一主在本宫取另一主)。PyJHora 对照行未重跑。
|
||||
payload = json.loads(FIXTURE.read_text(encoding="utf-8"))
|
||||
assert payload["pyjhora_version"] == "4.8.7"
|
||||
assert payload["generator_python_has_pyjhora"] is True
|
||||
|
||||
Reference in New Issue
Block a user