predictor/internal/weather/gfs/wind.go
2026-05-23 00:55:35 +09:00

113 lines
2.9 KiB
Go

package gfs
import (
"time"
"predictor-refactored/internal/numerics"
"predictor-refactored/internal/weather"
)
// Wind is a WindField backed by a GFS dataset file.
type Wind struct {
file *File
hourAxis numerics.Axis
latAxis numerics.Axis
lngAxis numerics.Axis
}
// NewWind returns a Wind backed by file. The axes are constructed from the
// file's variant geometry.
func NewWind(file *File) *Wind {
v := file.variant
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",
},
}
}
// Epoch returns the forecast run time of the underlying file.
func (w *Wind) Epoch() time.Time { return w.file.Epoch }
// Source returns the variant ID (e.g. "gfs-0p50-3h").
func (w *Wind) Source() string { return w.file.variant.ID }
// Close releases the underlying file's resources.
func (w *Wind) Close() error { return w.file.Close() }
// Wind samples the field at the given UNIX time, geographic coordinate, and
// altitude. Vertical interpolation matches Tawhiri: locate the two pressure
// levels whose interpolated geopotential heights bracket alt, then linearly
// interpolate U and V between them.
func (w *Wind) Wind(t, lat, lng, alt float64) (weather.Sample, error) {
hours := (t - float64(w.file.Epoch.Unix())) / 3600.0
bh, err := w.hourAxis.Locate(hours)
if err != nil {
return weather.Sample{}, err
}
bla, err := w.latAxis.Locate(lat)
if err != nil {
return weather.Sample{}, err
}
bln, err := w.lngAxis.Locate(lng)
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))
}
}
levelIdx := numerics.Bisect(0, w.file.variant.NumLevels()-2, alt, func(level int) float64 {
return numerics.EvalTrilinear(bs, height(level))
})
lowerHGT := numerics.EvalTrilinear(bs, height(levelIdx))
upperHGT := numerics.EvalTrilinear(bs, height(levelIdx+1))
var altFrac float64
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)
return weather.Sample{
U: lowerU*altFrac + upperU*(1-altFrac),
V: lowerV*altFrac + upperV*(1-altFrac),
AboveModel: altFrac < 0,
}, nil
}