129 lines
4.1 KiB
Go
129 lines
4.1 KiB
Go
package gfs
|
|
|
|
import (
|
|
"time"
|
|
|
|
"predictor-refactored/internal/numerics"
|
|
"predictor-refactored/internal/weather"
|
|
)
|
|
|
|
// 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. 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: 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),
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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 sample(level, VarHeight)
|
|
})
|
|
|
|
lowerHGT := sample(levelIdx, VarHeight)
|
|
upperHGT := sample(levelIdx+1, VarHeight)
|
|
|
|
altFrac := 0.5
|
|
if lowerHGT != upperHGT {
|
|
altFrac = (upperHGT - alt) / (upperHGT - lowerHGT)
|
|
}
|
|
|
|
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),
|
|
V: lowerV*altFrac + upperV*(1-altFrac),
|
|
AboveModel: altFrac < 0,
|
|
}, nil
|
|
}
|