272 lines
8.3 KiB
Go
272 lines
8.3 KiB
Go
package engine
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
|
|
"predictor-refactored/internal/weather"
|
|
)
|
|
|
|
// ConstraintSpec is the source-agnostic JSON-shape used to declare a
|
|
// constraint. The Type field is the registry key; remaining fields are
|
|
// extracted by the registered factory.
|
|
type ConstraintSpec struct {
|
|
Type string `json:"type"`
|
|
Action string `json:"action,omitempty"`
|
|
// Op is the comparison operator for scalar constraints (altitude, time).
|
|
Op string `json:"op,omitempty"`
|
|
Limit float64 `json:"limit,omitempty"`
|
|
// Vertices and Mode are used by the polygon constraint.
|
|
Vertices []PolygonVertex `json:"vertices,omitempty"`
|
|
Mode string `json:"mode,omitempty"`
|
|
// Label is an optional human-readable identifier surfaced via Name().
|
|
Label string `json:"label,omitempty"`
|
|
}
|
|
|
|
// ModelSpec is the source-agnostic JSON shape used to declare a model.
|
|
type ModelSpec struct {
|
|
Type string `json:"type"`
|
|
// Rate (m/s) for constant_rate.
|
|
Rate float64 `json:"rate,omitempty"`
|
|
// SeaLevelRate (m/s, positive) for parachute_descent.
|
|
SeaLevelRate float64 `json:"sea_level_rate,omitempty"`
|
|
// Segments for piecewise.
|
|
Segments []PiecewiseSegmentSpec `json:"segments,omitempty"`
|
|
// IncludeWind sums a WindTransport model into the resulting derivative.
|
|
IncludeWind bool `json:"include_wind,omitempty"`
|
|
}
|
|
|
|
// PiecewiseSegmentSpec is one entry in a piecewise rate schedule.
|
|
//
|
|
// Reference selects how the Until field is interpreted:
|
|
//
|
|
// - "absolute" (default): UNIX seconds.
|
|
// - "profile_start": seconds since the profile's launch time.
|
|
// - "propagator_start": seconds since this propagator began running.
|
|
type PiecewiseSegmentSpec struct {
|
|
Until float64 `json:"until"`
|
|
Rate float64 `json:"rate"`
|
|
Reference string `json:"reference,omitempty"`
|
|
}
|
|
|
|
// BuildDeps bundle the runtime dependencies factories may consult.
|
|
type BuildDeps struct {
|
|
Wind weather.WindField
|
|
Terrain TerrainProvider
|
|
Events *EventSink
|
|
}
|
|
|
|
// ConstraintFactory builds one Constraint from a spec.
|
|
type ConstraintFactory func(spec ConstraintSpec, deps BuildDeps) (Constraint, error)
|
|
|
|
// ModelFactory builds one model from a spec. The returned Built is held by
|
|
// a Propagator; if Build is set, it is invoked lazily by the profile
|
|
// runner before every stage so it can capture per-stage start times.
|
|
type ModelFactory func(spec ModelSpec, deps BuildDeps) (BuiltModel, error)
|
|
|
|
// BuiltModel is either an eager Model, a lazy Build, or both. The profile
|
|
// runner prefers Build when present.
|
|
type BuiltModel struct {
|
|
Model Model
|
|
Build func(ctx StageContext) Model
|
|
}
|
|
|
|
var (
|
|
regMu sync.RWMutex
|
|
constraintFactories = map[string]ConstraintFactory{}
|
|
modelFactories = map[string]ModelFactory{}
|
|
)
|
|
|
|
// RegisterConstraint installs a factory for typeName. Subsequent calls
|
|
// overwrite the previous factory.
|
|
func RegisterConstraint(typeName string, f ConstraintFactory) {
|
|
regMu.Lock()
|
|
defer regMu.Unlock()
|
|
constraintFactories[typeName] = f
|
|
}
|
|
|
|
// RegisterModel installs a model factory.
|
|
func RegisterModel(typeName string, f ModelFactory) {
|
|
regMu.Lock()
|
|
defer regMu.Unlock()
|
|
modelFactories[typeName] = f
|
|
}
|
|
|
|
// BuildConstraint dispatches spec to its registered factory.
|
|
func BuildConstraint(spec ConstraintSpec, deps BuildDeps) (Constraint, error) {
|
|
regMu.RLock()
|
|
f, ok := constraintFactories[spec.Type]
|
|
regMu.RUnlock()
|
|
if !ok {
|
|
return nil, fmt.Errorf("unknown constraint type %q", spec.Type)
|
|
}
|
|
return f(spec, deps)
|
|
}
|
|
|
|
// BuildModel dispatches spec to its registered factory.
|
|
func BuildModel(spec ModelSpec, deps BuildDeps) (BuiltModel, error) {
|
|
regMu.RLock()
|
|
f, ok := modelFactories[spec.Type]
|
|
regMu.RUnlock()
|
|
if !ok {
|
|
return BuiltModel{}, fmt.Errorf("unknown model type %q", spec.Type)
|
|
}
|
|
return f(spec, deps)
|
|
}
|
|
|
|
// RegisteredConstraints returns the names of every registered constraint type.
|
|
func RegisteredConstraints() []string {
|
|
regMu.RLock()
|
|
defer regMu.RUnlock()
|
|
out := make([]string, 0, len(constraintFactories))
|
|
for k := range constraintFactories {
|
|
out = append(out, k)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// RegisteredModels returns the names of every registered model type.
|
|
func RegisteredModels() []string {
|
|
regMu.RLock()
|
|
defer regMu.RUnlock()
|
|
out := make([]string, 0, len(modelFactories))
|
|
for k := range modelFactories {
|
|
out = append(out, k)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// --- Built-in registrations ------------------------------------------------
|
|
|
|
func init() {
|
|
RegisterConstraint("altitude", buildAltitude)
|
|
RegisterConstraint("time", buildTime)
|
|
RegisterConstraint("terrain_contact", buildTerrainContact)
|
|
RegisterConstraint("polygon", buildPolygon)
|
|
|
|
RegisterModel("constant_rate", buildConstantRate)
|
|
RegisterModel("parachute_descent", buildParachuteDescent)
|
|
RegisterModel("piecewise", buildPiecewise)
|
|
RegisterModel("wind", buildWind)
|
|
}
|
|
|
|
func buildAltitude(spec ConstraintSpec, _ BuildDeps) (Constraint, error) {
|
|
op, err := ParseOperator(spec.Op)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("altitude: %w", err)
|
|
}
|
|
act, err := ParseAction(spec.Action)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("altitude: %w", err)
|
|
}
|
|
return Altitude{Op: op, Limit: spec.Limit, On: act}, nil
|
|
}
|
|
|
|
func buildTime(spec ConstraintSpec, _ BuildDeps) (Constraint, error) {
|
|
op, err := ParseOperator(spec.Op)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("time: %w", err)
|
|
}
|
|
act, err := ParseAction(spec.Action)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("time: %w", err)
|
|
}
|
|
return Time{Op: op, Limit: spec.Limit, On: act}, nil
|
|
}
|
|
|
|
func buildTerrainContact(spec ConstraintSpec, deps BuildDeps) (Constraint, error) {
|
|
if deps.Terrain == nil {
|
|
return nil, fmt.Errorf("terrain_contact requires a terrain provider")
|
|
}
|
|
act, err := ParseAction(spec.Action)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("terrain_contact: %w", err)
|
|
}
|
|
return TerrainContact{Provider: deps.Terrain, On: act}, nil
|
|
}
|
|
|
|
func buildPolygon(spec ConstraintSpec, _ BuildDeps) (Constraint, error) {
|
|
if len(spec.Vertices) < 3 {
|
|
return nil, fmt.Errorf("polygon requires at least 3 vertices")
|
|
}
|
|
act, err := ParseAction(spec.Action)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("polygon: %w", err)
|
|
}
|
|
mode := PolygonInside
|
|
switch spec.Mode {
|
|
case "", "inside":
|
|
mode = PolygonInside
|
|
case "outside":
|
|
mode = PolygonOutside
|
|
default:
|
|
return nil, fmt.Errorf("polygon: unknown mode %q", spec.Mode)
|
|
}
|
|
return NewPolygon(spec.Vertices, mode, act, spec.Label), nil
|
|
}
|
|
|
|
func buildConstantRate(spec ModelSpec, _ BuildDeps) (BuiltModel, error) {
|
|
return BuiltModel{Model: ConstantRate(spec.Rate)}, nil
|
|
}
|
|
|
|
func buildParachuteDescent(spec ModelSpec, _ BuildDeps) (BuiltModel, error) {
|
|
if spec.SeaLevelRate <= 0 {
|
|
return BuiltModel{}, fmt.Errorf("parachute_descent requires positive sea_level_rate")
|
|
}
|
|
return BuiltModel{Model: ParachuteDescent(spec.SeaLevelRate)}, nil
|
|
}
|
|
|
|
func buildWind(_ ModelSpec, deps BuildDeps) (BuiltModel, error) {
|
|
if deps.Wind == nil {
|
|
return BuiltModel{}, fmt.Errorf("wind model requires a loaded wind field")
|
|
}
|
|
return BuiltModel{Model: WindTransport(deps.Wind, deps.Events)}, nil
|
|
}
|
|
|
|
func buildPiecewise(spec ModelSpec, deps BuildDeps) (BuiltModel, error) {
|
|
for _, s := range spec.Segments {
|
|
switch s.Reference {
|
|
case "", "absolute", "profile_start", "propagator_start":
|
|
default:
|
|
return BuiltModel{}, fmt.Errorf("piecewise: unknown segment reference %q", s.Reference)
|
|
}
|
|
}
|
|
// Always build lazily: the profile runner supplies a StageContext before
|
|
// each stage, which is what resolves absolute / profile-relative /
|
|
// propagator-relative segment times uniformly.
|
|
return BuiltModel{
|
|
Build: func(ctx StageContext) Model {
|
|
return maybeAddWind(Piecewise(resolveSegments(spec.Segments, ctx)), spec.IncludeWind, deps)
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// resolveSegments converts spec segments to engine.RateSegment using the
|
|
// stage context to resolve relative references.
|
|
func resolveSegments(in []PiecewiseSegmentSpec, ctx StageContext) []RateSegment {
|
|
out := make([]RateSegment, 0, len(in))
|
|
for _, s := range in {
|
|
var until float64
|
|
switch s.Reference {
|
|
case "", "absolute":
|
|
until = s.Until
|
|
case "profile_start":
|
|
until = ctx.ProfileStart + s.Until
|
|
case "propagator_start":
|
|
until = ctx.PropagatorStart + s.Until
|
|
}
|
|
out = append(out, RateSegment{Until: until, Rate: s.Rate})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// maybeAddWind sums a WindTransport model into base when the spec asks for it.
|
|
func maybeAddWind(base Model, includeWind bool, deps BuildDeps) Model {
|
|
if !includeWind {
|
|
return base
|
|
}
|
|
if deps.Wind == nil {
|
|
return base
|
|
}
|
|
return Sum(base, WindTransport(deps.Wind, deps.Events))
|
|
}
|