Files
Jyotisha/scripts/trikona_dasha.py
T
jesse-uxGrok (agent loop)Cursor
424f03a4d5 feat: vendor four upstream Goel Jaimini strength modules
Issue #94 T1. Place goel_jaimini_strength, trikona_raman, sthira_brahma_goel_short_method, and trikona_dasha from upstream 5fb8379c under scripts/. The only local edit is the SOURCE book citation. Raise ROOT_PY_BUDGET from 448 to 452.

Co-Authored-By: Grok (agent loop)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-22 00:52:52 +08:00

167 lines
10 KiB
Python

"""Separate Trikona profiles: Raghava Bhatta MD and Raman confirmed-parent AD."""
import argparse
from copy import deepcopy
from datetime import datetime, timedelta, timezone
import json
import math
from pathlib import Path
import sys
if __package__ in (None, ''):
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from scripts.narayana_dasha import SIGNS, SIGN_LORDS, _NARAYANA_DIGNITY
PROFILE = 'trikona_raghava_bhatta_explicit_seed_md_v1'
SOURCE = 'https://saptarishisastrology.com/trikona-dasa-a-critical-review-by-iranganti-rangacharya/'
RAMAN_AD_PROFILE = 'trikona_raman_94_confirmed_full_md_ad_v1'
RAMAN_SOURCE = 'https://www.scribd.com/doc/127930441/Studies-in-Jaimini-Astrology-by-b-v-Raman'
def build_trikona_raman_antardasa_report(*, md_sign_idx, full_md_years,
full_md_start, parent_source, parent_profile, year_days, profile):
"""Raman section94 AD only; a confirmed, untruncated parent is input."""
if profile != RAMAN_AD_PROFILE or parent_profile != 'raman_studies_jaimini_92_93':
raise ValueError('explicit Raman AD and Raman sections92/93 parent profiles required')
if type(md_sign_idx) is not int or not 0 <= md_sign_idx < 12:
raise ValueError('md_sign_idx must be integer 0..11')
if not isinstance(parent_source, str) or not parent_source.strip():
raise ValueError('parent_source must identify the confirmed full MD, not birth balance')
for name, value, low, high in [('full_md_years', full_md_years, 0, 12),
('year_days', year_days, 300, 400)]:
if (isinstance(value, bool) or not isinstance(value, (int, float)) or
not math.isfinite(value) or not low <= value <= high or value == 0):
raise ValueError(f'{name} outside supported finite range')
if not isinstance(full_md_start, str):
raise ValueError('full_md_start must be aware ISO datetime')
anchor = datetime.fromisoformat(full_md_start.replace('Z', '+00:00'))
if anchor.tzinfo is None or anchor.utcoffset() is None:
raise ValueError('full_md_start must include UTC offset')
anchor = anchor.astimezone(timezone.utc)
# Raman section155 specifies these groups; neither parity nor ninth-from-parent.
direction = 1 if md_sign_idx in (0, 1, 2, 6, 7, 8) else -1
offsets = [group + trine for group in range(4) for trine in (0, 4, 8)]
try:
edges = [(anchor + timedelta(days=full_md_years * year_days * i / 12)).isoformat()
for i in range(13)]
except OverflowError as exc:
raise ValueError('calendar boundary outside supported datetime range') from exc
periods = [dict(sign_idx=(md_sign_idx + direction * offset) % 12,
sign=SIGNS[(md_sign_idx + direction * offset) % 12],
years=full_md_years / 12, start_utc=edges[i], end_utc=edges[i + 1],
start_age=full_md_years * i / 12, end_age=full_md_years * (i + 1) / 12)
for i, offset in enumerate(offsets)]
result = dict(status='parameter_sensitive', variant_profile_id=profile,
tradition_profile='bv_raman_studies_in_jaimini_astrology', source=RAMAN_SOURCE,
source_locator='section94 pp61-62; Savya groups section155 p127',
parent=dict(sign_idx=md_sign_idx, sign=SIGNS[md_sign_idx], years=full_md_years,
start_utc=edges[0], end_utc=edges[-1], source=parent_source,
profile=parent_profile, status='supplied_confirmed_full_parent'),
year_days=year_days, direction=direction, periods=periods,
child_periods=dict(level='AD', status='executed', count=12),
pratyantardasa=dict(status='blocked', reason='section94_does_not_establish_PD'),
limits=['Parent seed, duration and full start supplied, not calculated or independently verified',
'No birth-balance clipping; do not supply a residual MD as a full parent',
'Fixed year_days is explicit caller convention, not a source date oracle',
'Raman AD only; never attach to Raghava Bhatta or PyJHora parents automatically',
'No PD, event inference or default customer export'])
lines = ['# Trikona — Raman confirmed full MD / AD', '', f'Profile: {profile}',
f'Source: {RAMAN_SOURCE} — {result["source_locator"]}', '',
'| AD | Years | Start UTC | End UTC |', '| --- | --- | --- | --- |']
lines += [f"| {p['sign']} | {p['years']} | {p['start_utc']} | {p['end_utc']} |" for p in periods]
lines += ['', *result['limits'], '', '```json', json.dumps(result, ensure_ascii=False, indent=2), '```']
return dict(status=result['status'], customer_export_admitted=False,
evidence_appendix=[result], exports=dict(markdown='\n'.join(lines),
ai_evidence_bundle=deepcopy(result)))
def build_trikona_native_report(*, lagna_sign_idx, seed_sign_idx, seed_source,
planet_longitudes, epoch, year_days, profile):
if profile != PROFILE:
raise ValueError('explicit Raghava Bhatta profile required')
if any(type(v) is not int or not 0 <= v < 12 for v in (lagna_sign_idx, seed_sign_idx)):
raise ValueError('sign indices must be integers 0..11')
if (seed_sign_idx - lagna_sign_idx) % 12 not in (0, 4, 8):
raise ValueError('seed must be Lagna, fifth or ninth')
if not isinstance(seed_source, str) or not seed_source.strip():
raise ValueError('seed_source must explain the supplied strength decision')
required = set(SIGN_LORDS.values())
if not isinstance(planet_longitudes, dict) or set(planet_longitudes) != required:
raise ValueError('exactly seven classical planetary longitudes required; no node co-lords')
if any(isinstance(v, bool) or not isinstance(v, (int, float)) or
not math.isfinite(v) or not 0 <= v < 360 for v in planet_longitudes.values()):
raise ValueError('planetary longitudes must be finite in [0,360)')
if isinstance(year_days, bool) or not isinstance(year_days, (int, float)) or not math.isfinite(year_days) or not 300 <= year_days <= 400:
raise ValueError('explicit year_days 300..400 required')
if not isinstance(epoch, str):
raise ValueError('aware ISO epoch required')
anchor = datetime.fromisoformat(epoch.replace('Z', '+00:00'))
if anchor.tzinfo is None or anchor.utcoffset() is None:
raise ValueError('epoch must include UTC offset')
anchor = anchor.astimezone(timezone.utc)
direction = 1 if seed_sign_idx % 2 == 0 else -1
if seed_sign_idx % 3 == 0:
offsets = list(range(12))
elif seed_sign_idx % 3 == 1:
offsets = [5 * i for i in range(12)]
else:
offsets = [group + trine for group in range(4) for trine in (0, 4, 8)]
periods = []
age = 0
for offset in offsets:
sign = (seed_sign_idx + direction * offset) % 12
lord = SIGN_LORDS[SIGNS[sign]]
lord_sign = int(planet_longitudes[lord] // 30)
count_direction = 1 if sign % 2 == 0 else -1
count = ((lord_sign - sign) * count_direction) % 12 + 1
dignity = _NARAYANA_DIGNITY[lord]
adjustment = int(lord_sign in dignity['exalted']) - int(lord_sign in dignity['debilitated'])
years = 12 if lord_sign == sign else min(12, count + adjustment)
end = age + years
try:
start_utc = (anchor + timedelta(days=age * year_days)).isoformat()
end_utc = (anchor + timedelta(days=end * year_days)).isoformat()
except OverflowError as exc:
raise ValueError('calendar boundary outside supported datetime range') from exc
periods.append(dict(sign_idx=sign, sign=SIGNS[sign], lord=lord,
lord_sign_idx=lord_sign, inclusive_count=count, dignity_adjustment=adjustment,
years=years, start_age=age, end_age=end, start_utc=start_utc, end_utc=end_utc))
age = end
result = dict(status='parameter_sensitive', variant_profile_id=PROFILE,
tradition_profile='raghava_bhatta_as_presented_by_rangacharya', source=SOURCE,
lagna_sign_idx=lagna_sign_idx, seed_sign_idx=seed_sign_idx,
seed_selection=dict(status='supplied_not_calculated', source=seed_source),
epoch_utc=anchor.isoformat(), year_days=year_days, periods=periods,
child_periods=dict(status='blocked', reason='method_specific_AD_PD_rules_not_established'),
limits=['Supplied strongest trine; strength selection is not calculated',
'Full first MD begins at explicit epoch; no fractional birth balance inferred',
'Fixed-day calendar is caller convention, not verified source dates',
'First cycle only; no automatic customer export or event certainty'])
lines = ['# Trikona — Raghava Bhatta / Rangacharya method 2', '',
'Status: parameter_sensitive', f'Source: {SOURCE}', f'Profile: {PROFILE}', '',
'| Sign | Years | Start UTC | End UTC |', '| --- | --- | --- | --- |']
lines += [f"| {p['sign']} | {p['years']} | {p['start_utc']} | {p['end_utc']} |" for p in periods]
lines += ['', *result['limits'], 'AD/PD: blocked — method-specific rules not established',
'', '```json', json.dumps(result, ensure_ascii=False, indent=2), '```']
return dict(status=result['status'], customer_export_admitted=False,
evidence_appendix=[result], exports=dict(markdown='\n'.join(lines), ai_evidence_bundle=deepcopy(result)))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--request', type=Path, required=True)
parser.add_argument('--mode', choices=('method2-md', 'raman-confirmed-md-ad', 'raman-native'), default='method2-md')
parser.add_argument('--format', choices=('json', 'markdown'), default='json')
args = parser.parse_args()
builder = (build_trikona_raman_antardasa_report if args.mode == 'raman-confirmed-md-ad'
else build_trikona_native_report)
if args.mode == 'raman-native':
from scripts.trikona_raman import build_trikona_raman_report
builder = build_trikona_raman_report
report = builder(**json.loads(args.request.read_text()))
print(report['exports']['markdown'] if args.format == 'markdown' else json.dumps(report, ensure_ascii=False, indent=2))
if __name__ == '__main__':
main()