feat(decart): interp
This commit is contained in:
parent
1bd9143186
commit
19557f3a62
14 changed files with 681 additions and 208 deletions
|
|
@ -119,14 +119,14 @@ func TestPiecewiseRate(t *testing.T) {
|
|||
{Until: math.Inf(1), Rate: 0},
|
||||
})
|
||||
|
||||
if r := m(50, State{}); r.Altitude != 5 {
|
||||
t.Errorf("rate at t=50 = %v, want 5", r.Altitude)
|
||||
if r := m(50, State{}); r.Vertical != 5 {
|
||||
t.Errorf("rate at t=50 = %v, want 5", r.Vertical)
|
||||
}
|
||||
if r := m(150, State{}); r.Altitude != 3 {
|
||||
t.Errorf("rate at t=150 = %v, want 3", r.Altitude)
|
||||
if r := m(150, State{}); r.Vertical != 3 {
|
||||
t.Errorf("rate at t=150 = %v, want 3", r.Vertical)
|
||||
}
|
||||
if r := m(300, State{}); r.Altitude != 0 {
|
||||
t.Errorf("rate at t=300 = %v, want 0", r.Altitude)
|
||||
if r := m(300, State{}); r.Vertical != 0 {
|
||||
t.Errorf("rate at t=300 = %v, want 0", r.Vertical)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -149,11 +149,11 @@ func TestPiecewiseReferenceResolution(t *testing.T) {
|
|||
ctx := StageContext{ProfileStart: 1000, PropagatorStart: 5000}
|
||||
m := built.Build(ctx)
|
||||
// Until=100 from propagator_start=5000 → absolute 5100.
|
||||
if r := m(5050, State{}); r.Altitude != 5 {
|
||||
t.Errorf("rate at t=5050 = %v, want 5", r.Altitude)
|
||||
if r := m(5050, State{}); r.Vertical != 5 {
|
||||
t.Errorf("rate at t=5050 = %v, want 5", r.Vertical)
|
||||
}
|
||||
if r := m(5150, State{}); r.Altitude != 3 {
|
||||
t.Errorf("rate at t=5150 = %v, want 3", r.Altitude)
|
||||
if r := m(5150, State{}); r.Vertical != 3 {
|
||||
t.Errorf("rate at t=5150 = %v, want 3", r.Vertical)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,22 +166,20 @@ func (w fixedWind) Wind(_ float64, _, _, _ float64) (weather.Sample, error) {
|
|||
func (fixedWind) Epoch() time.Time { return time.Unix(0, 0) }
|
||||
func (fixedWind) Source() string { return "test-fixed" }
|
||||
|
||||
func TestWindTransportUnitConversion(t *testing.T) {
|
||||
wind := WindTransport(fixedWind{u: 10, v: 0}, nil)
|
||||
d := wind(0, State{Lat: 0, Lng: 0, Altitude: 0})
|
||||
wantLng := (180.0 / math.Pi) * 10.0 / 6371009.0
|
||||
if math.Abs(d.Lng-wantLng) > 1e-12 {
|
||||
t.Errorf("dlng = %v, want %v", d.Lng, wantLng)
|
||||
}
|
||||
if math.Abs(d.Lat) > 1e-12 {
|
||||
t.Errorf("dlat = %v, want 0 for u=10 v=0", d.Lat)
|
||||
}
|
||||
func TestWindTransportPassesWindThroughUnchanged(t *testing.T) {
|
||||
// The wind field already gives a horizontal velocity, so the propagator
|
||||
// receives it verbatim. This replaces a test of the old deg/s conversion,
|
||||
// whose 1/cos(lat) factor is exactly what made the poles unusable.
|
||||
wind := WindTransport(fixedWind{u: 10, v: -4}, nil)
|
||||
|
||||
wind2 := WindTransport(fixedWind{u: 0, v: 5}, nil)
|
||||
d = wind2(0, State{Lat: 60, Lng: 0, Altitude: 0})
|
||||
wantLat := (180.0 / math.Pi) * 5.0 / 6371009.0
|
||||
if math.Abs(d.Lat-wantLat) > 1e-12 {
|
||||
t.Errorf("dlat at lat=60 = %v, want %v", d.Lat, wantLat)
|
||||
for _, lat := range []float64{0, 45, 60, 89, 89.999} {
|
||||
r := wind(0, State{Lat: lat, Lng: 0, Altitude: 0})
|
||||
if r.East != 10 || r.North != -4 {
|
||||
t.Errorf("lat %g: rate = %+v, want East=10 North=-4", lat, r)
|
||||
}
|
||||
if r.Vertical != 0 {
|
||||
t.Errorf("lat %g: wind must not produce vertical motion, got %v", lat, r.Vertical)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ func Sum(models ...Model) Model {
|
|||
if len(models) == 1 {
|
||||
return models[0]
|
||||
}
|
||||
return func(t float64, s State) State {
|
||||
var sum State
|
||||
return func(t float64, s State) numerics.Rate {
|
||||
var sum numerics.Rate
|
||||
for _, m := range models {
|
||||
sum = numerics.AddGeo(sum, m(t, s))
|
||||
sum = numerics.AddRate(sum, m(t, s))
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ func Sum(models ...Model) Model {
|
|||
// 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} }
|
||||
return func(_ float64, _ State) numerics.Rate { return numerics.Rate{Vertical: rate} }
|
||||
}
|
||||
|
||||
// ParachuteDescent returns a model where vertical velocity grows with
|
||||
|
|
@ -40,8 +40,8 @@ func ConstantRate(rate float64) Model {
|
|||
//
|
||||
// using the NASA atmosphere model for rho. Equivalent to Tawhiri's drag_descent.
|
||||
func ParachuteDescent(seaLevelRate float64) Model {
|
||||
return func(_ float64, s State) State {
|
||||
return State{Altitude: numerics.DragTerminalVelocity(seaLevelRate, s.Altitude)}
|
||||
return func(_ float64, s State) numerics.Rate {
|
||||
return numerics.Rate{Vertical: numerics.DragTerminalVelocity(seaLevelRate, s.Altitude)}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -64,33 +64,34 @@ func Piecewise(segments []RateSegment) Model {
|
|||
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 {
|
||||
return func(t float64, _ State) numerics.Rate {
|
||||
idx := sort.Search(len(sorted), func(i int) bool { return sorted[i].Until > t })
|
||||
if idx == len(sorted) {
|
||||
return State{Altitude: finalRate}
|
||||
return numerics.Rate{Vertical: finalRate}
|
||||
}
|
||||
return State{Altitude: sorted[idx].Rate}
|
||||
return numerics.Rate{Vertical: sorted[idx].Rate}
|
||||
}
|
||||
}
|
||||
|
||||
// WindTransport returns a model that moves laterally at the wind velocity
|
||||
// sampled from field. The vertical component is zero. Sampling and the
|
||||
// non-fatal "above_model" event live here (orchestration); the m/s → deg/s
|
||||
// conversion is numerics.WindToGeoRate.
|
||||
// wind is handed straight to the integrator as a horizontal velocity.
|
||||
//
|
||||
// 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 {
|
||||
return func(t float64, s State) State {
|
||||
return func(t float64, s State) numerics.Rate {
|
||||
sample, err := field.Wind(t, s.Lat, s.Lng, s.Altitude)
|
||||
if err != nil {
|
||||
return State{}
|
||||
return numerics.Rate{}
|
||||
}
|
||||
if sample.AboveModel && events != nil {
|
||||
events.Emit("above_model", t, s,
|
||||
"altitude exceeded the highest pressure level of the wind dataset; samples extrapolated")
|
||||
}
|
||||
dLat, dLng := numerics.WindToGeoRate(sample.U, sample.V, s.Lat, s.Altitude)
|
||||
return State{Lat: dLat, Lng: dLng}
|
||||
// The wind is already a horizontal velocity; it is handed over as-is.
|
||||
// Converting it to deg/s here is what used to blow up near the poles.
|
||||
return numerics.Rate{East: sample.U, North: sample.V}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ func (p *Propagator) run(ctx StageContext, t0 float64, s0 State, globals []Const
|
|||
constraints = p.BuildConstraints(ctx)
|
||||
}
|
||||
|
||||
field := numerics.Field(model)
|
||||
field := numerics.RateField(model)
|
||||
|
||||
out := Result{Propagator: p.Name, Outcome: OutcomeContinued, Path: numerics.NewPath(estimatedSteps)}
|
||||
out.Path.Append(t0, s0)
|
||||
|
|
|
|||
|
|
@ -20,11 +20,15 @@ import "predictor-refactored/internal/numerics"
|
|||
// the numeric core share one hot-path value type without conversions.
|
||||
type State = numerics.GeoVec
|
||||
|
||||
// Model returns the time derivative of state at (t, s).
|
||||
// Model returns the rate of change of state at (t, s), as a velocity in the
|
||||
// local horizontal frame (metres per second east/north/up).
|
||||
//
|
||||
// The derivative is direction-independent; the integrator applies the
|
||||
// sign of dt for reverse propagation.
|
||||
type Model func(t float64, s State) State
|
||||
// It is deliberately not a lat/lon derivative: that form carries a 1/cos(lat)
|
||||
// factor in longitude which diverges at the poles. See numerics.Rate.
|
||||
//
|
||||
// The rate is direction-independent; the integrator applies the sign of dt for
|
||||
// reverse propagation.
|
||||
type Model func(t float64, s State) numerics.Rate
|
||||
|
||||
// Direction is the time direction of integration.
|
||||
type Direction int8
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue