nx_forecast.nx source
↩ module page · 151 lines · 4081 B
1// nx_forecast.nx -- time-series forecasting primitives.
2//
3// Classical recursive forecasters complementing nx_kalman:
4//
5// 1. Simple Exponential Smoothing (Brown 1956)
6// s_t = alpha * x_t + (1 - alpha) * s_{t-1}
7//
8// 2. Holt Double Exponential (Holt 1957)
9// l_t = alpha * x_t + (1 - alpha) * (l_{t-1} + b_{t-1})
10// b_t = beta * (l_t - l_{t-1}) + (1 - beta) * b_{t-1}
11// forecast(h) = l_t + h * b_t
12//
13// 3. Autoregressive AR(p) forecaster
14// x_t = phi_1 x_{t-1} + ... + phi_p x_{t-p}
15//
16// All smoothing parameters in Q14 fixed-point. Pure i64.
17//
18// What this unlocks:
19// + sensor smoothing without state-model (lighter than Kalman)
20// + trend extrapolation
21// + AR-model rollout for short-horizon prediction
22// + anomaly detection (deviation from forecast)
23//
24// Sibling of nx_ts.nx (which handles Unix-epoch <-> calendar);
25// this module forecasts numeric time series.
26//
27// genealogy_id: brown_1956_ses + holt_1957_double_exp + box_jenkins_1970
28// lineage_id: recursive_exponential_smoother + autoregressive_predictor
29
30// nx_safety_envelope:
31// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
32// sil_target: SIL1
33// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
34// verdict: NOT_YET_EVALUATED
35
36import "syscalls.nx"
37
38const NX_FORECAST_Q: i64 = 16384 // Q14
39
40// ===== Simple Exponential Smoothing ====================================
41
42struct SES {
43 s: i64,
44 alpha: i64,
45 init: i64,
46}
47
48func nx_forecast_ses_init(ses: *SES, alpha_q14: i64) -> i64 {
49 ses.s = 0
50 ses.alpha = alpha_q14
51 ses.init = 0
52 return 0
53}
54
55func nx_forecast_ses_observe(ses: *SES, x: i64) -> i64 {
56 if ses.init == 0 {
57 ses.s = x
58 ses.init = 1
59 return 0
60 }
61 let a: i64 = ses.alpha
62 ses.s = (a * x + (NX_FORECAST_Q - a) * ses.s) / NX_FORECAST_Q
63 return 0
64}
65
66func nx_forecast_ses_predict(ses: *SES) -> i64 {
67 return ses.s
68}
69
70// ===== Holt Double Exponential (level + trend) =========================
71
72struct Holt {
73 l: i64,
74 b: i64,
75 alpha: i64,
76 beta: i64,
77 init_count: i64,
78 prev: i64,
79}
80
81func nx_forecast_holt_init(h: *Holt, alpha_q14: i64, beta_q14: i64) -> i64 {
82 h.l = 0
83 h.b = 0
84 h.alpha = alpha_q14
85 h.beta = beta_q14
86 h.init_count = 0
87 h.prev = 0
88 return 0
89}
90
91func nx_forecast_holt_observe(h: *Holt, x: i64) -> i64 {
92 if h.init_count == 0 {
93 h.prev = x
94 h.init_count = 1
95 return 0
96 }
97 if h.init_count == 1 {
98 h.l = x
99 h.b = x - h.prev
100 h.init_count = 2
101 return 0
102 }
103 let a: i64 = h.alpha
104 let bt: i64 = h.beta
105 let prev_l: i64 = h.l
106 let new_l: i64 = (a * x + (NX_FORECAST_Q - a) * (prev_l + h.b)) / NX_FORECAST_Q
107 let new_b: i64 = (bt * (new_l - prev_l) + (NX_FORECAST_Q - bt) * h.b) / NX_FORECAST_Q
108 h.l = new_l
109 h.b = new_b
110 return 0
111}
112
113func nx_forecast_holt_predict(h: *Holt) -> i64 {
114 return h.l + h.b
115}
116
117func nx_forecast_holt_predict_h(h: *Holt, steps: i64) -> i64 {
118 return h.l + steps * h.b
119}
120
121// ===== AR(p) prediction (coefficients in Q14) ==========================
122//
123// history[0] = x_{t-1}, history[1] = x_{t-2}, ...
124// coeffs_q14[0] = phi_1, coeffs_q14[1] = phi_2, ...
125
126func nx_forecast_ar_predict(history: *i64, coeffs_q14: *i64, p: i64) -> i64 {
127 var acc: i64 = 0
128 var i: i64 = 0
129 while i < p {
130 acc = acc + coeffs_q14[i] * history[i]
131 i = i + 1
132 }
133 return acc / NX_FORECAST_Q
134}
135
136func nx_forecast_ar_rollout(history: *i64, coeffs_q14: *i64, p: i64,
137 n_steps: i64, out_forecasts: *i64) -> i64 {
138 var step: i64 = 0
139 while step < n_steps {
140 let next: i64 = nx_forecast_ar_predict(history, coeffs_q14, p)
141 out_forecasts[step] = next
142 var j: i64 = p - 1
143 while j > 0 {
144 history[j] = history[j - 1]
145 j = j - 1
146 }
147 history[0] = next
148 step = step + 1
149 }
150 return 0
151}