Files
Jyotisha/scripts/kota_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

164 lines
9.2 KiB
Python

"""Source-named Kota/Durga structure, guardians and actual-motion routes.
Modern symmetrical plan; no imported external algorithm or predictive score.
"""
import argparse
from copy import deepcopy
import json
from pathlib import Path
import sys
if __package__ in (None, ''):
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from scripts.ayanamsa_utils import normalize_ayanamsa_name
SOURCE = 'https://vijayalur.com/2011/06/02/kota-chakra/'
TABLE_SOURCE = 'https://www.indiadivine.org/content/topic/1492433-kota-cakra/'
ALTERNATIVE_SOURCE = 'https://www.theartofvedicastrology.com/?page_id=389'
PADA_PROFILE = 'kota_2006_pada_guard_modern_v1'
STAR_PROFILE = 'kota_vs_nakshatra_guard_modern_v1'
PLANETS = ('Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn','Rahu','Ketu')
RASHI_LORDS = ('Mars','Venus','Mercury','Moon','Sun','Mercury','Venus','Mars','Jupiter','Saturn','Saturn','Jupiter')
STAR_LORDS = ('Ketu','Venus','Sun','Moon','Mars','Rahu','Jupiter','Saturn','Mercury')
# Numeric lookup facts transcribed from the signed Swee/Pradeep Gaurav thread.
# 27 ordinary stars, four padas each; source's 'Venusz' normalized to Venus.
PALA_TABLE = (
('Venus','Venus','Venus','Moon'), ('Moon',)*4, ('Sun',)*4,
('Sun','Moon','Moon','Moon'), ('Moon','Moon','Mars','Mars'),
('Mars','Mars','Mars','Venus'), ('Mars','Mars','Rahu','Rahu'),
('Rahu','Rahu','Rahu','Mercury'), ('Mercury',)*4, ('Saturn',)*4,
('Saturn','Mercury','Mercury','Mercury'), ('Mercury','Mercury','Saturn','Saturn'),
('Saturn','Rahu','Mercury','Mercury'), ('Saturn','Saturn','Moon','Moon'),
('Moon','Moon','Moon','Jupiter'), ('Jupiter',)*4, ('Jupiter',)*4,
('Jupiter','Moon','Moon','Moon'), ('Moon','Moon','Saturn','Saturn'),
('Saturn','Jupiter','Saturn','Mercury'), ('Saturn','Saturn','Venus','Venus'),
('Mars',)*4, ('Mars',)*4, ('Mars','Rahu','Rahu','Rahu'),
('Rahu','Rahu','Jupiter','Jupiter'), ('Jupiter','Jupiter','Venus','Venus'),
('Jupiter','Jupiter','Venus','Venus'),
)
LAYERS = ('bahya','prakara','madhya','stambha','madhya','prakara','bahya')
GATES = ('NE','SE','SW','NW')
EXITS = ('E','S','W','N')
def _integer(value, maximum, label):
if type(value) is not int or not 1 <= value <= maximum:
raise ValueError(f'{label} must be integer1..{maximum}')
return value
def _ring(value, basis):
_integer(value,27 if basis=='nakshatra_27' else 28,'star')
return value+(value>21) if basis=='nakshatra_27' else value
def build_kota_report(*, stars, motions, birth_star, birth_pada, moon_sign,
star_basis, ayanamsa, variant_profile_id):
"""stars are transits; birth_star/pada and moon_sign describe natal Moon.
All nine motions must be direct/retrograde/stationary. Node motion is input,
so true-node direct observations are not silently forced retrograde.
"""
if variant_profile_id not in (PADA_PROFILE,STAR_PROFILE):
raise ValueError('explicit supported variant_profile_id required')
if star_basis not in ('nakshatra_27','abhijit_ring_28'):
raise ValueError('explicit star_basis required for both birth and transit stars')
if not isinstance(stars,dict) or set(stars)!=set(PLANETS):
raise ValueError('all nine transit stars required')
if not isinstance(motions,dict) or set(motions)!=set(PLANETS):
raise ValueError('all nine actual motion states required')
if any(m not in ('direct','retrograde','stationary') for m in motions.values()):
raise ValueError('motion must be direct, retrograde or stationary')
if motions['Sun']!='direct' or motions['Moon']!='direct':
raise ValueError('Sun and Moon must be direct')
if not isinstance(ayanamsa,str) or not ayanamsa.strip():
raise ValueError('explicit ayanamsa required')
mode=normalize_ayanamsa_name(ayanamsa)
_integer(moon_sign,12,'moon_sign')
base=_ring(birth_star,star_basis)
natal27=None if base==22 else base-(base>22)
if natal27 is None:
if birth_pada is not None:
raise ValueError('Abhijit uses no ordinary27-star pada; pass birth_pada=None')
if moon_sign!=10:
raise ValueError('Abhijit must have Capricorn Moon')
else:
_integer(birth_pada,4,'birth_pada')
expected=((natal27-1)*4+birth_pada-1)//9+1
if moon_sign!=expected:
raise ValueError('moon_sign conflicts with supplied natal star/pada')
transit={p:_ring(stars[p],star_basis) for p in PLANETS}
rows=[]
for i in range(28):
group,pos=divmod(i,7)
ring=(base+i-1)%28+1
road='entry' if pos<3 else 'pillar_junction' if pos==3 else 'exit'
rows.append(dict(relative_star=i+1,ring_star_index=ring,
nakshatra_index=None if ring==22 else ring-(ring>22),is_abhijit=ring==22,
layer=LAYERS[pos],road=road,
direction=GATES[group] if pos<3 else EXITS[group] if pos>3 else None,
entry_gate=GATES[group],exit_side=EXITS[group],
planets=[p for p,star in transit.items() if star==ring]))
placements={}
for planet in PLANETS:
row=deepcopy(next(r for r in rows if planet in r['planets']))
motion=motions[planet]
if motion=='stationary': travel='stationary'
elif row['road']=='pillar_junction': travel='at_pillar'
elif motion=='retrograde': travel='exit' if row['road']=='entry' else 'entry'
else: travel=row['road']
row.update(motion=motion,travel=travel)
placements[planet]=row
swami=RASHI_LORDS[moon_sign-1]
if variant_profile_id==PADA_PROFILE:
pala='Venus' if natal27 is None else PALA_TABLE[natal27-1][birth_pada-1]
pala_result=dict(status='calculated',planet=pala,source_url=TABLE_SOURCE)
elif natal27 is None:
pala=None
pala_result=dict(status='blocked',planet=None,reason='No ordinary Vimshottari lord for Abhijit; select named pada profile or supply ordinary natal-star allocation separately')
else:
pala=STAR_LORDS[(natal27-1)%9]
pala_result=dict(status='calculated',planet=pala,source_url=ALTERNATIVE_SOURCE)
guardians=dict(swami=dict(planet=swami,placement=deepcopy(placements[swami])),
pala={**pala_result,'placement':deepcopy(placements[pala]) if pala else None})
# Evaluate only named placement predicates, not outcomes. The two sources
# prefer different zones; retain both rather than silently unify them.
checks=[dict(rule_id='vijayalur2011_swami_madhya',matched=placements[swami]['layer']=='madhya',source_url=SOURCE),
dict(rule_id='gaurav2006_swami_stambha',matched=placements[swami]['layer']=='stambha',source_url=TABLE_SOURCE)]
if pala:
checks += [dict(rule_id='vijayalur2011_pala_bahya',matched=placements[pala]['layer']=='bahya',source_url=SOURCE),
dict(rule_id='gaurav2006_pala_prakara',matched=placements[pala]['layer']=='prakara',source_url=TABLE_SOURCE)]
data=dict(schema='jyotish.kota_native.v1',status='calculated' if pala else 'partial',
variant_profile_id=variant_profile_id,tradition_profile='modern_kota_parallel_guard_profiles',
source_url=SOURCE,ayanamsa=mode,star_basis=star_basis,base_ring_star=base,
natal_moon_sign=moon_sign,birth_pada=birth_pada,rows=rows,placements=placements,
guardians=guardians,placement_rule_checks=checks,
claim_boundary='Source-named structure and rule predicates, not event, health or lifespan conclusions.',
limitations=['Transit positions and actual motions are supplied, not recomputed ephemerides',
'27-star basis inserts an empty Abhijit slot;28-ring allocation must be supplied explicitly',
'Pillar is a shared route junction; stationary is not entry or exit',
'Guardian preferred-zone rules differ; matched predicates are not predictions',
'No natural/functional benefic classification, Durga Bhanga verdict, Vedha or Dasha corroboration inferred',
'Year, balance and child periods not applicable; no automatic customer routing'])
lines=['# Kota / Durga Chakra','',f'Profile: {variant_profile_id}',
f'Kota Swami: {swami}; Kota Pala: {pala or "blocked (Abhijit has no ordinary star lord)"}.',data['claim_boundary'],'',
'| Planet | Star28 | Layer | Road | Motion | Travel |','|---|---:|---|---|---|---|']
lines += [f"| {p} | {r['ring_star_index']} | {r['layer']} | {r['road']} | {r['motion']} | {r['travel']} |" for p,r in placements.items()]
lines += ['', 'Named placement checks (not outcomes):']+[f"- {c['rule_id']}: {c['matched']}" for c in checks]
lines += ['']+[f'- {x}' for x in data['limitations']]
return dict(status=data['status'],customer_export_admitted=False,evidence_appendix=[data],
exports=dict(ai_evidence_bundle=deepcopy(data),markdown='\n'.join(lines)))
def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--request-json',required=True)
parser.add_argument('--format',choices=('json','markdown'),default='json')
args=parser.parse_args()
result=build_kota_report(**json.loads(Path(args.request_json).read_text()))
print(result['exports']['markdown'] if args.format=='markdown' else json.dumps(result,ensure_ascii=False,indent=2))
if __name__=='__main__':
main()