1use super::error::EconConvergence;
16
17pub const MAX_REGRESSORS: usize = 16;
19pub const MAX_OBSERVATIONS: usize = 1024;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum EconometricsError {
24 InvalidInput,
25 InsufficientData,
26 SingularSystem,
27 BufferTooSmall,
28 NonFinite,
29 NonConverged,
30 Underidentified,
31}
32
33#[derive(Debug, Clone, Copy)]
36#[repr(C)]
37pub struct CalibrationRecord {
38 pub model_name: &'static str,
40 pub data_hash: u64,
42 pub n_params: u32,
44 pub loss: f64,
46 pub iterations: u32,
48 pub epoch_seconds: u64,
50}
51
52impl CalibrationRecord {
53 pub const fn new(
54 model_name: &'static str,
55 data_hash: u64,
56 n_params: u32,
57 loss: f64,
58 iterations: u32,
59 epoch_seconds: u64,
60 ) -> Self {
61 Self {
62 model_name,
63 data_hash,
64 n_params,
65 loss,
66 iterations,
67 epoch_seconds,
68 }
69 }
70}
71
72fn gaussian_solve(a: &mut [f64], b: &mut [f64], n: usize) -> Result<(), EconometricsError> {
77 for col in 0..n {
78 let mut max_row = col;
80 let mut max_val = a[col * n + col].abs();
81 for row in (col + 1)..n {
82 let val = a[row * n + col].abs();
83 if val > max_val {
84 max_val = val;
85 max_row = row;
86 }
87 }
88 if max_val < 1e-14 {
89 return Err(EconometricsError::SingularSystem);
90 }
91 if max_row != col {
92 for j in 0..n {
93 let tmp = a[col * n + j];
94 a[col * n + j] = a[max_row * n + j];
95 a[max_row * n + j] = tmp;
96 }
97 let tmp = b[col];
98 b[col] = b[max_row];
99 b[max_row] = tmp;
100 }
101 let pivot = a[col * n + col];
103 for row in (col + 1)..n {
104 let factor = a[row * n + col] / pivot;
105 if !factor.is_finite() {
106 return Err(EconometricsError::NonFinite);
107 }
108 a[row * n + col] = 0.0;
109 for j in (col + 1)..n {
110 a[row * n + j] -= factor * a[col * n + j];
111 }
112 b[row] -= factor * b[col];
113 }
114 }
115 for row in (0..n).rev() {
117 let mut acc = b[row];
118 for j in (row + 1)..n {
119 acc -= a[row * n + j] * b[j];
120 }
121 b[row] = acc / a[row * n + row];
122 if !b[row].is_finite() {
123 return Err(EconometricsError::NonFinite);
124 }
125 }
126 Ok(())
127}
128
129pub fn ols_into(
135 x: &[f64],
136 y: &[f64],
137 n_obs: usize,
138 n_reg: usize,
139 coef_out: &mut [f64],
140 resid_out: &mut [f64],
141) -> Result<f64, EconometricsError> {
142 if n_obs == 0
143 || n_reg == 0
144 || n_obs < n_reg
145 || n_obs > MAX_OBSERVATIONS
146 || n_reg > MAX_REGRESSORS
147 {
148 return Err(EconometricsError::InsufficientData);
149 }
150 if x.len() < n_obs * n_reg
151 || y.len() < n_obs
152 || coef_out.len() < n_reg
153 || resid_out.len() < n_obs
154 {
155 return Err(EconometricsError::BufferTooSmall);
156 }
157 for v in x.iter().take(n_obs * n_reg) {
158 if !v.is_finite() {
159 return Err(EconometricsError::NonFinite);
160 }
161 }
162 for v in y.iter().take(n_obs) {
163 if !v.is_finite() {
164 return Err(EconometricsError::NonFinite);
165 }
166 }
167
168 let mut xtx = [0.0f64; MAX_REGRESSORS * MAX_REGRESSORS];
169 let mut xty = [0.0f64; MAX_REGRESSORS];
170 for i in 0..n_reg {
172 for j in 0..n_reg {
173 let mut acc = 0.0;
174 for k in 0..n_obs {
175 acc += x[k * n_reg + i] * x[k * n_reg + j];
176 }
177 xtx[i * n_reg + j] = acc;
178 }
179 }
180 for i in 0..n_reg {
182 let mut acc = 0.0;
183 for k in 0..n_obs {
184 acc += x[k * n_reg + i] * y[k];
185 }
186 xty[i] = acc;
187 }
188
189 gaussian_solve(&mut xtx, &mut xty, n_reg)?;
190
191 for i in 0..n_reg {
192 coef_out[i] = xty[i];
193 }
194
195 let y_mean = {
197 let mut sum = 0.0;
198 for k in 0..n_obs {
199 sum += y[k];
200 }
201 sum / n_obs as f64
202 };
203 let mut tss = 0.0;
204 let mut rss = 0.0;
205 for k in 0..n_obs {
206 let mut fitted = 0.0;
207 for i in 0..n_reg {
208 fitted += coef_out[i] * x[k * n_reg + i];
209 }
210 let resid = y[k] - fitted;
211 resid_out[k] = resid;
212 rss += resid * resid;
213 let dev = y[k] - y_mean;
214 tss += dev * dev;
215 }
216 let r_sq = if tss > 0.0 { 1.0 - rss / tss } else { 0.0 };
217 Ok(r_sq)
218}
219
220pub fn wls_into(
224 x: &[f64],
225 y: &[f64],
226 weights: &[f64],
227 n_obs: usize,
228 n_reg: usize,
229 coef_out: &mut [f64],
230 resid_out: &mut [f64],
231) -> Result<f64, EconometricsError> {
232 if n_obs == 0
233 || n_reg == 0
234 || n_obs < n_reg
235 || n_obs > MAX_OBSERVATIONS
236 || n_reg > MAX_REGRESSORS
237 {
238 return Err(EconometricsError::InsufficientData);
239 }
240 if x.len() < n_obs * n_reg
241 || y.len() < n_obs
242 || weights.len() < n_obs
243 || coef_out.len() < n_reg
244 || resid_out.len() < n_obs
245 {
246 return Err(EconometricsError::BufferTooSmall);
247 }
248 for v in weights.iter().take(n_obs) {
249 if !v.is_finite() || *v < 0.0 {
250 return Err(EconometricsError::NonFinite);
251 }
252 }
253
254 let mut xtwx = [0.0f64; MAX_REGRESSORS * MAX_REGRESSORS];
255 let mut xtwy = [0.0f64; MAX_REGRESSORS];
256 for i in 0..n_reg {
257 for j in 0..n_reg {
258 let mut acc = 0.0;
259 for k in 0..n_obs {
260 acc += x[k * n_reg + i] * weights[k] * x[k * n_reg + j];
261 }
262 xtwx[i * n_reg + j] = acc;
263 }
264 }
265 for i in 0..n_reg {
266 let mut acc = 0.0;
267 for k in 0..n_obs {
268 acc += x[k * n_reg + i] * weights[k] * y[k];
269 }
270 xtwy[i] = acc;
271 }
272
273 gaussian_solve(&mut xtwx, &mut xtwy, n_reg)?;
274 for i in 0..n_reg {
275 coef_out[i] = xtwy[i];
276 }
277
278 let mut rss = 0.0;
279 let mut wss = 0.0;
280 let y_wmean = {
281 let mut sw = 0.0;
282 let mut swy = 0.0;
283 for k in 0..n_obs {
284 sw += weights[k];
285 swy += weights[k] * y[k];
286 }
287 if sw > 0.0 {
288 swy / sw
289 } else {
290 0.0
291 }
292 };
293 for k in 0..n_obs {
294 let mut fitted = 0.0;
295 for i in 0..n_reg {
296 fitted += coef_out[i] * x[k * n_reg + i];
297 }
298 let resid = y[k] - fitted;
299 resid_out[k] = resid;
300 rss += weights[k] * resid * resid;
301 let dev = y[k] - y_wmean;
302 wss += weights[k] * dev * dev;
303 }
304 let r_sq = if wss > 0.0 { 1.0 - rss / wss } else { 0.0 };
305 Ok(r_sq)
306}
307
308pub fn iv_2sls_into(
314 x_endogenous: &[f64],
315 z_instruments: &[f64],
316 y: &[f64],
317 n_obs: usize,
318 n_reg: usize,
319 n_instr: usize,
320 coef_out: &mut [f64],
321) -> Result<f64, EconometricsError> {
322 if n_instr < n_reg {
323 return Err(EconometricsError::Underidentified);
324 }
325 if n_obs == 0 || n_obs < n_reg || n_obs > MAX_OBSERVATIONS || n_reg > MAX_REGRESSORS {
326 return Err(EconometricsError::InsufficientData);
327 }
328 if x_endogenous.len() < n_obs * n_reg
329 || z_instruments.len() < n_obs * n_instr
330 || y.len() < n_obs
331 || coef_out.len() < n_reg
332 {
333 return Err(EconometricsError::BufferTooSmall);
334 }
335
336 let mut x_hat = [0.0f64; MAX_OBSERVATIONS * MAX_REGRESSORS];
338 let mut ztz = [0.0f64; MAX_REGRESSORS * MAX_REGRESSORS];
339 let mut ztx_col = [0.0f64; MAX_REGRESSORS];
340 let mut first_stage_coef = [0.0f64; MAX_REGRESSORS];
341
342 for col in 0..n_reg {
343 for i in 0..n_instr {
345 for j in 0..n_instr {
346 let mut acc = 0.0;
347 for k in 0..n_obs {
348 acc += z_instruments[k * n_instr + i] * z_instruments[k * n_instr + j];
349 }
350 ztz[i * n_instr + j] = acc;
351 }
352 }
353 for i in 0..n_instr {
355 let mut acc = 0.0;
356 for k in 0..n_obs {
357 acc += z_instruments[k * n_instr + i] * x_endogenous[k * n_reg + col];
358 }
359 ztx_col[i] = acc;
360 }
361 let mut ztz_copy = ztz;
363 let mut ztx_copy = ztx_col;
364 gaussian_solve(&mut ztz_copy[..n_instr * n_instr], &mut ztx_copy, n_instr)?;
365 for i in 0..n_instr {
366 first_stage_coef[i] = ztx_copy[i];
367 }
368 for k in 0..n_obs {
370 let mut acc = 0.0;
371 for i in 0..n_instr {
372 acc += z_instruments[k * n_instr + i] * first_stage_coef[i];
373 }
374 x_hat[k * n_reg + col] = acc;
375 }
376 }
377
378 let mut resid = [0.0f64; MAX_OBSERVATIONS];
380 ols_into(&x_hat, y, n_obs, n_reg, coef_out, &mut resid)
381}
382
383pub fn logistic_mle_into(
388 x: &[f64],
389 y_binary: &[f64],
390 n_obs: usize,
391 n_reg: usize,
392 max_iter: u32,
393 tolerance: f64,
394 coef_out: &mut [f64],
395) -> Result<EconConvergence, EconometricsError> {
396 use super::error::EconStatus;
397 if n_obs == 0
398 || n_reg == 0
399 || n_obs < n_reg
400 || n_obs > MAX_OBSERVATIONS
401 || n_reg > MAX_REGRESSORS
402 {
403 return Err(EconometricsError::InsufficientData);
404 }
405 if x.len() < n_obs * n_reg || y_binary.len() < n_obs || coef_out.len() < n_reg {
406 return Err(EconometricsError::BufferTooSmall);
407 }
408 for v in y_binary.iter().take(n_obs) {
409 if !v.is_finite() || (*v != 0.0 && *v != 1.0) {
410 return Err(EconometricsError::InvalidInput);
411 }
412 }
413
414 for i in 0..n_reg {
416 coef_out[i] = 0.0;
417 }
418 let mut hessian = [0.0f64; MAX_REGRESSORS * MAX_REGRESSORS];
419 let mut gradient = [0.0f64; MAX_REGRESSORS];
420 let mut score = [0.0f64; MAX_OBSERVATIONS];
421
422 for iter in 0..max_iter {
423 for k in 0..n_obs {
425 let mut eta = 0.0;
426 for i in 0..n_reg {
427 eta += x[k * n_reg + i] * coef_out[i];
428 }
429 let p = 1.0 / (1.0 + (-eta).exp());
430 score[k] = y_binary[k] - p;
431 }
432 for i in 0..n_reg {
434 let mut acc = 0.0;
435 for k in 0..n_obs {
436 acc += x[k * n_reg + i] * score[k];
437 }
438 gradient[i] = acc;
439 }
440 for i in 0..n_reg {
442 for j in 0..n_reg {
443 let mut acc = 0.0;
444 for k in 0..n_obs {
445 let mut eta = 0.0;
446 for r in 0..n_reg {
447 eta += x[k * n_reg + r] * coef_out[r];
448 }
449 let p = 1.0 / (1.0 + (-eta).exp());
450 let w = p * (1.0 - p);
451 acc += x[k * n_reg + i] * w * x[k * n_reg + j];
452 }
453 hessian[i * n_reg + j] = -acc;
454 }
455 }
456 let mut h_copy = hessian;
458 let mut g_copy = gradient;
459 match gaussian_solve(&mut h_copy[..n_reg * n_reg], &mut g_copy, n_reg) {
460 Ok(()) => {
461 let mut delta_norm = 0.0;
462 for i in 0..n_reg {
463 coef_out[i] -= g_copy[i];
468 delta_norm += g_copy[i] * g_copy[i];
469 }
470 let delta_norm = delta_norm.sqrt();
471 if !delta_norm.is_finite() {
472 return Ok(EconConvergence::stalled(
473 EconStatus::NonFinite,
474 iter + 1,
475 delta_norm,
476 ));
477 }
478 if delta_norm < tolerance {
479 return Ok(EconConvergence::converged(iter + 1, delta_norm));
480 }
481 }
482 Err(EconometricsError::SingularSystem) => {
483 return Ok(EconConvergence::stalled(
484 EconStatus::Singular,
485 iter + 1,
486 0.0,
487 ));
488 }
489 Err(e) => return Err(e),
490 }
491 }
492 Ok(EconConvergence::stalled(
493 EconStatus::MaxIterations,
494 max_iter,
495 0.0,
496 ))
497}
498
499pub fn gmm_moment_eval(
506 moment_values: &[f64],
507 n_obs: usize,
508 n_moments: usize,
509 out: &mut [f64],
510) -> Result<usize, EconometricsError> {
511 if n_obs == 0
512 || n_moments == 0
513 || moment_values.len() < n_obs * n_moments
514 || out.len() < n_moments
515 {
516 return Err(EconometricsError::InvalidInput);
517 }
518 for v in moment_values.iter().take(n_obs * n_moments) {
519 if !v.is_finite() {
520 return Err(EconometricsError::NonFinite);
521 }
522 }
523 for j in 0..n_moments {
524 let mut acc = 0.0;
525 for i in 0..n_obs {
526 acc += moment_values[i * n_moments + j];
527 }
528 out[j] = acc / n_obs as f64;
529 }
530 Ok(n_moments)
531}
532
533#[cfg(test)]
534mod tests {
535 use super::*;
536
537 fn approx(a: f64, b: f64) -> bool {
538 (a - b).abs() < 1e-6
539 }
540
541 #[test]
542 fn ols_recovers_exact_linear() {
543 let x = [1.0, 1.0, 1.0, 2.0, 1.0, 3.0, 1.0, 4.0, 1.0, 5.0];
546 let y = [5.0, 8.0, 11.0, 14.0, 17.0];
547 let mut coef = [0.0f64; 2];
548 let mut resid = [0.0f64; 5];
549 let r_sq = ols_into(&x, &y, 5, 2, &mut coef, &mut resid).unwrap();
550 assert!(approx(coef[0], 2.0));
551 assert!(approx(coef[1], 3.0));
552 assert!(approx(r_sq, 1.0));
553 for r in resid.iter() {
554 assert!(approx(*r, 0.0));
555 }
556 }
557
558 #[test]
559 fn ols_insufficient_data() {
560 let x = [1.0, 1.0];
561 let y = [1.0];
562 let mut coef = [0.0f64; 2];
563 let mut resid = [0.0f64];
564 let err = ols_into(&x, &y, 1, 2, &mut coef, &mut resid).unwrap_err();
565 assert_eq!(err, EconometricsError::InsufficientData);
566 }
567
568 #[test]
569 fn ols_singular_system_multicollinearity() {
570 let x = [1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0];
572 let y = [1.0, 2.0, 3.0, 4.0];
573 let mut coef = [0.0f64; 2];
574 let mut resid = [0.0f64; 4];
575 let err = ols_into(&x, &y, 4, 2, &mut coef, &mut resid).unwrap_err();
576 assert_eq!(err, EconometricsError::SingularSystem);
577 }
578
579 #[test]
580 fn wls_recovers_weighted_fit() {
581 let x = [1.0, 1.0, 1.0, 2.0, 1.0, 3.0];
583 let y = [1.0, 2.0, 3.0];
584 let w = [1.0, 1.0, 1.0];
585 let mut coef = [0.0f64; 2];
586 let mut resid = [0.0f64; 3];
587 let r_sq = wls_into(&x, &y, &w, 3, 2, &mut coef, &mut resid).unwrap();
588 assert!(approx(coef[0], 0.0));
589 assert!(approx(coef[1], 1.0));
590 assert!(approx(r_sq, 1.0));
591 }
592
593 #[test]
594 fn iv_2sls_underidentified() {
595 let x = [1.0, 1.0, 1.0, 1.0];
596 let z = [1.0, 1.0]; let y = [1.0, 2.0];
598 let mut coef = [0.0f64; 2];
599 let err = iv_2sls_into(&x, &z, &y, 2, 2, 1, &mut coef).unwrap_err();
600 assert_eq!(err, EconometricsError::Underidentified);
601 }
602
603 #[test]
604 fn logistic_mle_separable() {
605 let x = [
609 1.0, -5.0, 1.0, -3.0, 1.0, -1.0, 1.0, 1.0, 1.0, 3.0, 1.0, 5.0,
610 ];
611 let y = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
612 let mut coef = [0.0f64; 2];
613 let conv = logistic_mle_into(&x, &y, 6, 2, 100, 1e-8, &mut coef).unwrap();
614 assert!(coef[1] > 1.0, "coef[1] = {}", coef[1]);
616 let _ = conv;
617 }
618
619 #[test]
620 fn logistic_mle_rejects_non_binary() {
621 let x = [1.0, 0.0, 1.0, 1.0, 1.0, 2.0, 1.0, 3.0];
626 let y = [0.0, 0.5, 1.0, 1.0];
627 let mut coef = [0.0f64; 2];
628 let err = logistic_mle_into(&x, &y, 4, 2, 100, 1e-8, &mut coef).unwrap_err();
629 assert_eq!(err, EconometricsError::InvalidInput);
630 }
631
632 #[test]
633 fn gmm_moment_eval_averages() {
634 let m = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
636 let mut out = [0.0f64; 2];
637 gmm_moment_eval(&m, 3, 2, &mut out).unwrap();
638 assert!(approx(out[0], 3.0)); assert!(approx(out[1], 4.0)); }
641
642 #[test]
643 fn calibration_record_construction() {
644 let rec = CalibrationRecord::new("ols_v1", 0xDEAD_BEEF, 3, 0.05, 0, 12345);
645 assert_eq!(rec.model_name, "ols_v1");
646 assert_eq!(rec.data_hash, 0xDEAD_BEEF);
647 assert_eq!(rec.n_params, 3);
648 assert!(approx(rec.loss, 0.05));
649 }
650
651 #[test]
652 fn buffer_too_small_errors() {
653 let x = [1.0, 1.0, 1.0, 2.0];
654 let y = [1.0, 2.0];
655 let mut coef = [0.0f64; 1]; let mut resid = [0.0f64; 2];
657 let err = ols_into(&x, &y, 2, 2, &mut coef, &mut resid).unwrap_err();
658 assert_eq!(err, EconometricsError::BufferTooSmall);
659 }
660}