feat(decart): interp

This commit is contained in:
gili8420 2026-08-03 22:10:10 +09:00
parent 1bd9143186
commit 19557f3a62
14 changed files with 681 additions and 208 deletions

View file

@ -74,25 +74,66 @@ The contribution at time $t$ is
\]
\paragraph{Wind transport.} The horizontal contribution from sampling the
loaded wind field $W$:
loaded wind field $W$ is the wind itself:
\[
\mathbf{F}_{\text{wind}}(t, \mathbf{s}) = \Bigl(
\frac{180}{\pi}\,\frac{v}{R + h},\;\;
\frac{180}{\pi}\,\frac{u}{(R + h)\cos\bigl(\varphi\,\pi/180\bigr)},\;\;
0
\Bigr),
\mathbf{F}_{\text{wind}}(t, \mathbf{s}) = (u,\; v,\; 0),
\qquad (u, v) = W(t, \varphi, \lambda, h),
\]
where $(u, v) = W(t, \varphi, \lambda, h)$ are the eastward and northward
wind components in metres per second, and $R = 6{,}371{,}009$~m is the
spherical Earth radius. The implementation lives in
in metres per second east and north. No conversion is performed.
Earlier revisions converted this to degrees per second, which introduced a
$1/\cos\varphi$ factor in longitude. That factor diverges at the poles: for
$u = 10$~m/s it grows from $8.95\times10^{-5}$~deg/s at the equator to
$0.513$~deg/s at $\varphi = 89.99^\circ$ and $1.46\times10^{12}$~deg/s at
$\varphi = 90^\circ$ (large but finite, since $\cos(\pi/2)$ evaluates to
$6.12\times10^{-17}$ in double precision rather than to zero). Against a fixed
step this made the integrator meaningless near the poles. The factor is now
absent from the formulation rather than guarded against; see
section~\ref{sec:geostep}. The implementation lives in
\verb|engine.WindTransport| (\verb|engine/models.go|).
\paragraph{Coordinate system.} The model is a spherical Earth in
plate-carrée (latitude/longitude/altitude) coordinates. This matches the
reference Tawhiri predictor exactly and is necessary for bit-identical
back-to-back testing. A WGS84/ECEF variant is planned but deferred: it
would require converting U/V wind components from the GFS sphere model
to the ellipsoid, which is not a trivial coordinate transform.
\paragraph{Coordinate system.} The model is a spherical Earth. State is
still carried as $(\varphi, \lambda, h)$ in degrees and metres, because
constraints, path recording and the REST API all speak latitude and
longitude --- but motion is \emph{not} integrated in those coordinates.
Displacement is applied by rotating the position vector along a great
circle (section~\ref{sec:geostep}), so no longitude derivative is ever
formed and there is no coordinate singularity at the poles. Latitude also
cannot leave $[-90, 90]$, since it is read back from a unit vector instead
of accumulated.
This is a deliberate departure from the reference Tawhiri predictor, which
integrates in plate-carrée coordinates. Outputs are therefore no longer
bit-identical to it. The measured cost is small: on GFS
\verb|2026-08-03T00:00:00Z|, launch $89^\circ$N $68^\circ$E, burst
25~km --- a 114~km flight --- the two integrators differ by at most
$1.608$~m along the track and $0.058$~m at the landing point. That is the
order of the truncation error, and far below the representation error of
the wind data itself. Back-to-back testing against Tawhiri is retained as
an agreement-within-tolerance check rather than an equality check, run with
\verb|cmd/compare-tawhiri| and its \verb|-align-dataset| flag (on by default),
which asks the hosted service to use the local predictor's GFS run --- without
it the two sides silently compare different weather. Note the hosted service
retains only recent runs, so alignment fails once the local dataset ages out.
Measured on GFS \verb|2026-08-03T06:00:00Z|, burst 25~km:
\begin{center}
\begin{tabular}{lrrrr}
launch & burst $\Delta$ & landing $\Delta$ & apex alt $\Delta$ & land alt $\Delta$ \\
\hline
$52.2^\circ$N $0.1^\circ$E & 1~m & 60~m & 0~m & 47~m \\
$89^\circ$N $68^\circ$E & 2~m & 0~m & 0~m & 0~m \\
\end{tabular}
\end{center}
The polar launch agrees exactly. The 60~m at mid-latitude is not integrator
error: it follows from the 47~m difference in termination altitude, because the
reference has the ruaumoko elevation dataset and terminates on terrain while
this deployment has none and terminates at sea level. At $89^\circ$N the
surface is sea ice, so sea level is the terrain and the difference vanishes.
A WGS84 ellipsoid variant remains deferred: it would require converting
U/V wind components from the GFS sphere model to the ellipsoid, which is
not a trivial coordinate transform.
% =========================================================================
\section{Profiles and propagators}
@ -181,12 +222,29 @@ $x_i = \ell + i \cdot s$ for $i = 0, 1, \ldots, N - 1$, parameterised by
the left edge $\ell$, the step $s > 0$, and the point count $N$.
Given a query $v$, the \emph{bracket} is the pair $(i_0, i_1)$ with
$x_{i_0} \le v < x_{i_1}$ and the dimensionless position
$x_{i_0} \le v \le x_{i_1}$ and the dimensionless position
\[
f = \frac{v - x_{i_0}}{s} \in [0, 1).
f = \frac{v - x_{i_0}}{s} \in [0, 1].
\]
Implemented as \verb|Axis.Locate| in \verb|internal/numerics/grid.go|.
\paragraph{Both ends are closed.} The accepted range is
$[\ell, \ell + (N-1)s]$, and the upper end resolves to the last cell at
$f = 1$ rather than opening a cell with no neighbour above it. This matters
on the latitude axis, where $\ell + (N-1)s = 90^\circ$ is the north pole:
that row carries real data --- NCEP resolves the GFS pole row per longitude
--- so it is a usable grid row like any other. While the upper end was open,
sampling exactly $90^\circ$ returned an error; the wind model discarded that
error and returned a zero rate, so a prediction launched at the pole froze in
place, and the wind-field endpoint reported a row of calm where a 29~m/s flow
was blowing. The same argument applies to the last forecast hour and the
topmost pressure level.
Bound-checking is done on $p = (v - \ell)/s$ before truncation. Testing the
truncated index instead admitted values just below $\ell$, because Go's
\verb|int()| truncates toward zero: $p = -0{.}002$ became index $0$ and
extrapolated off the end of the axis.
\paragraph{Wrapping axes.} For periodic axes (e.g.\ longitude), the
sequence is extended by the convention $x_N = x_0$ so a value approaching
$x_N$ from below brackets $(N{-}1, 0)$ with fraction
@ -194,7 +252,8 @@ $f = (v - x_{N-1})/s$.
\paragraph{Worked example.} Latitude axis with $\ell = -90$, $s = 0{.}5$,
$N = 361$. Query $v = -89{.}75$ yields $p = 0{.}5$, so $i_0 = 0$,
$i_1 = 1$, $f = 0{.}5$.
$i_1 = 1$, $f = 0{.}5$. Query $v = 90$ yields $p = 360$, which clamps to
$i_0 = 359$, $i_1 = 360$, $f = 1$ --- the north pole row at full weight.
\subsection{Multilinear interpolation}
@ -238,8 +297,56 @@ and step $\Delta t$, \verb|RK4Step| applies
\end{aligned}
\]
Reverse-time integration uses $\Delta t < 0$ unchanged; the implementation
contains no branch on the sign of $\Delta t$. Domain-specific vector
arithmetic (longitude wrap) is injected via \verb|VecAdd|.
contains no branch on the sign of $\Delta t$.
\paragraph{Stage combination on the sphere.} The additions above are not
performed in $(\varphi, \lambda, h)$. Each stage rate is a velocity in the
local horizontal frame at \emph{its own} evaluation point, and that frame
rotates from one stage to the next --- near a pole, fast enough that
averaging east/north components directly would reintroduce the error this
formulation exists to remove. The stages are therefore converted to
earth-centred vectors, combined with the usual $\tfrac16(1,2,2,1)$
weights, read back in the frame at the starting point (which also discards
the small radial component the averaging introduces), and applied as a
single great-circle step:
\[
\bar{\mathbf V} = \sum_i w_i \bigl(k_i^{E}\,\hat{\mathbf e}_i + k_i^{N}\,\hat{\mathbf n}_i\bigr),
\qquad
y(t + \Delta t) = \mathrm{GeoStep}\bigl(y,\; \bar{\mathbf V},\; \Delta t\bigr).
\]
\subsection{Great-circle stepping}
\label{sec:geostep}
\verb|GeoStep| advances a position by a horizontal velocity
$(u, v)$ and a vertical rate $w$ over $\Delta t$. With
$\hat{\mathbf r}, \hat{\mathbf e}, \hat{\mathbf n}$ the outward radial,
east and north unit vectors at $(\varphi, \lambda)$, speed
$V = \sqrt{u^2 + v^2}$ and unit travel direction
$\hat{\mathbf t} = (u\,\hat{\mathbf e} + v\,\hat{\mathbf n})/V$:
\[
\delta = \frac{V \Delta t}{R + h},
\qquad
\hat{\mathbf r}' = \hat{\mathbf r}\cos\delta + \hat{\mathbf t}\sin\delta,
\qquad
h' = h + w\,\Delta t,
\]
and $(\varphi', \lambda')$ are read back from $\hat{\mathbf r}'$ via
$\varphi' = \arcsin r'_z$, $\lambda' = \operatorname{atan2}(r'_y, r'_x)$.
The step is exact for a constant rate. Because $\hat{\mathbf t}$ is
orthogonal to $\hat{\mathbf r}$ by construction, $\hat{\mathbf r}'$ stays
on the unit sphere without renormalisation. All three basis vectors are
unit length at every latitude including the poles; what happens at a pole
is not a degeneracy but a genuine ambiguity, since east and north there
depend on which meridian $\lambda$ names. That matches the data --- NCEP
resolves the GFS pole row per longitude for exactly this reason, so any
choice yields the same physical vector.
A step that reaches a pole simply continues down the far side and
$\lambda$ picks up $180^\circ$ on its own, with no special case and no
threshold latitude. This is asserted directly in
\verb|numerics/spherical_test.go|.
\subsection{Termination refinement}
@ -335,13 +442,15 @@ arbitrary State types via generics in numerics; the engine could lift
its State to $(\mathbf{s}, \mathbf{v}_p)$ for a future mass-aware
propagator without breaking the existing models.
\paragraph{Coordinate system upgrades.} Migrating to WGS84/ECEF would
remove the cosine factor in the horizontal wind transport equation and
make distances metric directly. GFS itself uses a spherical Earth; the
wind components are not directly portable. A clean implementation
provides a coordinate-system parameter on the profile request; for now,
the spherical model is used uniformly so that outputs remain bit
identical to the upstream Tawhiri.
\paragraph{Coordinate system upgrades.} The cosine factor is no longer a
deferral: horizontal motion is integrated by great-circle rotation and the
$1/\cos\varphi$ term is gone from the formulation entirely. What remains
deferred is the \emph{ellipsoid}: migrating from a spherical Earth to
WGS84 would make distances metric directly, but GFS itself uses a
spherical Earth and its wind components are not directly portable to the
ellipsoid. A clean implementation would provide a coordinate-system
parameter on the profile request; for now the spherical model is used
uniformly.
\paragraph{Monte Carlo.} GEFS already provides 21 ensemble members per
epoch. A Monte Carlo prediction would sample $K$ trajectories per