step one
This commit is contained in:
parent
7a8d5d13fa
commit
9e663db9dc
68 changed files with 5647 additions and 2958 deletions
151
internal/engine/models.go
Normal file
151
internal/engine/models.go
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
package engine
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
"sync/atomic"
|
||||
|
||||
"predictor-refactored/internal/weather"
|
||||
)
|
||||
|
||||
// Sum composes models by summing their derivatives at each evaluation point.
|
||||
//
|
||||
// Useful for combining e.g. 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).
|
||||
// A positive rate is upward (ascent); a negative rate is downward.
|
||||
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 number).
|
||||
// The 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 the given 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.
|
||||
type RateSegment struct {
|
||||
// Until is the UNIX timestamp at which this segment ends.
|
||||
// The model applies the segment's Rate for all t < Until.
|
||||
Until float64
|
||||
// Rate is the vertical velocity (m/s) during the segment. Positive is up.
|
||||
Rate float64
|
||||
}
|
||||
|
||||
// Piecewise returns a model that produces a piecewise-constant vertical rate
|
||||
// over a sequence of time intervals.
|
||||
//
|
||||
// Segments are searched by their Until field; the first segment whose Until
|
||||
// exceeds t supplies the active rate. For t at or after the last Until, the
|
||||
// final segment's Rate is held indefinitely. Input is sorted ascending by
|
||||
// Until on construction.
|
||||
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}
|
||||
}
|
||||
}
|
||||
|
||||
// Warnings aggregates non-fatal conditions encountered during integration.
|
||||
type Warnings struct {
|
||||
// AltitudeTooHigh counts evaluations where the wind sampler reported
|
||||
// that altitude was above the highest pressure level of the dataset.
|
||||
AltitudeTooHigh atomic.Int64
|
||||
}
|
||||
|
||||
// ToMap returns warnings as a map suitable for JSON output. Only counters
|
||||
// that have fired are included.
|
||||
func (w *Warnings) ToMap() map[string]any {
|
||||
out := make(map[string]any)
|
||||
if n := w.AltitudeTooHigh.Load(); n > 0 {
|
||||
out["altitude_too_high"] = map[string]any{
|
||||
"count": n,
|
||||
"description": "altitude exceeded the highest pressure level of the wind dataset; samples were extrapolated",
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// WindTransport returns a model that moves laterally at the wind velocity
|
||||
// sampled from field. The vertical component of the returned derivative is
|
||||
// zero. Wind units are converted from m/s to deg/s on Earth's surface.
|
||||
//
|
||||
// If warnings is non-nil, the AltitudeTooHigh counter is incremented for any
|
||||
// sample where the wind field reported altitude above the model top.
|
||||
func WindTransport(field weather.WindField, warnings *Warnings) 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 && warnings != nil {
|
||||
warnings.AltitudeTooHigh.Add(1)
|
||||
}
|
||||
r := earthR + s.Altitude
|
||||
return State{
|
||||
Lat: degPerRad * sample.V / r,
|
||||
Lng: degPerRad * sample.U / (r * math.Cos(s.Lat*piOver180)),
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue