feat: predictions
This commit is contained in:
parent
42e7924be9
commit
11be8f351f
42 changed files with 2221 additions and 516 deletions
|
|
@ -20,12 +20,17 @@ type memCache struct {
|
|||
func (c *memCache) get(k uint64) (vec, bool) {
|
||||
if v, ok := c.m.Load(k); ok {
|
||||
it := v.(item)
|
||||
|
||||
if time.Now().Before(it.exp) {
|
||||
return it.v, true
|
||||
}
|
||||
|
||||
c.m.Delete(k)
|
||||
}
|
||||
|
||||
return vec{}, false
|
||||
}
|
||||
|
||||
func (c *memCache) set(k uint64, v vec) { c.m.Store(k, item{v, time.Now().Add(c.ttl)}) }
|
||||
func (c *memCache) set(k uint64, v vec) {
|
||||
c.m.Store(k, item{v, time.Now().Add(c.ttl)})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package grib
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"git.intra.yksa.space/gsn/predictor/internal/pkg/errcodes"
|
||||
env "github.com/caarlos0/env/v11"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
|
|
@ -11,5 +13,15 @@ type Config struct {
|
|||
CacheTTL time.Duration `env:"CACHE_TTL" envDefault:"1h"`
|
||||
Parallel int `env:"PARALLEL" envDefault:"4"`
|
||||
Timeout time.Duration `env:"TIMEOUT" envDefault:"30s"`
|
||||
DatasetURL url.URL `env:"DATASET_URL" envDefault:"https://nomads.ncep.noaa.gov/"`
|
||||
DatasetURL string `env:"DATASET_URL" envDefault:"https://nomads.ncep.noaa.gov/pub/data/nccf/com/gfs/prod"`
|
||||
}
|
||||
|
||||
func NewConfig() (*Config, error) {
|
||||
cfg := &Config{}
|
||||
if err := env.ParseWithOptions(cfg, env.Options{
|
||||
PrefixTagName: "GSN_PREDICTOR_GRIB_",
|
||||
}); err != nil {
|
||||
return nil, errcodes.Wrap(err, "failed to parse GRIB config")
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
)
|
||||
|
||||
type cube struct {
|
||||
mm mmap.MMap // read‑only, U followed by V (float32 LE)
|
||||
mm mmap.MMap
|
||||
t, p, lat, lon int
|
||||
bytesPerVar int64
|
||||
file *os.File
|
||||
|
|
|
|||
|
|
@ -13,17 +13,15 @@ import (
|
|||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// NOMADS only.
|
||||
const nomadsRoot = "https://nomads.ncep.noaa.gov/pub/data/nccf/com/gfs/prod"
|
||||
|
||||
type Downloader struct {
|
||||
Dir string
|
||||
Parallel int
|
||||
Client *http.Client
|
||||
Dir string
|
||||
Parallel int
|
||||
Client *http.Client
|
||||
DatasetURL string
|
||||
}
|
||||
|
||||
func (d *Downloader) fileURL(run string, hour int, step int) string {
|
||||
return fmt.Sprintf("%s/gfs.%s/%02d/atmos/gfs.t%02dz.pgrb2.0p50.f%03d", nomadsRoot, run, hour, hour, step)
|
||||
return fmt.Sprintf("%s/gfs.%s/%02d/atmos/gfs.t%02dz.pgrb2.0p50.f%03d", d.DatasetURL, run, hour, hour, step)
|
||||
}
|
||||
|
||||
func (d *Downloader) fetch(ctx context.Context, url, dst string) error {
|
||||
|
|
|
|||
|
|
@ -27,15 +27,17 @@ type Service interface {
|
|||
Update(ctx context.Context) error
|
||||
Extract(ctx context.Context, lat, lon, alt float64, ts time.Time) ([2]float64, error)
|
||||
Close() error
|
||||
GetStatus() (ready bool, lastUpdate time.Time, isFresh bool, errMsg string)
|
||||
}
|
||||
|
||||
type ServiceConfig struct {
|
||||
Dir string
|
||||
TTL time.Duration
|
||||
CacheTTL time.Duration
|
||||
Redis RedisIface
|
||||
Parallel int
|
||||
Client *http.Client
|
||||
Dir string
|
||||
TTL time.Duration
|
||||
CacheTTL time.Duration
|
||||
Redis RedisIface
|
||||
Parallel int
|
||||
Client *http.Client
|
||||
DatasetURL string
|
||||
}
|
||||
|
||||
type service struct {
|
||||
|
|
@ -147,7 +149,7 @@ func (s *service) Update(ctx context.Context) error {
|
|||
}
|
||||
}
|
||||
|
||||
dl := Downloader{Dir: s.cfg.Dir, Parallel: s.cfg.Parallel, Client: s.cfg.Client}
|
||||
dl := Downloader{Dir: s.cfg.Dir, Parallel: s.cfg.Parallel, Client: s.cfg.Client, DatasetURL: s.cfg.DatasetURL}
|
||||
run := nearestRun(time.Now().UTC().Add(-4 * time.Hour))
|
||||
|
||||
// Check if we already have this run
|
||||
|
|
@ -334,3 +336,16 @@ func (s *service) Close() error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) GetStatus() (ready bool, lastUpdate time.Time, isFresh bool, errMsg string) {
|
||||
d := s.data.Load()
|
||||
if d == nil {
|
||||
return false, time.Time{}, false, "no dataset loaded"
|
||||
}
|
||||
runTime := time.Unix(d.runUTC, 0)
|
||||
fresh := time.Since(runTime) < s.cfg.TTL
|
||||
if !fresh {
|
||||
return false, runTime, false, "dataset is too old"
|
||||
}
|
||||
return true, runTime, true, ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
package grib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestServiceCreation(t *testing.T) {
|
||||
cfg := ServiceConfig{
|
||||
Dir: "/tmp/grib_test",
|
||||
TTL: 24 * time.Hour,
|
||||
CacheTTL: 1 * time.Hour,
|
||||
Redis: &MockRedis{},
|
||||
Parallel: 2,
|
||||
}
|
||||
|
||||
service, err := New(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create service: %v", err)
|
||||
}
|
||||
defer service.Close()
|
||||
|
||||
if service == nil {
|
||||
t.Fatal("Service is nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNearestRun(t *testing.T) {
|
||||
now := time.Date(2024, 1, 15, 14, 30, 0, 0, time.UTC)
|
||||
expected := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
result := nearestRun(now)
|
||||
if !result.Equal(expected) {
|
||||
t.Errorf("Expected %v, got %v", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPressureFromAlt(t *testing.T) {
|
||||
alt := 10000.0 // 10km
|
||||
pressure := pressureFromAlt(alt)
|
||||
|
||||
// At 10km, pressure should be around 264 hPa
|
||||
if pressure < 200 || pressure > 300 {
|
||||
t.Errorf("Unexpected pressure at 10km: %f hPa", pressure)
|
||||
}
|
||||
}
|
||||
|
||||
// MockRedis for testing
|
||||
type MockRedis struct{}
|
||||
|
||||
func (m *MockRedis) Lock(ctx context.Context, key string, ttl time.Duration) (func(context.Context), error) {
|
||||
return func(ctx context.Context) {}, nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) Set(key string, value []byte, ttl time.Duration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) Get(key string) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue