Files
enkly-ai/pkg/mathkit/mathkit_test.go
enkly 79ce8dc4c0
Some checks failed
Go CI Pipeline / build_and_test (push) Failing after 22m0s
Init Project
2026-10-30 18:06:55 +01:00

365 lines
8.7 KiB
Go

package mathkit
import (
"math"
"strings"
"testing"
)
func almostEqual(a, b, tol float64) bool { return math.Abs(a-b) <= tol }
func mustExpr(t *testing.T, src string) *Expr {
t.Helper()
e, err := ParseExpr(src)
if err != nil {
t.Fatalf("ParseExpr(%q): %v", src, err)
}
return e
}
func TestExprEval(t *testing.T) {
cases := []struct {
src string
vars map[string]float64
want float64
}{
{"2 + 3 * 4", nil, 14},
{"(2 + 3) * 4", nil, 20},
{"2 ^ 3 ^ 2", nil, 512}, // right-associative: 2^(3^2) = 2^9
{"-2 ^ 2", nil, -4}, // unary minus binds looser than ^: -(2^2)
{"x * x + 2*x + 1", map[string]float64{"x": 3}, 16},
{"sigmoid(0)", nil, 0.5},
{"relu(-5)", nil, 0},
{"relu(5)", nil, 5},
{"max(3, 7)", nil, 7},
{"sqrt(16)", nil, 4},
{"pi", nil, math.Pi},
}
for _, c := range cases {
e := mustExpr(t, c.src)
got, err := e.Eval(c.vars)
if err != nil {
t.Fatalf("Eval(%q): %v", c.src, err)
}
if !almostEqual(got, c.want, 1e-9) {
t.Errorf("Eval(%q) = %v, want %v", c.src, got, c.want)
}
}
}
func TestExprUnboundVariable(t *testing.T) {
e := mustExpr(t, "x + 1")
if _, err := e.Eval(nil); err == nil {
t.Fatal("expected an error for an unbound variable, got nil")
}
}
func TestMatMul(t *testing.T) {
a := Matrix{{1, 2}, {3, 4}}
b := Matrix{{5, 6}, {7, 8}}
got, err := MatMul(a, b)
if err != nil {
t.Fatal(err)
}
want := Matrix{{19, 22}, {43, 50}}
for i := range want {
for j := range want[i] {
if got[i][j] != want[i][j] {
t.Errorf("MatMul[%d][%d] = %v, want %v", i, j, got[i][j], want[i][j])
}
}
}
}
func TestMatMulShapeMismatch(t *testing.T) {
a := Matrix{{1, 2, 3}}
b := Matrix{{1, 2}}
if _, err := MatMul(a, b); err == nil {
t.Fatal("expected a shape-mismatch error")
}
}
func TestDeterminantAndInverse(t *testing.T) {
a := Matrix{{4, 7}, {2, 6}}
det, err := Determinant(a)
if err != nil {
t.Fatal(err)
}
if !almostEqual(det, 10, 1e-9) {
t.Errorf("det = %v, want 10", det)
}
inv, err := Inverse(a)
if err != nil {
t.Fatal(err)
}
// A * A^-1 should be the identity.
prod, err := MatMul(a, inv)
if err != nil {
t.Fatal(err)
}
for i := 0; i < 2; i++ {
for j := 0; j < 2; j++ {
want := 0.0
if i == j {
want = 1
}
if !almostEqual(prod[i][j], want, 1e-9) {
t.Errorf("A*A^-1[%d][%d] = %v, want %v", i, j, prod[i][j], want)
}
}
}
}
func TestInverseSingular(t *testing.T) {
a := Matrix{{1, 2}, {2, 4}} // row 2 = 2 * row 1
if _, err := Inverse(a); err == nil {
t.Fatal("expected a singular-matrix error")
}
}
func TestDominantEigen(t *testing.T) {
// [[2,0],[0,1]] has eigenvalues 2 and 1 — dominant is 2, eigenvector (1,0).
a := Matrix{{2, 0}, {0, 1}}
lambda, v, err := DominantEigen(a, 0)
if err != nil {
t.Fatal(err)
}
if !almostEqual(lambda, 2, 1e-6) {
t.Errorf("eigenvalue = %v, want 2", lambda)
}
if !almostEqual(math.Abs(v[0]), 1, 1e-6) || !almostEqual(v[1], 0, 1e-6) {
t.Errorf("eigenvector = %v, want ~(1,0)", v)
}
}
func TestDerivative(t *testing.T) {
// d/dx[x^2] at x=3 is 6.
e := mustExpr(t, "x^2")
d, err := Derivative(e, "x", map[string]float64{"x": 3}, 0)
if err != nil {
t.Fatal(err)
}
if !almostEqual(d, 6, 1e-4) {
t.Errorf("derivative = %v, want 6", d)
}
}
func TestGradient(t *testing.T) {
// f(x,y) = x^2 + y^2 ; gradient at (3,4) is (6,8).
e := mustExpr(t, "x^2 + y^2")
g, err := Gradient(e, []string{"x", "y"}, map[string]float64{"x": 3, "y": 4}, 0)
if err != nil {
t.Fatal(err)
}
if !almostEqual(g[0], 6, 1e-4) || !almostEqual(g[1], 8, 1e-4) {
t.Errorf("gradient = %v, want [6 8]", g)
}
}
func TestIntegrate(t *testing.T) {
// ∫0..1 x^2 dx = 1/3.
e := mustExpr(t, "x^2")
v, err := Integrate(e, "x", 0, 1, 100, nil)
if err != nil {
t.Fatal(err)
}
if !almostEqual(v, 1.0/3.0, 1e-9) {
t.Errorf("integral = %v, want 1/3", v)
}
}
func TestGradientDescentConvergesToMinimum(t *testing.T) {
// f(x) = (x-3)^2 has its minimum at x=3.
e := mustExpr(t, "(x - 3)^2")
res, err := GradientDescent(e, []string{"x"}, []float64{0}, 0.1, 200, 1e-10)
if err != nil {
t.Fatal(err)
}
final := res.Path[len(res.Path)-1][0]
if !almostEqual(final, 3, 1e-3) {
t.Errorf("gradient descent converged to x=%v, want ~3", final)
}
}
func TestNewtonRootFindsSquareRoot(t *testing.T) {
// Root of x^2 - 2 = 0 is sqrt(2).
e := mustExpr(t, "x^2 - 2")
res, err := NewtonRoot(e, "x", 1, 0, 0, nil)
if err != nil {
t.Fatal(err)
}
root := res.Path[len(res.Path)-1]
if !almostEqual(root, math.Sqrt2, 1e-6) {
t.Errorf("newton root = %v, want %v", root, math.Sqrt2)
}
}
func TestStatistics(t *testing.T) {
data := []float64{2, 4, 4, 4, 5, 5, 7, 9}
mean, _ := Mean(data)
if !almostEqual(mean, 5, 1e-9) {
t.Errorf("mean = %v, want 5", mean)
}
popVar, _ := Variance(data, true)
if !almostEqual(popVar, 4, 1e-9) {
t.Errorf("population variance = %v, want 4", popVar)
}
std, _ := StdDev(data, true)
if !almostEqual(std, 2, 1e-9) {
t.Errorf("population std_dev = %v, want 2", std)
}
}
func TestCorrelationPerfect(t *testing.T) {
x := []float64{1, 2, 3, 4, 5}
y := []float64{2, 4, 6, 8, 10}
c, err := Correlation(x, y)
if err != nil {
t.Fatal(err)
}
if !almostEqual(c, 1, 1e-9) {
t.Errorf("correlation = %v, want 1", c)
}
}
func TestNormalDistribution(t *testing.T) {
// Standard normal at x=0: pdf = 1/sqrt(2*pi), cdf = 0.5.
pdf, _ := NormalPDF(0, 0, 1)
if !almostEqual(pdf, 1/math.Sqrt(2*math.Pi), 1e-9) {
t.Errorf("pdf(0) = %v", pdf)
}
cdf, _ := NormalCDF(0, 0, 1)
if !almostEqual(cdf, 0.5, 1e-9) {
t.Errorf("cdf(0) = %v, want 0.5", cdf)
}
}
func TestSigmoidNumericallyStable(t *testing.T) {
if v := Sigmoid(-1000); v != 0 {
t.Errorf("sigmoid(-1000) = %v, want 0", v)
}
if v := Sigmoid(1000); !almostEqual(v, 1, 1e-12) {
t.Errorf("sigmoid(1000) = %v, want ~1", v)
}
if v := Sigmoid(0); v != 0.5 {
t.Errorf("sigmoid(0) = %v, want 0.5", v)
}
}
func TestSoftmaxSumsToOne(t *testing.T) {
out := Softmax([]float64{1, 2, 3})
var sum float64
for _, v := range out {
sum += v
}
if !almostEqual(sum, 1, 1e-9) {
t.Errorf("softmax sums to %v, want 1", sum)
}
// Largest logit should get the largest probability.
if out[2] <= out[1] || out[1] <= out[0] {
t.Errorf("softmax ordering wrong: %v", out)
}
}
func TestMSELoss(t *testing.T) {
v, err := MSELoss([]float64{1, 2, 3}, []float64{1, 2, 4})
if err != nil {
t.Fatal(err)
}
// errors: 0, 0, -1 -> squared: 0,0,1 -> mean = 1/3
if !almostEqual(v, 1.0/3.0, 1e-9) {
t.Errorf("mse = %v, want 1/3", v)
}
}
func TestLinearRegressionPerfectFit(t *testing.T) {
// y = 2 + 3x, exactly.
X := Matrix{{1}, {2}, {3}, {4}, {5}}
y := []float64{5, 8, 11, 14, 17}
res, err := LinearRegression(X, y)
if err != nil {
t.Fatal(err)
}
if !almostEqual(res.Coefficients[0], 2, 1e-6) {
t.Errorf("intercept = %v, want 2", res.Coefficients[0])
}
if !almostEqual(res.Coefficients[1], 3, 1e-6) {
t.Errorf("slope = %v, want 3", res.Coefficients[1])
}
if !almostEqual(res.RSquared, 1, 1e-6) {
t.Errorf("R^2 = %v, want 1", res.RSquared)
}
}
// --- Solve dispatcher: the JSON-boundary layer the agent tool calls ---
func TestSolveEvaluate(t *testing.T) {
out, err := Solve("evaluate", map[string]any{
"expression": "x^2 + y^2",
"variables": map[string]any{"x": 3.0, "y": 4.0},
})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, "25") {
t.Errorf("Solve(evaluate) = %q, want it to contain 25", out)
}
}
func TestSolveMatrixMul(t *testing.T) {
out, err := Solve("matrix_mul", map[string]any{
"a": []any{[]any{1.0, 2.0}, []any{3.0, 4.0}},
"b": []any{[]any{5.0, 6.0}, []any{7.0, 8.0}},
})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, "19") || !strings.Contains(out, "50") {
t.Errorf("Solve(matrix_mul) = %q, missing expected entries", out)
}
}
func TestSolveUnknownOperation(t *testing.T) {
if _, err := Solve("not_a_real_operation", nil); err == nil {
t.Fatal("expected an error for an unknown operation")
}
}
func TestSolveGradientDescent(t *testing.T) {
out, err := Solve("gradient_descent", map[string]any{
"expression": "(x - 3)^2",
"variables": []any{"x"},
"start": []any{0.0},
"learning_rate": 0.1,
"iterations": 200.0,
})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, "converged=true") {
t.Errorf("Solve(gradient_descent) = %q, expected convergence", out)
}
}
func TestSolveMissingField(t *testing.T) {
_, err := Solve("mean", map[string]any{})
if err == nil {
t.Fatal("expected a missing-field error")
}
if !strings.Contains(err.Error(), "data") {
t.Errorf("error %q should mention the missing field", err.Error())
}
}
func TestOperationsListIsSorted(t *testing.T) {
ops := Operations()
if len(ops) == 0 {
t.Fatal("Operations() returned nothing")
}
for i := 1; i < len(ops); i++ {
if ops[i-1] > ops[i] {
t.Fatalf("Operations() not sorted: %q before %q", ops[i-1], ops[i])
}
}
}