109 lines
2.7 KiB
Go
109 lines
2.7 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
|
|
}
|
|
|
|
// NewWind returns a Wind backed by file.
|
|
func NewWind(file *File) *Wind {
|
|
return &Wind{file: file}
|
|
}
|
|
|
|
// Epoch returns the forecast run time of the underlying file.
|
|
func (w *Wind) Epoch() time.Time { return w.file.Epoch }
|
|
|
|
// Source returns the source identifier "noaa-gfs-0p50".
|
|
func (w *Wind) Source() string { return "noaa-gfs-0p50" }
|
|
|
|
// Close releases the underlying file's resources.
|
|
func (w *Wind) Close() error { return w.file.Close() }
|
|
|
|
// Grid axes for the GFS 0.5-degree dataset.
|
|
var (
|
|
hourAxis = numerics.Axis{
|
|
Left: 0,
|
|
Step: float64(HourStep),
|
|
N: NumHours,
|
|
Name: "hour",
|
|
}
|
|
latAxis = numerics.Axis{
|
|
Left: LatStart,
|
|
Step: Resolution,
|
|
N: NumLatitudes,
|
|
Name: "lat",
|
|
}
|
|
lngAxis = numerics.Axis{
|
|
Left: LonStart,
|
|
Step: Resolution,
|
|
N: NumLongitudes,
|
|
Wrap: true,
|
|
Name: "lng",
|
|
}
|
|
)
|
|
|
|
// 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 := hourAxis.Locate(hours)
|
|
if err != nil {
|
|
return weather.Sample{}, err
|
|
}
|
|
bla, err := latAxis.Locate(lat)
|
|
if err != nil {
|
|
return weather.Sample{}, err
|
|
}
|
|
bln, err := 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, 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
|
|
}
|