step one
This commit is contained in:
parent
7a8d5d13fa
commit
9e663db9dc
68 changed files with 5647 additions and 2958 deletions
153
cmd/compare-tawhiri/main.go
Normal file
153
cmd/compare-tawhiri/main.go
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
// Command compare-tawhiri runs the same prediction against a local predictor
|
||||
// instance and against the public SondeHub Tawhiri instance, reporting the
|
||||
// distance between the two predicted landing points.
|
||||
//
|
||||
// Intended use:
|
||||
//
|
||||
// ./compare-tawhiri --server http://localhost:8080
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
const tawhiriPublicURL = "https://api.v2.sondehub.org/tawhiri"
|
||||
|
||||
func main() {
|
||||
server := flag.String("server", "http://localhost:8080", "local predictor server URL")
|
||||
lat := flag.Float64("lat", 52.2135, "launch latitude")
|
||||
lng := flag.Float64("lng", 0.0964, "launch longitude")
|
||||
alt := flag.Float64("alt", 0, "launch altitude")
|
||||
rate := flag.Float64("ascent-rate", 5, "ascent rate m/s")
|
||||
burst := flag.Float64("burst", 30000, "burst altitude m")
|
||||
descent := flag.Float64("descent-rate", 5, "descent rate m/s")
|
||||
launch := flag.String("launch", "", "launch time RFC3339; default: 3 hours after the active dataset epoch")
|
||||
flag.Parse()
|
||||
|
||||
// Discover the active dataset epoch from /ready.
|
||||
epoch, err := fetchActiveEpoch(*server)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "ready:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
launchTime := epoch.Add(3 * time.Hour)
|
||||
if *launch != "" {
|
||||
t, err := time.Parse(time.RFC3339, *launch)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "invalid launch time:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
launchTime = t
|
||||
}
|
||||
|
||||
ourLat, ourLng, err := runPrediction(*server+"/api/v1/prediction", *lat, *lng, *alt, launchTime, *rate, *burst, *descent)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "local prediction:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("local landing: lat=%.4f, lng=%.4f\n", ourLat, ourLng)
|
||||
|
||||
tawLat, tawLng, err := runPrediction(tawhiriPublicURL, *lat, *lng, *alt, launchTime, *rate, *burst, *descent)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "tawhiri prediction:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("tawhiri landing: lat=%.4f, lng=%.4f\n", tawLat, tawLng)
|
||||
|
||||
d := haversine(ourLat, ourLng, tawLat, tawLng)
|
||||
fmt.Printf("distance: %.2f km\n", d/1000)
|
||||
|
||||
switch {
|
||||
case d < 1000:
|
||||
fmt.Println("MATCH (< 1 km)")
|
||||
case d < 50000:
|
||||
fmt.Printf("MODERATE (%.1f km) — likely different forecast runs\n", d/1000)
|
||||
default:
|
||||
fmt.Printf("LARGE (%.1f km) — investigate\n", d/1000)
|
||||
}
|
||||
}
|
||||
|
||||
type readinessResp struct {
|
||||
Status string `json:"status"`
|
||||
DatasetTime string `json:"dataset_time"`
|
||||
}
|
||||
|
||||
func fetchActiveEpoch(base string) (time.Time, error) {
|
||||
resp, err := http.Get(base + "/ready")
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != 200 {
|
||||
return time.Time{}, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
var r readinessResp
|
||||
if err := json.Unmarshal(body, &r); err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
if r.Status != "ok" {
|
||||
return time.Time{}, fmt.Errorf("server status %q", r.Status)
|
||||
}
|
||||
return time.Parse(time.RFC3339, r.DatasetTime)
|
||||
}
|
||||
|
||||
func runPrediction(endpoint string, lat, lng, alt float64, launch time.Time, rate, burst, descent float64) (float64, float64, error) {
|
||||
q := url.Values{}
|
||||
q.Set("launch_latitude", fmt.Sprintf("%.4f", lat))
|
||||
q.Set("launch_longitude", fmt.Sprintf("%.4f", lng))
|
||||
q.Set("launch_altitude", fmt.Sprintf("%.0f", alt))
|
||||
q.Set("launch_datetime", launch.Format(time.RFC3339))
|
||||
q.Set("ascent_rate", fmt.Sprintf("%.1f", rate))
|
||||
q.Set("burst_altitude", fmt.Sprintf("%.0f", burst))
|
||||
q.Set("descent_rate", fmt.Sprintf("%.1f", descent))
|
||||
|
||||
resp, err := http.Get(endpoint + "?" + q.Encode())
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != 200 {
|
||||
return 0, 0, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Prediction []struct {
|
||||
Stage string `json:"stage"`
|
||||
Trajectory []struct {
|
||||
Latitude float64 `json:"latitude"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
} `json:"trajectory"`
|
||||
} `json:"prediction"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
for _, stage := range result.Prediction {
|
||||
if stage.Stage == "descent" && len(stage.Trajectory) > 0 {
|
||||
last := stage.Trajectory[len(stage.Trajectory)-1]
|
||||
return last.Latitude, last.Longitude, nil
|
||||
}
|
||||
}
|
||||
return 0, 0, fmt.Errorf("no descent stage in response")
|
||||
}
|
||||
|
||||
func haversine(lat1, lng1, lat2, lng2 float64) float64 {
|
||||
const R = 6371000.0
|
||||
phi1 := lat1 * math.Pi / 180
|
||||
phi2 := lat2 * math.Pi / 180
|
||||
dphi := (lat2 - lat1) * math.Pi / 180
|
||||
dlam := (lng2 - lng1) * math.Pi / 180
|
||||
a := math.Sin(dphi/2)*math.Sin(dphi/2) + math.Cos(phi1)*math.Cos(phi2)*math.Sin(dlam/2)*math.Sin(dlam/2)
|
||||
return R * 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue