SSM-JTK – Data & Calibration — Calibration pseudocode (end-to-end) (2.7A)

Goal
A complete, deterministic calibration loop from inputs → selected kernel → portable manifest. Plain ASCII, reproducible.

Helpers (utilities)
wrap360(x) = x - 360*floor(x/360)
wrap180(d) = ((d + 180) % 360) - 180
unwrap_series(L_sid_deg) -> y (add/subtract 360 so neighbors stay continuous)
central_diff(y, dt) -> v (speed; endpoints one-sided), with dt = 1 on daily grids
cusp_dist_deg(x) = min( (x % 30), 30 - (x % 30) )

Event detectors (reference stubs)
detect_crossings(t, y_unwrapped, step=30) -> tau_k (linear-interpolated 30° crossings)
detect_stations(t, y_unwrapped) -> tau_s (minima of |v|; dedup within 2 d)
pair_events(A_times, B_times, cap_days) -> pairs (nearest-neighbor within a cap)

Calibration loop (single body)

  1. Time anchor & sampling.
    t0 = midpoint(train_start, train_stop) ; t = days_since(date, t0) ; pick a declared schedule (daily, D1D15, etc.).
  2. Inputs.
    Obtain daily sidereal angles L_sid_deg at the chosen timestamp (siderealize first if needed).
  3. Pre-process.
    y = unwrap_series(L_sid_deg) ; v = central_diff(y, dt=1).
  4. Design candidates.
    Choose family (fixed-n or free-n); define carrier sets Ω (e.g., BASE, CANDIDATE).
  5. Fit per candidate.
  • fixed-n: y_fit(t) = L_actual_unwrapped(t) - n*t ; X = [1, sin(w1*t), cos(w1*t), ...]
  • free-n : y_fit(t) = L_actual_unwrapped(t) ; X = [1, t, sin(w1*t), cos(w1*t), ...]
    Solve OLS: beta_hat = argmin || y_fit - X*beta ||_2^2 ; compute RSS, k, N.
  1. Score per candidate.
    BIC = k*log(N) + N*log( max(RSS/N, 1e-16) )
    Build L_hat_unwrapped(t) and central-diff v_hat.
    mae_v = mean( |v_hat - v_act| ) ; MAE_deg_train on wrapped degrees via wrap180.
    cusp_MAE_train = mean( |cusp(model) - cusp(actual)| ) with cusp(x) = cusp_dist_deg(x).
    Combined loss: loss = 1.0*MAE_deg_train + 0.3*cusp_MAE_train + 0.4*mae_v.
  2. Gate & select.
    ADMISSIBLE iff (BIC_EXTRA <= BIC_BASE - 6.0) and (loss_EXTRA < loss_BASE).
    Pick the admissible model with smallest (loss, then BIC); else use BASE.
  3. Freeze manifest.
  • fixed-n: store a0, harmonics; set n_deg_per_day = 360 / P_sid.
  • free-n : store a0, learned n_deg_per_day, harmonics.
  1. Report metrics (TEST slice).
    Compute: MAE_deg, P90_deg, MAX_deg, misclass_rate, rasi_cross_MAE_days, cusp_dist_MAE_deg, station_date_MAE_days.
    Use pairing caps: crossings ±60 d, stations ±90 d.

Metrics (TRAIN/TEST definitions)
err_deg[i] = | wrap180( L_model[i] - L_actual[i] ) |
Aggregate: MAE_deg, P90_deg, MAX_deg, misclass_rate.
rasi_cross_MAE_days via paired 30° crossings; station_date_MAE_days via paired stations; cusp_dist_MAE_deg via cusp_dist_deg.

Manifest (portable single source of truth)
{ "planet": "...", "family": "fixed-n|free-n", "t0": "YYYY-MM-DD", "P_sid": <days or null>, "n_deg_per_day": <float>, "omegas": { ... }, "beta": { ... }, "notes": "midpoint anchor; BIC-loss gate" }

Evaluator (runtime reminder)
L_hat_deg(D) = wrap360( a0 + n_deg_per_day*t + sum_k[ c_k*sin(omega_k*t) + d_k*cos(omega_k*t) ] ) with t = days_since(D, t0).

Navigation
Back: SSM-JTK – Data & Calibration — OLS target, BIC, event-aware loss, selection (2.7)
Next: SSM-JTK – Data & Calibration — Event detectors (reference) (2.8)