Skip to main content

qualia_core_db/specialized_libs/computational_economics/
econometrics.rs

1//! Econometrics: OLS, WLS, 2SLS, logistic MLE, GMM, and calibration records.
2//!
3//! Allocation class: **HotZeroHeap**. All scratch uses fixed-capacity stack
4//! arrays. No `Vec`/`String`/`Box` in any kernel.
5//!
6//! Assumptions:
7//! - OLS assumes exogeneity (E[ε|X] = 0), iid errors, no perfect
8//!   multicollinearity. Standard errors are not yet computed (future work).
9//! - WLS assumes known weights proportional to inverse error variance.
10//! - 2SLS assumes instrument relevance (n_instr >= n_reg) and exogeneity.
11//!   Underidentified models (n_instr < n_reg) are refused.
12//! - Logistic MLE assumes iid Bernoulli outcomes with logit link; uses
13//!   Newton-Raphson (IRLS).
14
15use super::error::EconConvergence;
16
17/// Maximum regressors (including constant) in a bounded regression.
18pub const MAX_REGRESSORS: usize = 16;
19/// Maximum observations in a bounded regression.
20pub 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/// A `repr(C)` calibration record linking a fitted model to its data and
34/// diagnostics.
35#[derive(Debug, Clone, Copy)]
36#[repr(C)]
37pub struct CalibrationRecord {
38    /// Static model name (e.g. "ols_v1", "logistic_irls").
39    pub model_name: &'static str,
40    /// FNV-1a or caller-supplied hash of the calibration dataset.
41    pub data_hash: u64,
42    /// Number of fitted parameters.
43    pub n_params: u32,
44    /// Final loss (RSS for OLS, negative log-likelihood for MLE).
45    pub loss: f64,
46    /// Iterations executed (0 for closed-form OLS).
47    pub iterations: u32,
48    /// Caller-supplied epoch seconds for provenance.
49    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
72/// Solve a linear system `A * x = b` in place using Gaussian elimination with
73/// partial pivoting. `a` is `n x n` row-major (destroyed), `b` is length `n`
74/// (overwritten with solution). Returns `SingularSystem` if a zero pivot is
75/// encountered.
76fn gaussian_solve(a: &mut [f64], b: &mut [f64], n: usize) -> Result<(), EconometricsError> {
77    for col in 0..n {
78        // Find pivot.
79        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        // Eliminate below.
102        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    // Back-substitution.
116    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
129/// Ordinary Least Squares via normal equations `X'X b = X'y`.
130///
131/// `x` is `n_obs x n_reg` row-major. `y` is length `n_obs`. Writes
132/// coefficients into `coef_out[..n_reg]` and residuals into
133/// `resid_out[..n_obs]`. Returns R-squared.
134pub 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    // X'X
171    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    // X'y
181    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    // Residuals and R-squared.
196    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
220/// Weighted Least Squares: `(X'WX) b = X'Wy`.
221///
222/// `weights` is length `n_obs`. Writes coefficients and residuals.
223pub 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
308/// Two-Stage Least Squares (2SLS).
309///
310/// First stage: regress endogenous `X` on instruments `Z`, get `X-hat`.
311/// Second stage: OLS of `y` on `X-hat`. Refuses if `n_instr < n_reg`
312/// (underidentified).
313pub 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    // First stage: for each regressor, regress on instruments → x_hat.
337    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        // Z'Z
344        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        // Z'x_col
354        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        // Solve Z'Z * gamma = Z'x_col
362        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        // x_hat[k, col] = Z[k] · gamma
369        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    // Second stage: OLS of y on x_hat.
379    let mut resid = [0.0f64; MAX_OBSERVATIONS];
380    ols_into(&x_hat, y, n_obs, n_reg, coef_out, &mut resid)
381}
382
383/// Logistic regression via Newton-Raphson (IRLS).
384///
385/// `x` is `n_obs x n_reg` row-major. `y_binary` in {0, 1}. Writes
386/// coefficients into `coef_out[..n_reg]`. Returns convergence report.
387pub 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    // Initialize coef = 0.
415    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        // Compute p_k = sigmoid(X_k · coef) and score_k = y_k - p_k.
424        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        // Gradient = X' (y - p)
433        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        // Hessian = -X' W X where W = diag(p(1-p))
441        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        // Solve Hessian * delta = gradient (Newton step).
457        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                    // Newton-Raphson maximizing the log-likelihood: β_new = β − H⁻¹∇L.
464                    // `g_copy` is H⁻¹∇L (H = ∇²L = −X'WX, negative definite), so the
465                    // ascent step subtracts it. (Adding it, as before, descended away
466                    // from the MLE — coefficients moved the wrong direction.)
467                    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
499/// Evaluate GMM moment conditions: `g(theta) = (1/n) sum m_i(theta)`.
500///
501/// `moments` is a caller-supplied closure-free precomputed table:
502/// `moments[i * n_moments + j]` = m_j(observation_i, params). Actually this
503/// function computes the sample average of the supplied moment values.
504/// `out` receives `n_moments` averaged moment conditions.
505pub 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        // y = 2 + 3x, x = [1, 2, 3, 4, 5]
544        // Design matrix with constant: [[1, 1], [1, 2], [1, 3], [1, 4], [1, 5]]
545        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        // Two identical columns → singular X'X
571        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        // Simple: y = x, weights all 1 → same as OLS
582        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]; // 1 instrument for 2 regressors
597        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        // Perfectly separable: y=1 when x>0, y=0 when x<0
606        // Design: [[1, -5], [1, -3], [1, -1], [1, 1], [1, 3], [1, 5]]
607        // y = [0, 0, 0, 1, 1, 1]
608        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        // For separable data, coef[1] should be large positive.
615        assert!(coef[1] > 1.0, "coef[1] = {}", coef[1]);
616        let _ = conv;
617    }
618
619    #[test]
620    fn logistic_mle_rejects_non_binary() {
621        // 4 observations × 2 regressors → the design needs 8 values. (The prior
622        // test passed only 4, so it tripped the BufferTooSmall guard before ever
623        // reaching the binary-y check it meant to exercise.) With a correctly
624        // sized design, the non-binary y=0.5 is what triggers InvalidInput.
625        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        // 3 observations, 2 moments: m = [[1, 2], [3, 4], [5, 6]]
635        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)); // (1+3+5)/3
639        assert!(approx(out[1], 4.0)); // (2+4+6)/3
640    }
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]; // too small
656        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}