feat: polish & windviz & deploy
This commit is contained in:
parent
81b8e763bd
commit
465ad00f7b
78 changed files with 20622 additions and 2154 deletions
|
|
@ -108,6 +108,14 @@ func (d *File) Val(hour, level, variable, lat, lng int) float32 {
|
|||
return math.Float32frombits(binary.LittleEndian.Uint32(d.mm[off : off+4]))
|
||||
}
|
||||
|
||||
// ValByElem reads the float32 at a precomputed flat element index (not a byte
|
||||
// offset). The wind sampler uses this to read the eight interpolation corners
|
||||
// after computing their flat indices once via cube strides.
|
||||
func (d *File) ValByElem(elem int64) float32 {
|
||||
off := elem * ElementSize
|
||||
return math.Float32frombits(binary.LittleEndian.Uint32(d.mm[off : off+4]))
|
||||
}
|
||||
|
||||
// SetVal writes one cell. Only valid on writable files.
|
||||
func (d *File) SetVal(hour, level, variable, lat, lng int, val float32) {
|
||||
off := d.offset(hour, level, variable, lat, lng)
|
||||
|
|
|
|||
|
|
@ -8,39 +8,40 @@ import (
|
|||
)
|
||||
|
||||
// Wind is a WindField backed by a GFS dataset file.
|
||||
//
|
||||
// The cube is addressed in flat element units with fixed strides so the
|
||||
// sampler can compute the eight horizontal interpolation corners once and
|
||||
// reach any (level, variable) by adding constant strides — avoiding the
|
||||
// five-multiply offset computation per corner per evaluation.
|
||||
type Wind struct {
|
||||
file *File
|
||||
|
||||
hourAxis numerics.Axis
|
||||
latAxis numerics.Axis
|
||||
lngAxis numerics.Axis
|
||||
|
||||
hourStride int64 // elements between successive hours
|
||||
levelStride int64 // elements between successive pressure levels
|
||||
varStride int64 // elements between successive variables
|
||||
latStride int64 // elements between successive latitudes
|
||||
}
|
||||
|
||||
// NewWind returns a Wind backed by file. The axes are constructed from the
|
||||
// file's variant geometry.
|
||||
// NewWind returns a Wind backed by file. Axes and strides are derived from
|
||||
// the file's variant geometry.
|
||||
func NewWind(file *File) *Wind {
|
||||
v := file.variant
|
||||
nLat := v.NumLatitudes()
|
||||
nLng := v.NumLongitudes()
|
||||
nLev := v.NumLevels()
|
||||
return &Wind{
|
||||
file: file,
|
||||
hourAxis: numerics.Axis{
|
||||
Left: 0,
|
||||
Step: float64(v.HourStep),
|
||||
N: v.NumHours(),
|
||||
Name: "hour",
|
||||
},
|
||||
latAxis: numerics.Axis{
|
||||
Left: LatStart,
|
||||
Step: v.Resolution,
|
||||
N: v.NumLatitudes(),
|
||||
Name: "lat",
|
||||
},
|
||||
lngAxis: numerics.Axis{
|
||||
Left: LonStart,
|
||||
Step: v.Resolution,
|
||||
N: v.NumLongitudes(),
|
||||
Wrap: true,
|
||||
Name: "lng",
|
||||
},
|
||||
file: file,
|
||||
hourAxis: numerics.Axis{Left: 0, Step: float64(v.HourStep), N: v.NumHours(), Name: "hour"},
|
||||
latAxis: numerics.Axis{Left: LatStart, Step: v.Resolution, N: nLat, Name: "lat"},
|
||||
lngAxis: numerics.Axis{Left: LonStart, Step: v.Resolution, N: nLng, Wrap: true, Name: "lng"},
|
||||
hourStride: int64(nLev) * NumVariables * int64(nLat) * int64(nLng),
|
||||
levelStride: NumVariables * int64(nLat) * int64(nLng),
|
||||
varStride: int64(nLat) * int64(nLng),
|
||||
latStride: int64(nLng),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,38 +73,53 @@ func (w *Wind) Wind(t, lat, lng, alt float64) (weather.Sample, error) {
|
|||
if err != nil {
|
||||
return weather.Sample{}, err
|
||||
}
|
||||
bs := [3]numerics.Bracket{bh, bla, bln}
|
||||
|
||||
height := func(level int) func(i, j, k int) float64 {
|
||||
return func(i, j, k int) float64 {
|
||||
return float64(w.file.Val(i, level, VarHeight, j, k))
|
||||
weights := numerics.TrilinearWeights([3]numerics.Bracket{bh, bla, bln})
|
||||
|
||||
// Flat element index of each of the eight horizontal corners, at level 0
|
||||
// variable 0, in the canonical TrilinearWeights order (hour outer, lng
|
||||
// inner). Reaching a given (level, variable) corner only adds constant
|
||||
// strides.
|
||||
var base [8]int64
|
||||
hours2 := [2]int64{int64(bh.Lo) * w.hourStride, int64(bh.Hi) * w.hourStride}
|
||||
lats2 := [2]int64{int64(bla.Lo) * w.latStride, int64(bla.Hi) * w.latStride}
|
||||
lngs2 := [2]int64{int64(bln.Lo), int64(bln.Hi)}
|
||||
i := 0
|
||||
for _, h := range hours2 {
|
||||
for _, la := range lats2 {
|
||||
for _, ln := range lngs2 {
|
||||
base[i] = h + la + ln
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sample := func(level int, varIdx int64) float64 {
|
||||
off := int64(level)*w.levelStride + varIdx*w.varStride
|
||||
var vals [8]float64
|
||||
for k := range 8 {
|
||||
vals[k] = float64(w.file.ValByElem(base[k] + off))
|
||||
}
|
||||
return numerics.Dot8(&weights, &vals)
|
||||
}
|
||||
|
||||
// Largest pressure level whose interpolated geopotential height is below alt.
|
||||
levelIdx := numerics.Bisect(0, w.file.variant.NumLevels()-2, alt, func(level int) float64 {
|
||||
return numerics.EvalTrilinear(bs, height(level))
|
||||
return sample(level, VarHeight)
|
||||
})
|
||||
|
||||
lowerHGT := numerics.EvalTrilinear(bs, height(levelIdx))
|
||||
upperHGT := numerics.EvalTrilinear(bs, height(levelIdx+1))
|
||||
lowerHGT := sample(levelIdx, VarHeight)
|
||||
upperHGT := sample(levelIdx+1, VarHeight)
|
||||
|
||||
var altFrac float64
|
||||
altFrac := 0.5
|
||||
if lowerHGT != upperHGT {
|
||||
altFrac = (upperHGT - alt) / (upperHGT - lowerHGT)
|
||||
} else {
|
||||
altFrac = 0.5
|
||||
}
|
||||
|
||||
component := func(level, variable int) float64 {
|
||||
return numerics.EvalTrilinear(bs, func(i, j, k int) float64 {
|
||||
return float64(w.file.Val(i, level, variable, j, k))
|
||||
})
|
||||
}
|
||||
|
||||
lowerU := component(levelIdx, VarWindU)
|
||||
upperU := component(levelIdx+1, VarWindU)
|
||||
lowerV := component(levelIdx, VarWindV)
|
||||
upperV := component(levelIdx+1, VarWindV)
|
||||
lowerU := sample(levelIdx, VarWindU)
|
||||
upperU := sample(levelIdx+1, VarWindU)
|
||||
lowerV := sample(levelIdx, VarWindV)
|
||||
upperV := sample(levelIdx+1, VarWindV)
|
||||
|
||||
return weather.Sample{
|
||||
U: lowerU*altFrac + upperU*(1-altFrac),
|
||||
|
|
|
|||
69
internal/weather/gfs/wind_test.go
Normal file
69
internal/weather/gfs/wind_test.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
package gfs
|
||||
|
||||
import (
|
||||
"math"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// testVariant is a tiny cube (2 hours × 3 levels × 3 lat × 4 lng) used to
|
||||
// exercise the sampler without allocating a multi-gigabyte real dataset.
|
||||
func testVariant() *Variant {
|
||||
return &Variant{
|
||||
ID: "gfs-test",
|
||||
ResToken: "test",
|
||||
Resolution: 90, // 180/90+1 = 3 lats, 360/90 = 4 lngs
|
||||
HourStep: 3,
|
||||
MaxHour: 3, // 2 hours
|
||||
Pressures: []int{1000, 500, 100},
|
||||
PressuresPgrb2: []int{1000, 500, 100},
|
||||
PressuresPgrb2b: []int{},
|
||||
}
|
||||
}
|
||||
|
||||
func TestWindSampler(t *testing.T) {
|
||||
v := testVariant()
|
||||
path := filepath.Join(t.TempDir(), "cube.bin")
|
||||
f, err := Create(path, v)
|
||||
if err != nil {
|
||||
t.Fatalf("Create: %v", err)
|
||||
}
|
||||
|
||||
// HGT increases with level so the altitude bisection has a gradient;
|
||||
// U and V are constant so interpolation must return them exactly.
|
||||
for h := range v.NumHours() {
|
||||
for lvl := range v.NumLevels() {
|
||||
for la := range v.NumLatitudes() {
|
||||
for ln := range v.NumLongitudes() {
|
||||
f.SetVal(h, lvl, VarHeight, la, ln, float32(lvl*1000))
|
||||
f.SetVal(h, lvl, VarWindU, la, ln, 7)
|
||||
f.SetVal(h, lvl, VarWindV, la, ln, 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
f.Flush()
|
||||
f.Close()
|
||||
|
||||
epoch := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
rf, err := Open(path, v, epoch)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
defer rf.Close()
|
||||
w := NewWind(rf)
|
||||
|
||||
// Query at the dataset epoch, equator, lng 45, altitude 500m (between
|
||||
// level 0 @ 0m and level 1 @ 1000m).
|
||||
s, err := w.Wind(float64(epoch.Unix()), 0, 45, 500)
|
||||
if err != nil {
|
||||
t.Fatalf("Wind: %v", err)
|
||||
}
|
||||
if math.Abs(s.U-7) > 1e-5 || math.Abs(s.V-3) > 1e-5 {
|
||||
t.Errorf("constant wind not recovered: got U=%v V=%v, want 7,3", s.U, s.V)
|
||||
}
|
||||
if s.AboveModel {
|
||||
t.Errorf("AboveModel should be false at altitude within model range")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue