feat: polish & windviz & deploy
This commit is contained in:
parent
81b8e763bd
commit
465ad00f7b
78 changed files with 20622 additions and 2154 deletions
|
|
@ -1,57 +1,46 @@
|
|||
package numerics
|
||||
|
||||
// VecAdd computes y + k*dy on the domain state type S.
|
||||
// Any coordinate-wrap or other domain-specific operation lives here.
|
||||
type VecAdd[S any] func(y S, k float64, dy S) S
|
||||
// Field returns the time derivative of a geographic state at (t, y).
|
||||
// The derivative is direction-independent; the integrator applies the sign
|
||||
// of dt for reverse-time integration.
|
||||
type Field func(t float64, y GeoVec) GeoVec
|
||||
|
||||
// VecLerp computes (1-l)*a + l*b on the domain state type S.
|
||||
type VecLerp[S any] func(a, b S, l float64) S
|
||||
|
||||
// Deriv computes the time derivative of state.
|
||||
type Deriv[S any] func(t float64, y S) S
|
||||
|
||||
// Trigger reports whether a termination condition holds at (t, y).
|
||||
type Trigger[S any] func(t float64, y S) bool
|
||||
// Crossed reports whether a termination condition holds at (t, y).
|
||||
type Crossed func(t float64, y GeoVec) bool
|
||||
|
||||
// RK4Step performs one classical Runge-Kutta-4 step from (t, y) with step dt.
|
||||
// dt may be negative to integrate backwards in time.
|
||||
func RK4Step[S any](t float64, y S, dt float64, deriv Deriv[S], add VecAdd[S]) S {
|
||||
k1 := deriv(t, y)
|
||||
k2 := deriv(t+dt/2, add(y, dt/2, k1))
|
||||
k3 := deriv(t+dt/2, add(y, dt/2, k2))
|
||||
k4 := deriv(t+dt, add(y, dt, k3))
|
||||
// dt may be negative to integrate backwards in time. Longitude wrapping is
|
||||
// applied at every intermediate add via GeoAdd, matching the reference
|
||||
// integrator. The function performs no heap allocation.
|
||||
func RK4Step(t float64, y GeoVec, dt float64, f Field) GeoVec {
|
||||
half := dt / 2
|
||||
k1 := f(t, y)
|
||||
k2 := f(t+half, GeoAdd(y, half, k1))
|
||||
k3 := f(t+half, GeoAdd(y, half, k2))
|
||||
k4 := f(t+dt, GeoAdd(y, dt, k3))
|
||||
|
||||
y2 := y
|
||||
y2 = add(y2, dt/6, k1)
|
||||
y2 = add(y2, dt/3, k2)
|
||||
y2 = add(y2, dt/3, k3)
|
||||
y2 = add(y2, dt/6, k4)
|
||||
y2 := GeoAdd(y, dt/6, k1)
|
||||
y2 = GeoAdd(y2, dt/3, k2)
|
||||
y2 = GeoAdd(y2, dt/3, k3)
|
||||
y2 = GeoAdd(y2, dt/6, k4)
|
||||
return y2
|
||||
}
|
||||
|
||||
// RefineTrigger locates the trigger point between (t1, y1) (trigger not fired)
|
||||
// and (t2, y2) (trigger fired) via binary search in the linear-interpolation
|
||||
// parameter space, stopping when the parameter interval is narrower than tol.
|
||||
// RefineCrossing locates a crossing between (t1, y1) (not crossed) and
|
||||
// (t2, y2) (crossed) by binary search in the linear-interpolation parameter
|
||||
// space, stopping when the parameter interval is narrower than tol.
|
||||
//
|
||||
// Returns the final midpoint sampled, matching the behavior of Tawhiri's
|
||||
// solver.pyx (the returned point is *not* guaranteed to satisfy the trigger;
|
||||
// for tol << 1 the difference is at most one tolerance-width either side).
|
||||
func RefineTrigger[S any](
|
||||
t1 float64, y1 S,
|
||||
t2 float64, y2 S,
|
||||
trigger Trigger[S],
|
||||
lerp VecLerp[S],
|
||||
tol float64,
|
||||
) (float64, S) {
|
||||
// It returns the final midpoint sampled, matching Tawhiri's solver.pyx: the
|
||||
// returned point is not guaranteed to satisfy the predicate, but for tol << 1
|
||||
// it is within one tolerance-width of the true crossing.
|
||||
func RefineCrossing(t1 float64, y1 GeoVec, t2 float64, y2 GeoVec, crossed Crossed, tol float64) (float64, GeoVec) {
|
||||
left, right := 0.0, 1.0
|
||||
t3 := t2
|
||||
y3 := y2
|
||||
|
||||
t3, y3 := t2, y2
|
||||
for right-left > tol {
|
||||
mid := (left + right) / 2
|
||||
t3 = Lerp(t1, t2, mid)
|
||||
y3 = lerp(y1, y2, mid)
|
||||
if trigger(t3, y3) {
|
||||
y3 = GeoLerp(y1, y2, mid)
|
||||
if crossed(t3, y3) {
|
||||
right = mid
|
||||
} else {
|
||||
left = mid
|
||||
|
|
@ -59,3 +48,47 @@ func RefineTrigger[S any](
|
|||
}
|
||||
return t3, y3
|
||||
}
|
||||
|
||||
// Path is a struct-of-arrays trajectory: parallel slices of time and the
|
||||
// three state components. SoA layout keeps each component contiguous, which
|
||||
// is friendlier to cache and to vectorised post-processing than a slice of
|
||||
// point structs, and lets the integrator append with a single bounds check
|
||||
// per component.
|
||||
type Path struct {
|
||||
T []float64
|
||||
Lat []float64
|
||||
Lng []float64
|
||||
Altitude []float64
|
||||
}
|
||||
|
||||
// NewPath returns a Path with capacity reserved for n points.
|
||||
func NewPath(n int) Path {
|
||||
return Path{
|
||||
T: make([]float64, 0, n),
|
||||
Lat: make([]float64, 0, n),
|
||||
Lng: make([]float64, 0, n),
|
||||
Altitude: make([]float64, 0, n),
|
||||
}
|
||||
}
|
||||
|
||||
// Len returns the number of points in the path.
|
||||
func (p *Path) Len() int { return len(p.T) }
|
||||
|
||||
// Append adds one point to the path.
|
||||
func (p *Path) Append(t float64, y GeoVec) {
|
||||
p.T = append(p.T, t)
|
||||
p.Lat = append(p.Lat, y.Lat)
|
||||
p.Lng = append(p.Lng, y.Lng)
|
||||
p.Altitude = append(p.Altitude, y.Altitude)
|
||||
}
|
||||
|
||||
// Last returns the final (t, state) of the path. It panics on an empty path.
|
||||
func (p *Path) Last() (float64, GeoVec) {
|
||||
i := len(p.T) - 1
|
||||
return p.T[i], GeoVec{Lat: p.Lat[i], Lng: p.Lng[i], Altitude: p.Altitude[i]}
|
||||
}
|
||||
|
||||
// At returns the point at index i.
|
||||
func (p *Path) At(i int) (float64, GeoVec) {
|
||||
return p.T[i], GeoVec{Lat: p.Lat[i], Lng: p.Lng[i], Altitude: p.Altitude[i]}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue