feat: polish & windviz & deploy
This commit is contained in:
parent
81b8e763bd
commit
465ad00f7b
78 changed files with 20622 additions and 2154 deletions
217
internal/api/mapping.go
Normal file
217
internal/api/mapping.go
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"predictor-refactored/internal/api/async"
|
||||
"predictor-refactored/internal/engine"
|
||||
apirest "predictor-refactored/pkg/rest"
|
||||
)
|
||||
|
||||
// normalizeLng folds a longitude into [0, 360) for internal use.
|
||||
func normalizeLng(lng float64) float64 {
|
||||
if lng < 0 {
|
||||
return lng + 360
|
||||
}
|
||||
return lng
|
||||
}
|
||||
|
||||
// signedLng converts an internal [0, 360) longitude back to [-180, 180).
|
||||
func signedLng(lng float64) float64 {
|
||||
if lng > 180 {
|
||||
return lng - 360
|
||||
}
|
||||
return lng
|
||||
}
|
||||
|
||||
// buildProfile translates an API prediction request into an engine profile
|
||||
// using the engine's model/constraint registry.
|
||||
// maxProfileStages bounds the propagator chain length to keep a single
|
||||
// request's work bounded.
|
||||
const maxProfileStages = 32
|
||||
|
||||
func buildProfile(req *apirest.PredictionV2Request, deps engine.BuildDeps) (engine.Profile, error) {
|
||||
if len(req.Profile) == 0 {
|
||||
return engine.Profile{}, fmt.Errorf("profile must contain at least one stage")
|
||||
}
|
||||
if len(req.Profile) > maxProfileStages {
|
||||
return engine.Profile{}, fmt.Errorf("profile has %d stages; maximum is %d", len(req.Profile), maxProfileStages)
|
||||
}
|
||||
|
||||
step := 60.0
|
||||
tol := 0.01
|
||||
if o, ok := req.Options.Get(); ok {
|
||||
step = o.StepSeconds.Or(step)
|
||||
tol = o.Tolerance.Or(tol)
|
||||
}
|
||||
if step <= 0 || step > 3600 {
|
||||
return engine.Profile{}, fmt.Errorf("options.step_seconds must be in (0, 3600], got %g", step)
|
||||
}
|
||||
if tol <= 0 || tol >= 1 {
|
||||
return engine.Profile{}, fmt.Errorf("options.tolerance must be in (0, 1), got %g", tol)
|
||||
}
|
||||
|
||||
dir := engine.Forward
|
||||
if req.Direction.Or(apirest.PredictionV2RequestDirectionForward) == apirest.PredictionV2RequestDirectionReverse {
|
||||
dir = engine.Reverse
|
||||
}
|
||||
|
||||
props := make([]*engine.Propagator, len(req.Profile))
|
||||
for i, stage := range req.Profile {
|
||||
if stage.Name == "" {
|
||||
return engine.Profile{}, fmt.Errorf("stage %d: name is required", i)
|
||||
}
|
||||
built, err := engine.BuildModel(toEngineModelSpec(stage.Model), deps)
|
||||
if err != nil {
|
||||
return engine.Profile{}, fmt.Errorf("stage %q model: %w", stage.Name, err)
|
||||
}
|
||||
constraints, err := toEngineConstraints(stage.Constraints, deps)
|
||||
if err != nil {
|
||||
return engine.Profile{}, fmt.Errorf("stage %q: %w", stage.Name, err)
|
||||
}
|
||||
props[i] = &engine.Propagator{
|
||||
Name: stage.Name,
|
||||
Step: step,
|
||||
Model: built.Model,
|
||||
BuildModel: built.Build,
|
||||
Constraints: constraints,
|
||||
Tolerance: tol,
|
||||
}
|
||||
}
|
||||
for i, stage := range req.Profile {
|
||||
idx, ok := stage.FallbackIndex.Get()
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if idx < 0 || idx >= len(props) {
|
||||
return engine.Profile{}, fmt.Errorf("stage %q: fallback_index %d out of range", stage.Name, idx)
|
||||
}
|
||||
props[i].Fallback = props[idx]
|
||||
}
|
||||
|
||||
globals, err := toEngineConstraints(req.Globals, deps)
|
||||
if err != nil {
|
||||
return engine.Profile{}, fmt.Errorf("globals: %w", err)
|
||||
}
|
||||
return engine.Profile{Stages: props, Direction: dir, Globals: globals}, nil
|
||||
}
|
||||
|
||||
func toEngineModelSpec(m apirest.ModelSpec) engine.ModelSpec {
|
||||
out := engine.ModelSpec{
|
||||
Type: string(m.Type),
|
||||
Rate: m.Rate.Or(0),
|
||||
SeaLevelRate: m.SeaLevelRate.Or(0),
|
||||
IncludeWind: m.IncludeWind.Or(false),
|
||||
}
|
||||
for _, s := range m.Segments {
|
||||
out.Segments = append(out.Segments, engine.PiecewiseSegmentSpec{
|
||||
Until: s.Until,
|
||||
Rate: s.Rate,
|
||||
Reference: string(s.Reference.Or(apirest.PiecewiseSegmentReferenceAbsolute)),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func toEngineConstraints(specs []apirest.ConstraintSpec, deps engine.BuildDeps) ([]engine.Constraint, error) {
|
||||
out := make([]engine.Constraint, 0, len(specs))
|
||||
for i, s := range specs {
|
||||
c, err := engine.BuildConstraint(toEngineConstraintSpec(s), deps)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("constraint[%d]: %w", i, err)
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func toEngineConstraintSpec(c apirest.ConstraintSpec) engine.ConstraintSpec {
|
||||
spec := engine.ConstraintSpec{
|
||||
Type: string(c.Type),
|
||||
Op: string(c.Op.Or("")),
|
||||
Limit: c.Limit.Or(0),
|
||||
Action: string(c.Action.Or(apirest.ConstraintSpecActionStop)),
|
||||
Mode: string(c.Mode.Or("")),
|
||||
Label: c.Label.Or(""),
|
||||
}
|
||||
for _, v := range c.Vertices {
|
||||
spec.Vertices = append(spec.Vertices, engine.PolygonVertex{Lat: v.Lat, Lng: v.Lng})
|
||||
}
|
||||
return spec
|
||||
}
|
||||
|
||||
// stageResultToAPI maps one engine stage result to the API representation.
|
||||
func stageResultToAPI(r engine.Result) apirest.StageResult {
|
||||
out := apirest.StageResult{
|
||||
Name: r.Propagator,
|
||||
Outcome: apirest.StageResultOutcome(r.Outcome.String()),
|
||||
Events: eventsToAPI(r.Events),
|
||||
}
|
||||
if r.Constraint != nil {
|
||||
out.Constraint = apirest.NewOptString(r.ConstraintName)
|
||||
out.Termination = apirest.NewOptTerminationInfo(apirest.TerminationInfo{
|
||||
ViolationTime: time.Unix(int64(r.ViolationTime), 0).UTC(),
|
||||
ViolationState: geoStateToAPI(r.ViolationState),
|
||||
RefinedTime: time.Unix(int64(r.RefinedTime), 0).UTC(),
|
||||
RefinedState: geoStateToAPI(r.RefinedState),
|
||||
})
|
||||
}
|
||||
n := r.Path.Len()
|
||||
out.Trajectory = make([]apirest.TrajectoryPoint, n)
|
||||
for i := range n {
|
||||
t, p := r.Path.At(i)
|
||||
out.Trajectory[i] = apirest.TrajectoryPoint{
|
||||
Time: time.Unix(int64(t), 0).UTC(),
|
||||
Latitude: p.Lat,
|
||||
Longitude: signedLng(p.Lng),
|
||||
Altitude: p.Altitude,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func geoStateToAPI(s engine.State) apirest.GeoState {
|
||||
return apirest.GeoState{Lat: s.Lat, Lng: signedLng(s.Lng), Altitude: s.Altitude}
|
||||
}
|
||||
|
||||
func eventsToAPI(in []engine.EventSummary) []apirest.EventSummary {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]apirest.EventSummary, 0, len(in))
|
||||
for _, e := range in {
|
||||
out = append(out, apirest.EventSummary{
|
||||
Type: e.Type,
|
||||
Count: e.Count,
|
||||
FirstTime: apirest.NewOptFloat64(e.FirstTime),
|
||||
LastTime: apirest.NewOptFloat64(e.LastTime),
|
||||
FirstState: apirest.NewOptGeoState(geoStateToAPI(e.FirstState)),
|
||||
LastState: apirest.NewOptGeoState(geoStateToAPI(e.LastState)),
|
||||
Message: apirest.NewOptString(e.Message),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// asyncJobToAPI maps an async job snapshot to the API PredictionJob.
|
||||
func asyncJobToAPI(info async.JobInfo) *apirest.PredictionJob {
|
||||
job := &apirest.PredictionJob{
|
||||
ID: info.ID,
|
||||
Status: apirest.PredictionJobStatus(info.Status),
|
||||
CreatedAt: info.CreatedAt,
|
||||
}
|
||||
if info.StartedAt != nil {
|
||||
job.StartedAt = apirest.NewOptDateTime(*info.StartedAt)
|
||||
}
|
||||
if info.CompletedAt != nil {
|
||||
job.CompletedAt = apirest.NewOptDateTime(*info.CompletedAt)
|
||||
}
|
||||
if info.Error != "" {
|
||||
job.Error = apirest.NewOptString(info.Error)
|
||||
}
|
||||
if info.Result != nil {
|
||||
job.Result = apirest.NewOptPredictionV2Response(*info.Result)
|
||||
}
|
||||
return job
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue