package numerics import ( "math" "testing" ) // scalarAdd / scalarLerp let us drive RK4 on a plain float64. func scalarAdd(y float64, k float64, dy float64) float64 { return y + k*dy } func scalarLerpF(a, b float64, l float64) float64 { return Lerp(a, b, l) } func TestRK4ExponentialDecay(t *testing.T) { // dy/dt = -y → exact: y(t) = y0 * exp(-t). deriv := func(_ float64, y float64) float64 { return -y } y := 1.0 tnow := 0.0 dt := 0.01 for range 100 { y = RK4Step(tnow, y, dt, deriv, scalarAdd) tnow += dt } want := math.Exp(-1.0) if math.Abs(y-want) > 1e-8 { t.Errorf("RK4 exp decay at t=1: got %v, want %v (diff %v)", y, want, y-want) } } func TestRK4ReverseTime(t *testing.T) { // dy/dt = y → exact: y(t) = y0 * exp(t). // Integrating from t=1 backwards with dt=-0.01 over 100 steps should give y0. deriv := func(_ float64, y float64) float64 { return y } y := math.E tnow := 1.0 dt := -0.01 for range 100 { y = RK4Step(tnow, y, dt, deriv, scalarAdd) tnow += dt } if math.Abs(y-1.0) > 1e-8 { t.Errorf("RK4 reverse: got %v, want 1.0 (diff %v)", y, y-1.0) } } func TestRefineTrigger(t *testing.T) { // y crosses 0 at l=0.4 between y1=1 and y2=-1.5. y1, y2 := 1.0, -1.5 t1, t2 := 0.0, 1.0 trig := func(_ float64, y float64) bool { return y <= 0 } tr, yr := RefineTrigger(t1, y1, t2, y2, trig, scalarLerpF, 0.001) // The exact crossing is at l = 1/(1+1.5) = 0.4 → t = 0.4, y = 0. if math.Abs(tr-0.4) > 0.01 { t.Errorf("Refined t = %v, want ~0.4", tr) } if math.Abs(yr) > 0.01 { t.Errorf("Refined y = %v, want ~0", yr) } }