124 lines
3.9 KiB
Go
124 lines
3.9 KiB
Go
package engine
|
|
|
|
import (
|
|
"math"
|
|
"sort"
|
|
|
|
"predictor-refactored/internal/weather"
|
|
)
|
|
|
|
// Sum composes models by summing their derivatives at each evaluation point.
|
|
//
|
|
// Useful for combining a vertical-rate model with a horizontal wind model
|
|
// into a single propagator. Equivalent to Tawhiri's LinearModel.
|
|
func Sum(models ...Model) Model {
|
|
if len(models) == 1 {
|
|
return models[0]
|
|
}
|
|
return func(t float64, s State) State {
|
|
var sum State
|
|
for _, m := range models {
|
|
d := m(t, s)
|
|
sum.Lat += d.Lat
|
|
sum.Lng += d.Lng
|
|
sum.Altitude += d.Altitude
|
|
}
|
|
return sum
|
|
}
|
|
}
|
|
|
|
// ConstantRate returns a model with a constant vertical velocity (m/s).
|
|
// Positive rates are upward.
|
|
func ConstantRate(rate float64) Model {
|
|
return func(_ float64, _ State) State { return State{Altitude: rate} }
|
|
}
|
|
|
|
// ParachuteDescent returns a model where vertical velocity grows with
|
|
// altitude because thinner air provides less drag. seaLevelRate is the
|
|
// descent speed at sea level (m/s, positive).
|
|
//
|
|
// Terminal velocity at altitude is computed as
|
|
//
|
|
// v = -k / sqrt(rho(alt)), k = seaLevelRate * 1.1045,
|
|
//
|
|
// using the NASA atmosphere model for rho. Equivalent to Tawhiri's drag_descent.
|
|
func ParachuteDescent(seaLevelRate float64) Model {
|
|
k := seaLevelRate * 1.1045
|
|
return func(_ float64, s State) State {
|
|
return State{Altitude: -k / math.Sqrt(nasaDensity(s.Altitude))}
|
|
}
|
|
}
|
|
|
|
// nasaDensity returns air density (kg/m^3) for an altitude in metres,
|
|
// using the NASA simple atmosphere model.
|
|
// See https://www.grc.nasa.gov/WWW/K-12/airplane/atmosmet.html.
|
|
func nasaDensity(alt float64) float64 {
|
|
var temp, pressure float64
|
|
switch {
|
|
case alt > 25000:
|
|
temp = -131.21 + 0.00299*alt
|
|
pressure = 2.488 * math.Pow((temp+273.1)/216.6, -11.388)
|
|
case alt > 11000:
|
|
temp = -56.46
|
|
pressure = 22.65 * math.Exp(1.73-0.000157*alt)
|
|
default:
|
|
temp = 15.04 - 0.00649*alt
|
|
pressure = 101.29 * math.Pow((temp+273.1)/288.08, 5.256)
|
|
}
|
|
return pressure / (0.2869 * (temp + 273.1))
|
|
}
|
|
|
|
// RateSegment is one entry in a Piecewise rate schedule. Until is the UNIX
|
|
// timestamp at which this segment ends — the model emits the segment's
|
|
// Rate for all t < Until. The final segment's Rate is held indefinitely.
|
|
type RateSegment struct {
|
|
Until float64
|
|
Rate float64
|
|
}
|
|
|
|
// Piecewise returns a model that produces a piecewise-constant vertical
|
|
// rate over a sequence of intervals. The input is sorted ascending by
|
|
// Until on construction; later segments shadow earlier ones.
|
|
func Piecewise(segments []RateSegment) Model {
|
|
if len(segments) == 0 {
|
|
return ConstantRate(0)
|
|
}
|
|
sorted := append([]RateSegment(nil), segments...)
|
|
sort.Slice(sorted, func(i, j int) bool { return sorted[i].Until < sorted[j].Until })
|
|
finalRate := sorted[len(sorted)-1].Rate
|
|
|
|
return func(t float64, _ State) State {
|
|
idx := sort.Search(len(sorted), func(i int) bool { return sorted[i].Until > t })
|
|
if idx == len(sorted) {
|
|
return State{Altitude: finalRate}
|
|
}
|
|
return State{Altitude: sorted[idx].Rate}
|
|
}
|
|
}
|
|
|
|
// WindTransport returns a model that moves laterally at the wind velocity
|
|
// sampled from field. Vertical component is zero. Wind components in m/s
|
|
// are converted to deg/s on Earth's surface using R = 6371009 m.
|
|
//
|
|
// If events is non-nil, an "above_model" event is emitted whenever the
|
|
// wind field reports altitude above the highest pressure level.
|
|
func WindTransport(field weather.WindField, events *EventSink) Model {
|
|
const earthR = 6371009.0
|
|
const piOver180 = math.Pi / 180.0
|
|
const degPerRad = 180.0 / math.Pi
|
|
return func(t float64, s State) State {
|
|
sample, err := field.Wind(t, s.Lat, s.Lng, s.Altitude)
|
|
if err != nil {
|
|
return State{}
|
|
}
|
|
if sample.AboveModel && events != nil {
|
|
events.Emit("above_model", t, s,
|
|
"altitude exceeded the highest pressure level of the wind dataset; samples extrapolated")
|
|
}
|
|
r := earthR + s.Altitude
|
|
return State{
|
|
Lat: degPerRad * sample.V / r,
|
|
Lng: degPerRad * sample.U / (r * math.Cos(s.Lat*piOver180)),
|
|
}
|
|
}
|
|
}
|