package numerics // VecAdd computes y + k*dy on the domain state type S. // Any coordinate-wrap or other domain-specific operation lives here. type VecAdd[S any] func(y S, k float64, dy S) S // VecLerp computes (1-l)*a + l*b on the domain state type S. type VecLerp[S any] func(a, b S, l float64) S // Deriv computes the time derivative of state. type Deriv[S any] func(t float64, y S) S // Trigger reports whether a termination condition holds at (t, y). type Trigger[S any] func(t float64, y S) bool // RK4Step performs one classical Runge-Kutta-4 step from (t, y) with step dt. // dt may be negative to integrate backwards in time. func RK4Step[S any](t float64, y S, dt float64, deriv Deriv[S], add VecAdd[S]) S { k1 := deriv(t, y) k2 := deriv(t+dt/2, add(y, dt/2, k1)) k3 := deriv(t+dt/2, add(y, dt/2, k2)) k4 := deriv(t+dt, add(y, dt, k3)) y2 := y y2 = add(y2, dt/6, k1) y2 = add(y2, dt/3, k2) y2 = add(y2, dt/3, k3) y2 = add(y2, dt/6, k4) return y2 } // RefineTrigger locates the trigger point between (t1, y1) (trigger not fired) // and (t2, y2) (trigger fired) via binary search in the linear-interpolation // parameter space, stopping when the parameter interval is narrower than tol. // // Returns the final midpoint sampled, matching the behavior of Tawhiri's // solver.pyx (the returned point is *not* guaranteed to satisfy the trigger; // for tol << 1 the difference is at most one tolerance-width either side). func RefineTrigger[S any]( t1 float64, y1 S, t2 float64, y2 S, trigger Trigger[S], lerp VecLerp[S], tol float64, ) (float64, S) { left, right := 0.0, 1.0 t3 := t2 y3 := y2 for right-left > tol { mid := (left + right) / 2 t3 = Lerp(t1, t2, mid) y3 = lerp(y1, y2, mid) if trigger(t3, y3) { right = mid } else { left = mid } } return t3, y3 }