feat(decart): interp
This commit is contained in:
parent
1bd9143186
commit
19557f3a62
14 changed files with 681 additions and 208 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue