Scope. How to reproduce back-tests and publish per-body scorecards using only daily CSVs and simple, transparent rules. This page is conceptual and self-contained; no downloads are offered here.
5.1 Prepare a reference CSV (observation-only)
- Frame & time.
geocentric,sidereal (Lahiri), sampled daily at05:30 IST. - Columns.
date(ISOYYYY-MM-DD),L_actual_deg(degrees in[0,360)). - Source. Your own tables or a reputable public ephemeris (verification only).
Attribution text (copyable):Angles cross-checked for observation-only verification against a public ephemeris service; kernels and evaluations are our own derived work under this document's terms.
5.2 Fit & select (summary, ready to implement)
- Choose windows & anchor
TRAIN = [YYYY-.. .. YYYY-..],TEST = [YYYY-.. .. YYYY-..](disjoint).t0 := midpoint(TRAIN)andt := days_since(date, t0). - Unwrap for OLS & events
Carry+-360soy(t)is continuous. - Design
- Fixed-n:
y = L_unwrapped - n*t; regressors[1, sin(wt), cos(wt), ...] - Free-n:
y = L_unwrapped; regressors[1, t, sin(wt), cos(wt), ...]
Export rule: roll the learned slope inton_deg_per_dayin the manifest; do not export a separate slope term.
- Model score & gate
BIC = k*log(N) + N*log(max(RSS/N, 1e-16))loss = 1.0*MAE_deg + 0.3*cusp_MAE + 0.4*MAE_speed
Admit extras iffDeltaBIC >= 6ANDlossdecreases. - Export manifest (schema per §4.1).
- Re-evaluate on TEST and compute metrics
MAE_deg,P90_deg(orP95for Moon),MAX_deg,misclass_rate,rasi_cross_MAE_days,station_date_MAE_days,cusp_dist_MAE_deg.
5.3 Publish the scorecard (template)
Per-body one-liner (public post):SSM-JTK {planet} — TRAIN {YYYY-..} | TEST {YYYY-..}Model={selected_model}; MAE={MAE_deg:.2f} deg; P90={P90_deg:.2f} deg;CrossMAE={rasi_cross_MAE_days:.2f} d; StationMAE={station_date_MAE_days:.2f} dManifest: per-body manifest (mid-anchor; DeltaBIC>=6 & event-loss gate)
(For Moon, you may replace P90 with P95 and foreground cusp/crossing metrics.)
5.4 What to keep locally (per planet)
- Manifest. Per-body JSON manifest (immutable once published).
- Back-test summary. Single CSV row with key metrics and ranges.
- Back-test time-series.
date, L_actual_deg, L_model_deg, deg_err, rasi_actual, rasi_model. - Reproduction notes. TRAIN/TEST spans, sampling mode, anchors, acceptance gates.
5.5 CSV-only pathway (no manifests)
If you only share daily “golden” angles, third parties can still generate scorecards by comparing:
- Predicted (from golden).
planet,date,lon_sidereal_lahiri_deg - Reference (their ephemeris CSV).
planet,date,L_actual_deg
Required alignment (per body/day):wrap360(x) = x - 360*floor(x/360)wrap180(d) = ((d + 180) % 360) - 180rasi(x) = floor( wrap360(x) / 30 )cusp_dist(x) = min( wrap360(x) % 30 , 30 - (wrap360(x) % 30) )err_deg = | wrap180(L_pred - L_actual) |
Event timing (CSV-only).
- 30 deg crossings: unwrap both series; linearly interpolate on the
k*30grid; pair nearest events with a cap (e.g.,60 d); average|Delta t|. - Stations: central-difference speed on unwrapped series; pick local minima; pair with a cap (e.g.,
90 d); average|Delta t|. - Nodes identity (optional check): ensure
Ketu(t) = wrap360(Rahu(t) + 180)per day.
5.6 Tiny reference script (CSV-only scorecard)
# scorecard_csv_only.py (plain ASCII; reads two CSVs with identical date/planet coverage)
# Usage: python scorecard_csv_only.py golden.csv reference.csv Venus
import sys, csv, math, datetime as dt
def wrap360(x): return (x % 360.0 + 360.0) % 360.0
def wrap180(d):
y = (d + 180.0) % 360.0
return y - 180.0
def rasi(x): return int(math.floor(wrap360(x) / 30.0))
def cusp_dist(x):
q = wrap360(x) % 30.0
return min(q, 30.0 - q)
def read_csv(path):
rows = []
with open(path, newline="", encoding="utf-8") as f:
for r in csv.DictReader(f):
rows.append({
"planet": r["planet"].strip().lower(),
"date": dt.date.fromisoformat(r["date"]),
"L": float(r.get("lon_sidereal_lahiri_deg") or r.get("L_actual_deg"))
})
return rows
def group_by(rows, planet):
return [r for r in rows if r["planet"] == planet.lower()]
def unwrap(series):
y = [series[0]]
for i in range(1, len(series)):
x = series[i]; best = x
for k in (-1, 0, 1):
cand = x + 360.0 * k
if abs(cand - y[-1]) < abs(best - y[-1]): best = cand
y.append(best)
return y
def central_diff(y):
if len(y) < 3: return [0.0]*len(y)
return [y[1]-y[0]] + [(y[i+1]-y[i-1])/2.0 for i in range(1,len(y)-1)] + [y[-1]-y[-2]]
def detect_crossings(t, y_unwrapped, step=30.0):
cr = []
y0, y1 = min(y_unwrapped[0], y_unwrapped[-1]), max(y_unwrapped[0], y_unwrapped[-1])
kmin = math.floor(y0/step) - 1; kmax = math.ceil(y1/step) + 1
for k in range(int(kmin), int(kmax)+1):
target = k*step
for i in range(1, len(y_unwrapped)):
s0 = y_unwrapped[i-1] - target
s1 = y_unwrapped[i] - target
if s0 == 0.0: cr.append(t[i-1]); continue
if s0*s1 < 0.0:
frac = (target - y_unwrapped[i-1]) / (y_unwrapped[i] - y_unwrapped[i-1])
tau = t[i-1] + frac*(t[i]-t[i-1])
cr.append(tau)
return cr
def pair_events(A, B, cap_days):
used, pairs = set(), []
for a in A:
jbest, dbest = None, None
for j,b in enumerate(B):
if j in used: continue
d = abs(b - a)
if dbest is None or d < dbest:
jbest, dbest = j, d
if dbest is not None and dbest <= cap_days:
pairs.append((a, B[jbest])); used.add(jbest)
return pairs
def main(golden_path, reference_path, planet):
G = group_by(read_csv(golden_path), planet)
R = group_by(read_csv(reference_path), planet)
if len(G) != len(R):
raise SystemExit("Mismatched coverage; align date/planet first.")
G.sort(key=lambda r: r["date"]); R.sort(key=lambda r: r["date"])
dates = [r["date"] for r in G]
Lg = [r["L"] for r in G]; Lr = [r["L"] for r in R]
# pointwise degree metrics
errs = [abs(wrap180(Lg[i]-Lr[i])) for i in range(len(dates))]
mae = sum(errs)/len(errs)
p90 = sorted(errs)[int(0.90*len(errs))-1]
mxx = max(errs)
mis = sum(1 for i in range(len(dates)) if rasi(Lg[i])!=rasi(Lr[i]))/len(dates)
# cusp MAE
cusp_mae = sum(abs(cusp_dist(Lg[i]) - cusp_dist(Lr[i])) for i in range(len(dates))) / len(dates)
# events (unwrapped)
t = [(d - dates[0]).days for d in dates]
yG = unwrap(Lg); yR = unwrap(Lr)
# crossings
cG = detect_crossings(t, yG, 30.0)
cR = detect_crossings(t, yR, 30.0)
Cp = pair_events(cR, cG, cap_days=60.0)
cross_mae = (sum(abs(a-b) for a,b in Cp)/len(Cp)) if Cp else float("nan")
# stations (simple)
vG, vR = central_diff(yG), central_diff(yR)
sG = [t[i] for i in range(1,len(vG)-1) if abs(vG[i])<=abs(vG[i-1]) and abs(vG[i])<=abs(vG[i+1])]
sR = [t[i] for i in range(1,len(vR)-1) if abs(vR[i])<=abs(vR[i-1]) and abs(vR[i])<=abs(vR[i+1])]
Sp = pair_events(sR, sG, cap_days=90.0)
stat_mae = (sum(abs(a-b) for a,b in Sp)/len(Sp)) if Sp else float("nan")
print(f"Planet={planet}")
print(f"MAE_deg={mae:.6f} P90_deg={p90:.6f} MAX_deg={mxx:.6f} misclass_rate={mis:.6f}")
print(f"cusp_dist_MAE_deg={cusp_mae:.6f}")
print(f"rasi_cross_MAE_days={cross_mae:.6f}")
print(f"station_date_MAE_days={stat_mae:.6f}")
if __name__ == "__main__":
if len(sys.argv) != 4:
print("Usage: python scorecard_csv_only.py golden.csv reference.csv PlanetName")
sys.exit(2)
main(sys.argv[1], sys.argv[2], sys.argv[3])
Notes. Identical date coverage expected; pairing caps are 60 d (crossings) and 90 d (stations). If events are too sparse in the chosen window, printing NaN is acceptable.
Publishing checklist (quick)
- Frame/time match confirmed (
05:30 IST, geocentric, sidereal Lahiri). - Manifests exported (mid-anchor; parsimony gate met).
- Scorecards computed on a disjoint TEST span and formatted via §5.3.
- Optional CSV-only audit with the tiny script in §5.6 for third-party verification.
Navigation
Back: SSM-JTK – Locked Kernels – Tiny Reproduction Kit (4)
Next: SSM-JTK – Methodology — short walkthrough (6)
Directory of Pages
SSM-JTK – Series index & links
Disclaimer
The contents in the Shunyaya Symbolic Mathematics Jyotish Transit Kernel (SSM-JTK) materials are research/observation material. They are not astrological advice, not a scientific ephemeris, and not operational guidance. Do not use for safety-critical, medical, legal, or financial decisions. Use at your own discretion; no warranties are provided; results depend on correct implementation and inputs.
Explore Further
https://github.com/OMPSHUN