>. Vectors are array. * Sized for the v1 ridge model (435 × 8); Gauss–Jordan with partial * pivoting is plenty for inverting the 8 × 8 normal-equation matrix. */ final class LinearAlgebra { /** * Transpose. m is rows × cols → result is cols × rows. * * @param array> $m * @return array> */ public static function transpose(array $m): array { $rows = count($m); if ($rows === 0) { return []; } $cols = count($m[0]); $out = array_fill(0, $cols, array_fill(0, $rows, 0.0)); for ($i = 0; $i < $rows; $i++) { for ($j = 0; $j < $cols; $j++) { $out[$j][$i] = $m[$i][$j]; } } return $out; } /** * Matrix multiply. a (r×k) * b (k×c) → r×c. * * @param array> $a * @param array> $b * @return array> */ public static function multiply(array $a, array $b): array { $r = count($a); $k = count($a[0] ?? []); $c = count($b[0] ?? []); if (count($b) !== $k) { throw new InvalidArgumentException('Matrix multiply dimension mismatch'); } $out = array_fill(0, $r, array_fill(0, $c, 0.0)); for ($i = 0; $i < $r; $i++) { for ($j = 0; $j < $c; $j++) { $sum = 0.0; for ($p = 0; $p < $k; $p++) { $sum += $a[$i][$p] * $b[$p][$j]; } $out[$i][$j] = $sum; } } return $out; } /** * Matrix × vector. a (r×k) * v (k) → r-vector. * * @param array> $a * @param array $v * @return array */ public static function multiplyVector(array $a, array $v): array { $r = count($a); $k = count($v); if (count($a[0] ?? []) !== $k) { throw new InvalidArgumentException('Matrix × vector dimension mismatch'); } $out = array_fill(0, $r, 0.0); for ($i = 0; $i < $r; $i++) { $sum = 0.0; for ($p = 0; $p < $k; $p++) { $sum += $a[$i][$p] * $v[$p]; } $out[$i] = $sum; } return $out; } /** * Identity matrix of size n. * * @return array> */ public static function identity(int $n): array { $out = array_fill(0, $n, array_fill(0, $n, 0.0)); for ($i = 0; $i < $n; $i++) { $out[$i][$i] = 1.0; } return $out; } /** * Solve A x = b using Gauss–Jordan elimination with partial pivoting. * A is square n×n. Returns x as an n-vector. * * @param array> $A * @param array $b * @return array */ public static function solve(array $A, array $b): array { $n = count($A); if (count($b) !== $n) { throw new InvalidArgumentException('solve: RHS dimension mismatch'); } // Build augmented matrix. $aug = []; for ($i = 0; $i < $n; $i++) { $aug[$i] = array_merge($A[$i], [$b[$i]]); } for ($col = 0; $col < $n; $col++) { // Partial pivot: find row with largest |value| in this column. $pivot = $col; $best = abs($aug[$col][$col]); for ($r = $col + 1; $r < $n; $r++) { $v = abs($aug[$r][$col]); if ($v > $best) { $best = $v; $pivot = $r; } } if ($best < 1e-12) { throw new RuntimeException('solve: matrix is singular or near-singular'); } if ($pivot !== $col) { [$aug[$col], $aug[$pivot]] = [$aug[$pivot], $aug[$col]]; } // Normalise pivot row. $div = $aug[$col][$col]; for ($j = 0; $j <= $n; $j++) { $aug[$col][$j] /= $div; } // Eliminate this column from every other row. for ($r = 0; $r < $n; $r++) { if ($r === $col) { continue; } $factor = $aug[$r][$col]; if ($factor === 0.0) { continue; } for ($j = 0; $j <= $n; $j++) { $aug[$r][$j] -= $factor * $aug[$col][$j]; } } } $x = array_fill(0, $n, 0.0); for ($i = 0; $i < $n; $i++) { $x[$i] = $aug[$i][$n]; } return $x; } /** * Ridge solve: β = (XᵀX + λI) ⁻¹ Xᵀy. * * λ is applied to all coefficients. Caller should standardise X and * centre y before calling, then add intercept back externally — the * intercept must NOT be regularised. * * @param array> $X * @param array $y * @return array */ public static function ridgeSolve(array $X, array $y, float $lambda): array { $Xt = self::transpose($X); $XtX = self::multiply($Xt, $X); $n = count($XtX); for ($i = 0; $i < $n; $i++) { $XtX[$i][$i] += $lambda; } $Xty = self::multiplyVector($Xt, $y); return self::solve($XtX, $Xty); } }