forked from gsn/predictor
feat: refactor
This commit is contained in:
parent
82ef1cb3b8
commit
51bbf3c579
44 changed files with 8589 additions and 0 deletions
153
internal/prediction/interpolate.go
Normal file
153
internal/prediction/interpolate.go
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
package prediction
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"predictor-refactored/internal/dataset"
|
||||
)
|
||||
|
||||
// Exact port of the reference interpolation logic (interpolate.pyx).
|
||||
// 4D interpolation: time, latitude, longitude, altitude (via geopotential height).
|
||||
|
||||
// lerp1 holds an index and interpolation weight for one axis.
|
||||
type lerp1 struct {
|
||||
index int
|
||||
lerp float64
|
||||
}
|
||||
|
||||
// lerp3 holds indices and a combined weight for the (hour, lat, lon) axes.
|
||||
type lerp3 struct {
|
||||
hour, lat, lng int
|
||||
lerp float64
|
||||
}
|
||||
|
||||
// RangeError indicates a coordinate is outside the dataset bounds.
|
||||
type RangeError struct {
|
||||
Variable string
|
||||
Value float64
|
||||
}
|
||||
|
||||
func (e *RangeError) Error() string {
|
||||
return fmt.Sprintf("%s=%f out of range", e.Variable, e.Value)
|
||||
}
|
||||
|
||||
// pick computes interpolation indices and weights for a single axis.
|
||||
// left: axis start, step: axis spacing, n: number of points, value: query value.
|
||||
// Returns two lerp1 values (lower and upper bracket).
|
||||
func pick(left, step float64, n int, value float64, variableName string) ([2]lerp1, error) {
|
||||
a := (value - left) / step
|
||||
b := int(a) // truncation toward zero, same as Cython <long> cast
|
||||
if b < 0 || b >= n-1 {
|
||||
return [2]lerp1{}, &RangeError{Variable: variableName, Value: value}
|
||||
}
|
||||
l := a - float64(b)
|
||||
return [2]lerp1{
|
||||
{index: b, lerp: 1 - l},
|
||||
{index: b + 1, lerp: l},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// pick3 computes 8 trilinear interpolation weights for (hour, lat, lng).
|
||||
func pick3(hour, lat, lng float64) ([8]lerp3, error) {
|
||||
lhour, err := pick(0, 3, 65, hour, "hour")
|
||||
if err != nil {
|
||||
return [8]lerp3{}, err
|
||||
}
|
||||
llat, err := pick(-90, 0.5, 361, lat, "lat")
|
||||
if err != nil {
|
||||
return [8]lerp3{}, err
|
||||
}
|
||||
// Longitude wraps: tell pick the axis is one larger, then wrap index 720 → 0
|
||||
llng, err := pick(0, 0.5, 720+1, lng, "lng")
|
||||
if err != nil {
|
||||
return [8]lerp3{}, err
|
||||
}
|
||||
if llng[1].index == 720 {
|
||||
llng[1].index = 0
|
||||
}
|
||||
|
||||
var out [8]lerp3
|
||||
i := 0
|
||||
for _, a := range lhour {
|
||||
for _, b := range llat {
|
||||
for _, c := range llng {
|
||||
out[i] = lerp3{
|
||||
hour: a.index,
|
||||
lat: b.index,
|
||||
lng: c.index,
|
||||
lerp: a.lerp * b.lerp * c.lerp,
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// interp3 performs 8-point weighted interpolation at a given variable and pressure level.
|
||||
func interp3(ds *dataset.File, lerps [8]lerp3, variable, level int) float64 {
|
||||
var r float64
|
||||
for i := 0; i < 8; i++ {
|
||||
v := ds.Val(lerps[i].hour, level, variable, lerps[i].lat, lerps[i].lng)
|
||||
r += float64(v) * lerps[i].lerp
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// search finds the largest pressure level index where interpolated geopotential
|
||||
// height is less than the target altitude. Searches levels 0..45 (excludes topmost).
|
||||
func search(ds *dataset.File, lerps [8]lerp3, target float64) int {
|
||||
lower, upper := 0, 45
|
||||
|
||||
for lower < upper {
|
||||
mid := (lower + upper + 1) / 2
|
||||
test := interp3(ds, lerps, dataset.VarHeight, mid)
|
||||
if target <= test {
|
||||
upper = mid - 1
|
||||
} else {
|
||||
lower = mid
|
||||
}
|
||||
}
|
||||
|
||||
return lower
|
||||
}
|
||||
|
||||
// interp4 performs altitude-interpolated wind lookup using two bracketing levels.
|
||||
func interp4(ds *dataset.File, lerps [8]lerp3, altLerp lerp1, variable int) float64 {
|
||||
lower := interp3(ds, lerps, variable, altLerp.index)
|
||||
upper := interp3(ds, lerps, variable, altLerp.index+1)
|
||||
return lower*altLerp.lerp + upper*(1-altLerp.lerp)
|
||||
}
|
||||
|
||||
// GetWind returns interpolated (u, v) wind components for the given position.
|
||||
// hour: fractional hours since dataset start.
|
||||
// lat: latitude in degrees (-90 to +90).
|
||||
// lng: longitude in degrees (0 to 360).
|
||||
// alt: altitude in metres above sea level.
|
||||
func GetWind(ds *dataset.File, warnings *Warnings, hour, lat, lng, alt float64) (u, v float64, err error) {
|
||||
lerps, err := pick3(hour, lat, lng)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
altidx := search(ds, lerps, alt)
|
||||
lower := interp3(ds, lerps, dataset.VarHeight, altidx)
|
||||
upper := interp3(ds, lerps, dataset.VarHeight, altidx+1)
|
||||
|
||||
var altLerp float64
|
||||
if lower != upper {
|
||||
altLerp = (upper - alt) / (upper - lower)
|
||||
} else {
|
||||
altLerp = 0.5
|
||||
}
|
||||
|
||||
if altLerp < 0 {
|
||||
warnings.AltitudeTooHigh.Add(1)
|
||||
}
|
||||
|
||||
alt1 := lerp1{index: altidx, lerp: altLerp}
|
||||
u = interp4(ds, lerps, alt1, dataset.VarWindU)
|
||||
v = interp4(ds, lerps, alt1, dataset.VarWindV)
|
||||
|
||||
return u, v, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue