61 lines
1.8 KiB
Go
61 lines
1.8 KiB
Go
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
|
|
}
|