forked from gsn/predictor
180 lines
4.4 KiB
Go
180 lines
4.4 KiB
Go
package prediction
|
|
|
|
import "math"
|
|
|
|
// Exact port of the reference RK4 solver (solver.pyx).
|
|
// Integrates balloon state using RK4 with dt=60 seconds.
|
|
// Termination uses binary search refinement (tolerance 0.01).
|
|
|
|
// Vec holds the balloon state: latitude, longitude, altitude.
|
|
type Vec struct {
|
|
Lat float64
|
|
Lng float64
|
|
Alt float64
|
|
}
|
|
|
|
// Model is a function that returns (dlat/dt, dlng/dt, dalt/dt) given state.
|
|
// t is UNIX timestamp, lat/lng in degrees, alt in metres.
|
|
type Model func(t float64, lat, lng, alt float64) (dlat, dlng, dalt float64)
|
|
|
|
// Terminator returns true when integration should stop.
|
|
type Terminator func(t float64, lat, lng, alt float64) bool
|
|
|
|
// StageResult holds the trajectory points for one flight stage.
|
|
type StageResult struct {
|
|
Points []TrajectoryPoint
|
|
}
|
|
|
|
// TrajectoryPoint is a single point in a trajectory (used by solver).
|
|
type TrajectoryPoint struct {
|
|
T float64 // UNIX timestamp
|
|
Lat float64
|
|
Lng float64
|
|
Alt float64
|
|
}
|
|
|
|
// pymod returns a % b with Python semantics (always non-negative when b > 0).
|
|
func pymod(a, b float64) float64 {
|
|
r := math.Mod(a, b)
|
|
if r < 0 {
|
|
r += b
|
|
}
|
|
return r
|
|
}
|
|
|
|
// vecadd returns a + k*b, with lng wrapped to [0, 360).
|
|
func vecadd(a Vec, k float64, b Vec) Vec {
|
|
return Vec{
|
|
Lat: a.Lat + k*b.Lat,
|
|
Lng: pymod(a.Lng+k*b.Lng, 360.0),
|
|
Alt: a.Alt + k*b.Alt,
|
|
}
|
|
}
|
|
|
|
// scalarLerp returns (1-l)*a + l*b.
|
|
func scalarLerp(a, b, l float64) float64 {
|
|
return (1-l)*a + l*b
|
|
}
|
|
|
|
// lngLerp interpolates longitude handling the 0/360 wrap-around.
|
|
func lngLerp(a, b, l float64) float64 {
|
|
l2 := 1 - l
|
|
|
|
if a > b {
|
|
a, b = b, a
|
|
l, l2 = l2, l
|
|
}
|
|
|
|
// distance round one way: b - a
|
|
// distance around other: (a + 360) - b
|
|
if b-a < 180.0 {
|
|
return l2*a + l*b
|
|
}
|
|
return pymod(l2*(a+360)+l*b, 360.0)
|
|
}
|
|
|
|
// vecLerp returns (1-l)*a + l*b with proper longitude wrapping.
|
|
func vecLerp(a, b Vec, l float64) Vec {
|
|
return Vec{
|
|
Lat: scalarLerp(a.Lat, b.Lat, l),
|
|
Lng: lngLerp(a.Lng, b.Lng, l),
|
|
Alt: scalarLerp(a.Alt, b.Alt, l),
|
|
}
|
|
}
|
|
|
|
// rk4 integrates from initial conditions using RK4.
|
|
// dt=60.0 seconds, terminationTolerance=0.01.
|
|
func rk4(t float64, lat, lng, alt float64, model Model, terminator Terminator) []TrajectoryPoint {
|
|
const dt = 60.0
|
|
const terminationTolerance = 0.01
|
|
|
|
y := Vec{Lat: lat, Lng: lng, Alt: alt}
|
|
result := []TrajectoryPoint{{T: t, Lat: y.Lat, Lng: y.Lng, Alt: y.Alt}}
|
|
|
|
for {
|
|
// Evaluate model at 4 points (standard RK4)
|
|
k1lat, k1lng, k1alt := model(t, y.Lat, y.Lng, y.Alt)
|
|
k1 := Vec{Lat: k1lat, Lng: k1lng, Alt: k1alt}
|
|
|
|
mid1 := vecadd(y, dt/2, k1)
|
|
k2lat, k2lng, k2alt := model(t+dt/2, mid1.Lat, mid1.Lng, mid1.Alt)
|
|
k2 := Vec{Lat: k2lat, Lng: k2lng, Alt: k2alt}
|
|
|
|
mid2 := vecadd(y, dt/2, k2)
|
|
k3lat, k3lng, k3alt := model(t+dt/2, mid2.Lat, mid2.Lng, mid2.Alt)
|
|
k3 := Vec{Lat: k3lat, Lng: k3lng, Alt: k3alt}
|
|
|
|
end := vecadd(y, dt, k3)
|
|
k4lat, k4lng, k4alt := model(t+dt, end.Lat, end.Lng, end.Alt)
|
|
k4 := Vec{Lat: k4lat, Lng: k4lng, Alt: k4alt}
|
|
|
|
// y2 = y + dt/6*k1 + dt/3*k2 + dt/3*k3 + dt/6*k4
|
|
y2 := y
|
|
y2 = vecadd(y2, dt/6, k1)
|
|
y2 = vecadd(y2, dt/3, k2)
|
|
y2 = vecadd(y2, dt/3, k3)
|
|
y2 = vecadd(y2, dt/6, k4)
|
|
|
|
t2 := t + dt
|
|
|
|
if terminator(t2, y2.Lat, y2.Lng, y2.Alt) {
|
|
// Binary search to refine the termination point.
|
|
// Find l in [0, 1] such that (t3, y3) = lerp((t, y), (t2, y2), l)
|
|
// is near where the terminator fires.
|
|
left := 0.0
|
|
right := 1.0
|
|
|
|
var t3 float64
|
|
var y3 Vec
|
|
t3 = t2
|
|
y3 = y2
|
|
|
|
for right-left > terminationTolerance {
|
|
mid := (left + right) / 2
|
|
t3 = scalarLerp(t, t2, mid)
|
|
y3 = vecLerp(y, y2, mid)
|
|
|
|
if terminator(t3, y3.Lat, y3.Lng, y3.Alt) {
|
|
right = mid
|
|
} else {
|
|
left = mid
|
|
}
|
|
}
|
|
|
|
result = append(result, TrajectoryPoint{T: t3, Lat: y3.Lat, Lng: y3.Lng, Alt: y3.Alt})
|
|
break
|
|
}
|
|
|
|
// Update current state
|
|
t = t2
|
|
y = y2
|
|
result = append(result, TrajectoryPoint{T: t, Lat: y.Lat, Lng: y.Lng, Alt: y.Alt})
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Solve runs through a chain of (model, terminator) stages.
|
|
// Returns one StageResult per stage.
|
|
func Solve(t, lat, lng, alt float64, chain []struct {
|
|
Model Model
|
|
Terminator Terminator
|
|
}) []StageResult {
|
|
var results []StageResult
|
|
|
|
for _, stage := range chain {
|
|
points := rk4(t, lat, lng, alt, stage.Model, stage.Terminator)
|
|
results = append(results, StageResult{Points: points})
|
|
|
|
// Next stage starts where this one ended
|
|
if len(points) > 0 {
|
|
last := points[len(points)-1]
|
|
t = last.T
|
|
lat = last.Lat
|
|
lng = last.Lng
|
|
alt = last.Alt
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|