Files
Jyotisha/scripts/kalanala_chakra.py
T
ClaudeandClaude Opus 5.5 ac9be6f88c feat(techniques): port the astrologer upstream skill's missing techniques
Port from yinduzhanxing 5fb8379c (MIT): Naisargika, Kaala, Karaka, Tara,
Lagnamsaka, Yogardha, Mandooka, Paryaaya and Padanadhamsa dashas;
Sarvatobhadra, Kota and Kalanala chakras; Nakshatra Latta; Pachakadi;
Raman Amsayu and Pindayu. Producers and their upstream tests are copied;
scripts/upstream_technique_layers.py feeds them our natal and transit
positions and attaches chart.consultation_native_layers.upstream_techniques.

- dasha_registry: kaala and padanadhamsa keys (35 -> 37); the nine ported
  dashas are served by compute_dasha or fail closed with upstream's reason.
- technique_registry.json: 16 partial entries; README badges follow.
- Evidence lookup section `upstream_techniques` in the consultation tool.
- Boundaries: Kalanala carries no PyJHora (AGPL) grid coordinates;
  Padanadhamsa/Lagnamsaka run on our Narayana engine (no Rath Table 12,
  PVR co-lord variants or second cycle). PyJHora observers and the Amsayu
  worked-example scaffold live in scripts/research (root .py budget).
- PyJHora black-box cross-checks: Naisargika (lords/years) and Kaala
  (seed segment and fraction within 5e-4) on a public chart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXoyn7Hj3LCW3BUs5bZ8
2026-11-07 14:44:37 +08:00

83 lines
5.1 KiB
Python

"""Surya/Chandra Kalanala star-slot placement.
Ported from upstream yinduzhanxing (5fb8379c) scripts/kalanala_chakra.py with
one license-boundary change: upstream embeds the PyJHora (AGPL) UI grid
coordinates (src/jhora/ui/chakra.py ChandraKalanala / SuryaKalanala
`rasi_labels`). This repo keeps PyJHora black-box only (SKILL.md: GPL / AGPL
are benchmarked, never copied), so the coordinate tables are not carried here.
The native part — the absolute 28-star target (Abhijit slot after star 21) and
the slot relative to the base star — is plain arithmetic and is computed here.
Grid coordinates are attached only when a caller passes `positions` read from
an installed PyJHora by `read_pyjhora_kalanala_positions` (a subprocess AST
read, like scripts/research/sarvatobhadra_external.py); production never does that.
"""
from copy import deepcopy
import json
import subprocess
import sys
try:
from scripts.ayanamsa_utils import normalize_ayanamsa_name
except ModuleNotFoundError: # pragma: no cover - scripts/ on sys.path
from ayanamsa_utils import normalize_ayanamsa_name
SOURCE = 'https://github.com/naturalstupid/PyJHora/blob/main/src/jhora/ui/chakra.py'
PLANETS = ('Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn','Rahu','Ketu')
_CLASS_NAMES = {'chandra': 'ChandraKalanala', 'surya': 'SuryaKalanala'}
def _validate(stars, base_star, ayanamsa):
if not isinstance(stars, dict) or set(stars) != set(PLANETS): raise ValueError('all nine planetary star indices required')
if type(base_star) is not int or not 1 <= base_star <= 28: raise ValueError('base_star must be 1..28')
if not isinstance(ayanamsa,str) or not ayanamsa.strip(): raise ValueError('explicit ayanamsa required')
for k,v in stars.items():
if type(v) is not int or not 1 <= v <= 27: raise ValueError(f'{k} star index must be 1..27')
return normalize_ayanamsa_name(ayanamsa)
def read_pyjhora_kalanala_positions(kind: str, timeout: int = 30) -> dict:
"""Black-box read of the installed PyJHora slot->(x, y) literal; never imports Qt."""
if kind not in _CLASS_NAMES: raise ValueError('kind must be chandra or surya')
code = '''import ast, json, importlib.util, sys
from pathlib import Path
p = Path(importlib.util.find_spec('jhora').origin).parent / 'ui/chakra.py'
tree = ast.parse(p.read_bytes())
cls = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == sys.argv[1])
init = next(n for n in cls.body if isinstance(n, ast.FunctionDef) and n.name == '__init__')
hits = [n.value for n in init.body if isinstance(n, ast.Assign) and isinstance(n.value, ast.Dict)
and any(isinstance(t, ast.Attribute) and t.attr == 'rasi_labels' for t in n.targets)]
if len(hits) != 1: raise ValueError('ambiguous layout definition')
print(json.dumps({str(k): list(v) for k, v in ast.literal_eval(hits[0]).items()}))
'''
run = subprocess.run([sys.executable, '-c', code, _CLASS_NAMES[kind]], text=True,
capture_output=True, timeout=timeout, check=True)
return {int(k): tuple(v) for k, v in json.loads(run.stdout).items()}
def build_kalanala_report(*, stars: dict, base_star: int, ayanamsa: str, kind: str='chandra',
positions: dict | None = None) -> dict:
mode = _validate(stars,base_star,ayanamsa)
if kind not in ('chandra','surya'): raise ValueError('kind must be chandra or surya')
mapped = None
if positions is not None:
if set(positions) != set(range(1, 29)): raise ValueError('positions must cover slots 1..28')
mapped = {((base_star-1)+(slot-1))%28+1: tuple(xy) for slot,xy in positions.items()}
rows=[]
for planet, star in stars.items():
# Upstream first rotates the star labels, then looks up the planet's
# absolute star (inserting the Abhijit slot after original star 21).
# Rotating the planet as well would cancel the base-star setting.
target = star + 1 if star > 21 else star
slot = (target - base_star) % 28 + 1
rows.append({'planet':planet,'source_star_index':star,'target_star_index':target,
'layout_slot':slot,'grid':mapped[target] if mapped else None})
evidence={'schema':'jyotish.kalanala_chakra.v1','status':'calculated' if mapped else 'partial',
'variant_profile_id':f'{kind}_kalanala_pyjhora_layout_v1','tradition_profile':'kalanala_external_pyjhora',
'kind':kind,'base_star':base_star,'ayanamsa':mode,'rows':rows,'source_url':SOURCE,
'grid_status':'external_pyjhora_layout' if mapped else 'not_carried_agpl_boundary',
'claim_boundary':'Layout and star placement only. No Vedha, transit activation, event timing or certainty conclusion.'}
lines=[f'# {kind.title()} Kalanala Chakra','',evidence['claim_boundary'],'','| Planet | Source star | Target star | Slot | Grid |','|---|---:|---:|---:|---|']
lines += [f'| {r["planet"]} | {r["source_star_index"]} | {r["target_star_index"]} | {r["layout_slot"]} | {r["grid"] or "—"} |' for r in rows]
return {'status':evidence['status'],'customer_export_admitted':False,'evidence_appendix':[evidence],'exports':{'ai_evidence_bundle':deepcopy(evidence),'markdown':'\n'.join(lines)}}