engine refactor
This commit is contained in:
parent
9e663db9dc
commit
81b8e763bd
37 changed files with 3532 additions and 1639 deletions
|
|
@ -2,8 +2,8 @@
|
|||
// (GET /api/v1/prediction). The request/response shapes match the original
|
||||
// Cambridge University Spaceflight predictor for drop-in compatibility.
|
||||
//
|
||||
// Internally the handler builds an engine.Profile from query parameters and
|
||||
// dispatches it through the same engine path as the new v2 endpoint.
|
||||
// Internally the handler builds an engine.Profile from query parameters
|
||||
// and dispatches it through the same engine path as the new v2 endpoint.
|
||||
package tawhiri
|
||||
|
||||
import (
|
||||
|
|
@ -18,11 +18,11 @@ import (
|
|||
"predictor-refactored/internal/elevation"
|
||||
"predictor-refactored/internal/engine"
|
||||
"predictor-refactored/internal/metrics"
|
||||
"predictor-refactored/internal/weather"
|
||||
api "predictor-refactored/pkg/rest"
|
||||
)
|
||||
|
||||
// Handler implements api.Handler (the ogen-generated interface for
|
||||
// performPrediction and readinessCheck).
|
||||
// Handler implements api.Handler (ogen-generated interface).
|
||||
type Handler struct {
|
||||
mgr *datasets.Manager
|
||||
elev *elevation.Dataset
|
||||
|
|
@ -41,111 +41,49 @@ func New(mgr *datasets.Manager, elev *elevation.Dataset, sink metrics.Sink, log
|
|||
return &Handler{mgr: mgr, elev: elev, metrics: sink, log: log}
|
||||
}
|
||||
|
||||
// Compile-time check that Handler satisfies api.Handler.
|
||||
var _ api.Handler = (*Handler)(nil)
|
||||
|
||||
// PerformPrediction runs the Tawhiri-style prediction.
|
||||
func (h *Handler) PerformPrediction(ctx context.Context, params api.PerformPredictionParams) (*api.PredictionResponse, error) {
|
||||
func (h *Handler) PerformPrediction(_ context.Context, params api.PerformPredictionParams) (*api.PredictionResponse, error) {
|
||||
field := h.mgr.Active()
|
||||
if field == nil {
|
||||
return nil, newError(http.StatusServiceUnavailable, "no dataset loaded, service is starting up")
|
||||
}
|
||||
|
||||
// Parameters with Tawhiri defaults.
|
||||
profileKind := "standard_profile"
|
||||
if v, ok := params.Profile.Get(); ok {
|
||||
profileKind = string(v)
|
||||
}
|
||||
ascentRate := 5.0
|
||||
if v, ok := params.AscentRate.Get(); ok {
|
||||
ascentRate = v
|
||||
}
|
||||
burstAltitude := 28000.0
|
||||
if v, ok := params.BurstAltitude.Get(); ok {
|
||||
burstAltitude = v
|
||||
}
|
||||
descentRate := 5.0
|
||||
if v, ok := params.DescentRate.Get(); ok {
|
||||
descentRate = v
|
||||
}
|
||||
launchAlt := 0.0
|
||||
if v, ok := params.LaunchAltitude.Get(); ok {
|
||||
launchAlt = v
|
||||
}
|
||||
profileKind := optString(params.Profile, "standard_profile")
|
||||
ascentRate := optFloat(params.AscentRate, 5.0)
|
||||
burstAltitude := optFloat(params.BurstAltitude, 28000.0)
|
||||
descentRate := optFloat(params.DescentRate, 5.0)
|
||||
launchAlt := optFloat(params.LaunchAltitude, 0.0)
|
||||
|
||||
lng := params.LaunchLongitude
|
||||
if lng < 0 {
|
||||
lng += 360
|
||||
}
|
||||
|
||||
launchTime := float64(params.LaunchDatetime.Unix())
|
||||
warnings := &engine.Warnings{}
|
||||
|
||||
// Build the profile.
|
||||
events := engine.NewEventSink()
|
||||
|
||||
var stageNames []string
|
||||
var prof engine.Profile
|
||||
switch profileKind {
|
||||
case "standard_profile":
|
||||
stageNames = []string{"ascent", "descent"}
|
||||
prof = engine.Profile{
|
||||
Direction: engine.Forward,
|
||||
Stages: []*engine.Propagator{
|
||||
{
|
||||
Name: "ascent",
|
||||
Step: 60,
|
||||
Model: engine.Sum(
|
||||
engine.ConstantRate(ascentRate),
|
||||
engine.WindTransport(field, warnings),
|
||||
),
|
||||
Constraints: []engine.Constraint{engine.MaxAltitude{Limit: burstAltitude, On: engine.ActionStop}},
|
||||
},
|
||||
{
|
||||
Name: "descent",
|
||||
Step: 60,
|
||||
Model: engine.Sum(
|
||||
engine.ParachuteDescent(descentRate),
|
||||
engine.WindTransport(field, warnings),
|
||||
),
|
||||
Constraints: descentConstraints(h.elev),
|
||||
},
|
||||
},
|
||||
}
|
||||
prof = standardProfile(field, h.elev, events, ascentRate, burstAltitude, descentRate)
|
||||
case "float_profile":
|
||||
floatAlt := 25000.0
|
||||
if v, ok := params.FloatAltitude.Get(); ok {
|
||||
floatAlt = v
|
||||
}
|
||||
floatAlt := optFloat(params.FloatAltitude, 25000.0)
|
||||
stopTime := params.LaunchDatetime.Add(24 * time.Hour)
|
||||
if v, ok := params.StopDatetime.Get(); ok {
|
||||
stopTime = v
|
||||
}
|
||||
stageNames = []string{"ascent", "float"}
|
||||
prof = engine.Profile{
|
||||
Direction: engine.Forward,
|
||||
Stages: []*engine.Propagator{
|
||||
{
|
||||
Name: "ascent",
|
||||
Step: 60,
|
||||
Model: engine.Sum(
|
||||
engine.ConstantRate(ascentRate),
|
||||
engine.WindTransport(field, warnings),
|
||||
),
|
||||
Constraints: []engine.Constraint{engine.MaxAltitude{Limit: floatAlt, On: engine.ActionStop}},
|
||||
},
|
||||
{
|
||||
Name: "float",
|
||||
Step: 60,
|
||||
Model: engine.WindTransport(field, warnings),
|
||||
Constraints: []engine.Constraint{engine.MaxTime{Limit: float64(stopTime.Unix()), On: engine.ActionStop}},
|
||||
},
|
||||
},
|
||||
}
|
||||
prof = floatProfile(field, events, ascentRate, floatAlt, stopTime)
|
||||
default:
|
||||
return nil, newError(http.StatusBadRequest, "unknown profile: "+profileKind)
|
||||
}
|
||||
|
||||
started := time.Now().UTC()
|
||||
results := prof.Run(launchTime, engine.State{Lat: params.LaunchLatitude, Lng: lng, Altitude: launchAlt})
|
||||
results := prof.Run(launchTime, engine.State{Lat: params.LaunchLatitude, Lng: lng, Altitude: launchAlt}, events)
|
||||
completed := time.Now().UTC()
|
||||
h.metrics.Prediction(profileKind, completed.Sub(started), nil)
|
||||
|
||||
|
|
@ -161,30 +99,7 @@ func (h *Handler) PerformPrediction(ctx context.Context, params api.PerformPredi
|
|||
if i < len(stageNames) {
|
||||
stageName = stageNames[i]
|
||||
}
|
||||
stageEnum := api.PredictionResponsePredictionItemStageAscent
|
||||
switch stageName {
|
||||
case "descent":
|
||||
stageEnum = api.PredictionResponsePredictionItemStageDescent
|
||||
case "float":
|
||||
stageEnum = api.PredictionResponsePredictionItemStageFloat
|
||||
}
|
||||
traj := make([]api.PredictionResponsePredictionItemTrajectoryItem, 0, len(r.Points))
|
||||
for _, pt := range r.Points {
|
||||
ptLng := pt.Lng
|
||||
if ptLng > 180 {
|
||||
ptLng -= 360
|
||||
}
|
||||
traj = append(traj, api.PredictionResponsePredictionItemTrajectoryItem{
|
||||
Datetime: time.Unix(int64(pt.Time), 0).UTC(),
|
||||
Latitude: pt.Lat,
|
||||
Longitude: ptLng,
|
||||
Altitude: pt.Altitude,
|
||||
})
|
||||
}
|
||||
resp.Prediction = append(resp.Prediction, api.PredictionResponsePredictionItem{
|
||||
Stage: stageEnum,
|
||||
Trajectory: traj,
|
||||
})
|
||||
resp.Prediction = append(resp.Prediction, buildPredictionItem(stageName, r))
|
||||
}
|
||||
|
||||
resp.Request = api.NewOptPredictionResponseRequest(api.PredictionResponseRequest{
|
||||
|
|
@ -195,7 +110,8 @@ func (h *Handler) PerformPrediction(ctx context.Context, params api.PerformPredi
|
|||
LaunchAltitude: params.LaunchAltitude,
|
||||
})
|
||||
|
||||
if warns := warnings.ToMap(); len(warns) > 0 {
|
||||
if ev := events.Snapshot(); len(ev) > 0 {
|
||||
// Preserve the OpenAPI-defined Warnings shape (open object).
|
||||
resp.Warnings = api.NewOptPredictionResponseWarnings(api.PredictionResponseWarnings{})
|
||||
}
|
||||
|
||||
|
|
@ -207,13 +123,78 @@ func (h *Handler) PerformPrediction(ctx context.Context, params api.PerformPredi
|
|||
return resp, nil
|
||||
}
|
||||
|
||||
// descentConstraints returns the descent termination set: TerrainContact if an
|
||||
// elevation dataset is loaded, MinAltitude(0) otherwise.
|
||||
func descentConstraints(elev *elevation.Dataset) []engine.Constraint {
|
||||
// standardProfile constructs the ascent → descent profile.
|
||||
func standardProfile(field weather.WindField, elev *elevation.Dataset, events *engine.EventSink, ascentRate, burstAltitude, descentRate float64) engine.Profile {
|
||||
wind := engine.WindTransport(field, events)
|
||||
descentTerm := []engine.Constraint{engine.Altitude{Op: engine.OpLessEqual, Limit: 0, On: engine.ActionStop}}
|
||||
if elev != nil {
|
||||
return []engine.Constraint{engine.TerrainContact{Provider: elev, On: engine.ActionStop}}
|
||||
descentTerm = []engine.Constraint{engine.TerrainContact{Provider: elev, On: engine.ActionStop}}
|
||||
}
|
||||
return []engine.Constraint{engine.MinAltitude{Limit: 0, On: engine.ActionStop}}
|
||||
return engine.Profile{
|
||||
Direction: engine.Forward,
|
||||
Stages: []*engine.Propagator{
|
||||
{
|
||||
Name: "ascent",
|
||||
Step: 60,
|
||||
Model: engine.Sum(engine.ConstantRate(ascentRate), wind),
|
||||
Constraints: []engine.Constraint{engine.Altitude{Op: engine.OpGreaterEqual, Limit: burstAltitude, On: engine.ActionStop}},
|
||||
},
|
||||
{
|
||||
Name: "descent",
|
||||
Step: 60,
|
||||
Model: engine.Sum(engine.ParachuteDescent(descentRate), wind),
|
||||
Constraints: descentTerm,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// floatProfile constructs the ascent → float profile.
|
||||
func floatProfile(field weather.WindField, events *engine.EventSink, ascentRate, floatAlt float64, stopTime time.Time) engine.Profile {
|
||||
wind := engine.WindTransport(field, events)
|
||||
return engine.Profile{
|
||||
Direction: engine.Forward,
|
||||
Stages: []*engine.Propagator{
|
||||
{
|
||||
Name: "ascent",
|
||||
Step: 60,
|
||||
Model: engine.Sum(engine.ConstantRate(ascentRate), wind),
|
||||
Constraints: []engine.Constraint{engine.Altitude{Op: engine.OpGreaterEqual, Limit: floatAlt, On: engine.ActionStop}},
|
||||
},
|
||||
{
|
||||
Name: "float",
|
||||
Step: 60,
|
||||
Model: wind,
|
||||
Constraints: []engine.Constraint{engine.Time{Op: engine.OpGreater, Limit: float64(stopTime.Unix()), On: engine.ActionStop}},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildPredictionItem(stageName string, r engine.Result) api.PredictionResponsePredictionItem {
|
||||
var stageEnum api.PredictionResponsePredictionItemStage
|
||||
switch stageName {
|
||||
case "descent":
|
||||
stageEnum = api.PredictionResponsePredictionItemStageDescent
|
||||
case "float":
|
||||
stageEnum = api.PredictionResponsePredictionItemStageFloat
|
||||
default:
|
||||
stageEnum = api.PredictionResponsePredictionItemStageAscent
|
||||
}
|
||||
traj := make([]api.PredictionResponsePredictionItemTrajectoryItem, 0, len(r.Points))
|
||||
for _, pt := range r.Points {
|
||||
ptLng := pt.Lng
|
||||
if ptLng > 180 {
|
||||
ptLng -= 360
|
||||
}
|
||||
traj = append(traj, api.PredictionResponsePredictionItemTrajectoryItem{
|
||||
Datetime: time.Unix(int64(pt.Time), 0).UTC(),
|
||||
Latitude: pt.Lat,
|
||||
Longitude: ptLng,
|
||||
Altitude: pt.Altitude,
|
||||
})
|
||||
}
|
||||
return api.PredictionResponsePredictionItem{Stage: stageEnum, Trajectory: traj}
|
||||
}
|
||||
|
||||
// ReadinessCheck reports whether a dataset is currently loaded.
|
||||
|
|
@ -250,3 +231,21 @@ func newError(status int, description string) *api.ErrorStatusCode {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
// optString returns the option's value if set, else fallback.
|
||||
func optString[T ~string](o interface {
|
||||
Get() (T, bool)
|
||||
}, fallback string) string {
|
||||
if v, ok := o.Get(); ok {
|
||||
return string(v)
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// optFloat returns the option's float64 value if set, else fallback.
|
||||
func optFloat(o api.OptFloat64, fallback float64) float64 {
|
||||
if v, ok := o.Get(); ok {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue