474 lines
21 KiB
TeX
474 lines
21 KiB
TeX
\documentclass[a4paper,11pt]{article}
|
|
\usepackage[margin=1in]{geometry}
|
|
\usepackage{amsmath, amssymb}
|
|
\usepackage{algorithm, algpseudocode}
|
|
\usepackage{hyperref}
|
|
|
|
\title{stratoflights-predictor: Mathematical Reference}
|
|
\author{stratoflights-predictor}
|
|
\date{}
|
|
|
|
\begin{document}
|
|
\maketitle
|
|
|
|
\noindent This document is the end-to-end mathematical specification of the
|
|
trajectory calculation performed by stratoflights-predictor. It is meant
|
|
to be detailed enough to permit hand verification of the numerical
|
|
output. Section~\ref{sec:numerics} covers the small numerics library
|
|
(\verb|internal/numerics|); the remaining sections describe the engine
|
|
(\verb|internal/engine|) and the data plane.
|
|
|
|
\tableofcontents
|
|
|
|
% =========================================================================
|
|
\section{State vector and equations of motion}
|
|
\label{sec:state}
|
|
|
|
\paragraph{State vector.} The balloon state is the four-tuple
|
|
\[
|
|
\mathbf{s}(t) \;=\; \bigl(t,\; \varphi(t),\; \lambda(t),\; h(t)\bigr),
|
|
\]
|
|
where $t$ is UNIX seconds, $\varphi \in [-90, 90]$ is geographic latitude
|
|
in degrees, $\lambda \in [0, 360)$ is geographic longitude in degrees,
|
|
and $h$ is altitude above mean sea level in metres. Inside the
|
|
implementation, the spatial part $(\varphi, \lambda, h)$ is the
|
|
\verb|engine.State| struct; $t$ is tracked separately by the integrator.
|
|
|
|
\paragraph{Equations of motion.} The time derivative of state is the
|
|
direction-agnostic vector
|
|
\[
|
|
\dot{\mathbf{s}}(t) \;=\; \bigl(\dot \varphi,\; \dot \lambda,\; \dot h\bigr)
|
|
\;=\; \sum_{m \in \text{Models}(t)} \mathbf{F}_m(t, \mathbf{s}),
|
|
\]
|
|
i.e.\ the sum of the active stage's models evaluated at the current
|
|
state. The supported per-model contributions are:
|
|
|
|
\paragraph{Constant rate.} A purely vertical model with no horizontal
|
|
component, used for the standard balloon ascent profile:
|
|
\[
|
|
\mathbf{F}_{\text{const}}(t, \mathbf{s}) = (0, 0, r), \qquad r \in \mathbb{R}.
|
|
\]
|
|
Positive $r$ is upward; a negative $r$ may be combined with reverse-time
|
|
integration to model an ascent backwards from a known apex.
|
|
|
|
\paragraph{Parachute descent.} The vertical contribution under a constant
|
|
drag coefficient with the NASA atmosphere model density $\rho(h)$:
|
|
\[
|
|
\mathbf{F}_{\text{drag}}(t, \mathbf{s}) = \Bigl(0, 0, -\frac{k}{\sqrt{\rho(h)}}\Bigr),
|
|
\qquad k = r_0 \cdot 1{.}1045,
|
|
\]
|
|
where $r_0$ is the descent rate at sea level. Density is computed
|
|
piecewise from the layered model described in
|
|
\href{https://www.grc.nasa.gov/WWW/K-12/airplane/atmosmet.html}{NASA's
|
|
atmosphere page}.
|
|
|
|
\paragraph{Piecewise rate.} A schedule $\{(\tau_i, r_i)\}_{i=1}^N$
|
|
parameterised in either absolute UNIX time, or seconds since
|
|
profile start, or seconds since the propagator's own start. Resolution
|
|
happens lazily through the \verb|Propagator.BuildModel| hook so the same
|
|
spec can be reused across profiles with different launch times.
|
|
The contribution at time $t$ is
|
|
\[
|
|
\mathbf{F}_{\text{pwc}}(t, \mathbf{s}) = (0, 0, r_{i^\star}),
|
|
\qquad i^\star = \min\{i : \tau_i > t\}.
|
|
\]
|
|
|
|
\paragraph{Wind transport.} The horizontal contribution from sampling the
|
|
loaded wind field $W$ is the wind itself:
|
|
\[
|
|
\mathbf{F}_{\text{wind}}(t, \mathbf{s}) = (u,\; v,\; 0),
|
|
\qquad (u, v) = W(t, \varphi, \lambda, h),
|
|
\]
|
|
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. 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}
|
|
\label{sec:profile}
|
|
|
|
\paragraph{Propagator.} A propagator owns one Model and a list of
|
|
Constraints; it produces a sequence of trajectory points via classical
|
|
Runge--Kutta--4 integration with step $\Delta t$ (positive for forward,
|
|
negative for reverse propagation):
|
|
\[
|
|
\Pi : (t_0, \mathbf{s}_0) \;\longmapsto\; \bigl[(t_k, \mathbf{s}_k)\bigr]_{k=0}^{K}.
|
|
\]
|
|
The sequence ends at the first $k$ where any constraint is violated;
|
|
the violation point is refined by binary search (see
|
|
\S\ref{sec:numerics}).
|
|
|
|
\paragraph{Profile.} A profile is an ordered chain of propagators
|
|
$[\Pi_1, \Pi_2, \ldots, \Pi_N]$. Stage $i$ starts where stage $i-1$
|
|
ended; the time direction (sign of $\Delta t$) is shared.
|
|
|
|
\paragraph{Constraint actions.} When a constraint $c$ is violated at the
|
|
refined point $(t^\star, \mathbf{s}^\star)$, $c.\text{Action}$ controls
|
|
the dispatch:
|
|
\begin{itemize}
|
|
\item \texttt{stop} — the profile ends at $(t^\star, \mathbf{s}^\star)$.
|
|
\item \texttt{fallback} — the current propagator hands off to its
|
|
\texttt{Fallback} propagator (chains supported).
|
|
\item \texttt{clip} — the violated coordinate is clipped to the
|
|
constraint's boundary and integration continues. Useful for soft
|
|
constraints such as ``hold altitude above 500~m''.
|
|
\end{itemize}
|
|
Constraints fire on full RK4 steps only, never on intermediate
|
|
sub-evaluations. This matches the reference Tawhiri behaviour
|
|
bit-for-bit.
|
|
|
|
\paragraph{Reverse propagation.} A profile with \verb|Direction = Reverse|
|
|
runs every propagator with $\Delta t = -\Delta t$. Models are
|
|
direction-agnostic: their derivative formulas above hold unchanged. The
|
|
typical use is to start from a known landing point and recover the
|
|
launch position by integrating backwards in time.
|
|
|
|
% =========================================================================
|
|
\section{Constraint geometry}
|
|
\label{sec:constraints}
|
|
|
|
The engine ships four constraint primitives.
|
|
|
|
\paragraph{Scalar comparison: altitude.}
|
|
\(
|
|
c_{\text{alt}}(t, \mathbf{s}) \;=\; h \,\mathrel{\bigotimes}\, h_0,
|
|
\)
|
|
where $\bigotimes \in \{<, \le, >, \ge, =\}$ is the configured operator
|
|
and $h_0$ is the limit (metres). The implementation is
|
|
\verb|engine.Altitude| (\verb|engine/constraints.go|).
|
|
|
|
\paragraph{Scalar comparison: time.} Same shape as altitude, but acting
|
|
on $t$ in UNIX seconds. Implementation: \verb|engine.Time|.
|
|
|
|
\paragraph{Terrain contact.}
|
|
\(
|
|
c_{\text{terr}}(t, \mathbf{s}) = \bigl(z(\varphi, \lambda) > h\bigr),
|
|
\)
|
|
with $z$ provided by the ruaumoko-compatible elevation dataset.
|
|
|
|
\paragraph{Polygon.} For a polygon $P$ with vertices
|
|
$(\varphi_i, \lambda_i)_{i=1}^N$ and mode $\mu \in
|
|
\{\text{inside}, \text{outside}\}$, the constraint is
|
|
$c_{\text{poly}}(\mathbf{s}) = \bigl(\mathbf{s} \in P\bigr) \oplus
|
|
[\mu = \text{outside}]$. Containment is tested by ray casting in
|
|
plate-carrée after normalising every longitude to within 180\textdegree{}
|
|
of the first vertex; this handles antimeridian-crossing edges so long
|
|
as the polygon spans no more than 180\textdegree{} in longitude.
|
|
|
|
% =========================================================================
|
|
\section{Numerics library}
|
|
\label{sec:numerics}
|
|
|
|
The numerics package (\verb|internal/numerics|) provides four primitives:
|
|
regular-grid bracketing, multilinear interpolation, monotone bisection,
|
|
and classical RK4 with termination-point refinement.
|
|
|
|
\subsection{Regular-grid bracketing}
|
|
|
|
\paragraph{Definition.} An \emph{axis} is the regularly-spaced sequence
|
|
$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 \le x_{i_1}$ and the dimensionless position
|
|
\[
|
|
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
|
|
$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$. 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}
|
|
|
|
\paragraph{Definition.} For a scalar field $u$ defined at the grid nodes
|
|
of three axes, the trilinear interpolant at brackets
|
|
$b_a, b_b, b_c$ is
|
|
\[
|
|
\tilde u = \sum_{i, j, k \in \{0, 1\}} w_{a,i} \, w_{b,j} \, w_{c,k}
|
|
\; u\bigl(b_a^i, b_b^j, b_c^k\bigr),
|
|
\]
|
|
where $w_{\bullet, 0} = 1 - f_\bullet$ and $w_{\bullet, 1} = f_\bullet$.
|
|
The corner terms are accumulated in the order
|
|
$(0,0,0), (0,0,1), \ldots, (1,1,1)$, matching the reference Cython
|
|
implementation so that double-precision results agree byte for byte.
|
|
|
|
\paragraph{Linear exactness.} For any affine field
|
|
$u(i, j, k) = \alpha i + \beta j + \gamma k + \delta$, the formula returns
|
|
$\alpha p_a + \beta p_b + \gamma p_c + \delta$ exactly (modulo
|
|
floating-point rounding), where $p_\bullet = b_\bullet^0 + f_\bullet$.
|
|
|
|
\subsection{Monotone bisection}
|
|
|
|
For an integer-indexed monotone non-decreasing sequence
|
|
$f : \{i_{\min}, \ldots, i_{\max}\} \to \mathbb{R}$ and a target $t$,
|
|
\verb|Bisect| returns the largest index $i^\star$ with $f(i^\star) < t$.
|
|
Used by the wind sampler to locate the pressure level bracketing the
|
|
query altitude. Time complexity:
|
|
$\mathcal{O}(\log(i_{\max} - i_{\min}))$.
|
|
|
|
\subsection{Classical RK4}
|
|
|
|
\paragraph{Definition.} For a state $y$, derivative $\dot y = f(t, y)$,
|
|
and step $\Delta t$, \verb|RK4Step| applies
|
|
\[
|
|
\begin{aligned}
|
|
k_1 &= f(t, y), \\
|
|
k_2 &= f\bigl(t + \tfrac{\Delta t}{2}, \; y + \tfrac{\Delta t}{2} k_1\bigr), \\
|
|
k_3 &= f\bigl(t + \tfrac{\Delta t}{2}, \; y + \tfrac{\Delta t}{2} k_2\bigr), \\
|
|
k_4 &= f\bigl(t + \Delta t, \; y + \Delta t \cdot k_3\bigr), \\
|
|
y(t + \Delta t) &= y + \tfrac{\Delta t}{6}\bigl(k_1 + 2 k_2 + 2 k_3 + k_4\bigr).
|
|
\end{aligned}
|
|
\]
|
|
Reverse-time integration uses $\Delta t < 0$ unchanged; the implementation
|
|
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}
|
|
|
|
After each integration step the propagator checks one or more
|
|
constraints. When a constraint reports a violation between $(t_1, y_1)$
|
|
(not violated) and $(t_2, y_2)$ (violated), \verb|RefineTrigger|
|
|
locates the crossing within tolerance $\tau \in (0, 1)$ by binary
|
|
search in the linear-interpolation parameter $\lambda \in [0, 1]$:
|
|
|
|
\begin{algorithm}[H]
|
|
\caption{RefineTrigger}\label{alg:refine}
|
|
\begin{algorithmic}[1]
|
|
\State $L \gets 0,\; R \gets 1$
|
|
\State $t_3 \gets t_2,\; y_3 \gets y_2$
|
|
\While{$R - L > \tau$}
|
|
\State $m \gets (L + R)/2$
|
|
\State $t_3 \gets (1 - m)\,t_1 + m\,t_2$
|
|
\State $y_3 \gets \mathrm{lerp}(y_1, y_2, m)$
|
|
\If{constraint violated at $(t_3, y_3)$}
|
|
\State $R \gets m$
|
|
\Else
|
|
\State $L \gets m$
|
|
\EndIf
|
|
\EndWhile
|
|
\State \Return $(t_3, y_3)$
|
|
\end{algorithmic}
|
|
\end{algorithm}
|
|
|
|
After $\lceil \log_2 \tau^{-1} \rceil$ iterations, $R - L \le \tau$.
|
|
With $\tau = 0{.}01$ and $\Delta t = 60$~s, the returned point is within
|
|
$0{.}6$~s of the true crossing in parameter space; the corresponding
|
|
altitude error is bounded by $0{.}6\,|\dot y|$, which for typical
|
|
balloon ascent and parachute descent rates is at most $\sim 3$~m.
|
|
|
|
The returned point is the \emph{last midpoint sampled} rather than
|
|
guaranteed to lie on the triggered side; this matches the reference
|
|
Tawhiri implementation byte for byte.
|
|
|
|
% =========================================================================
|
|
\section{Wind data pipeline}
|
|
\label{sec:winddata}
|
|
|
|
\paragraph{Data source.} NOAA GFS 0.5\textdegree{} (default) or 0.25\textdegree{}
|
|
forecasts, optionally subset by region or hour range. GEFS ensemble
|
|
runs are supported by selecting one of the 21 members; each member is a
|
|
separate dataset (\verb|DatasetID.Subset.Members = \{m\}|).
|
|
|
|
\paragraph{Cube layout.} A flat C-order row-major float32 array, shape
|
|
$(N_{\text{hours}}, N_{\text{levels}}, 3, N_{\text{lat}}, N_{\text{lng}})$,
|
|
where the variable axis is fixed to (HGT, UGRD, VGRD). Per-variant sizes
|
|
live in \verb|internal/weather/gfs/variant.go|.
|
|
|
|
\paragraph{Sampling.} Given a query $(t, \varphi, \lambda, h)$, the
|
|
sampler computes the time-in-hours offset
|
|
$\tau = (t - t_0)/3600$ from the dataset epoch $t_0$, brackets
|
|
$(\tau, \varphi, \lambda)$ on the three horizontal axes, then bisects
|
|
the pressure-level axis to find the largest level $\ell$ whose
|
|
trilinearly-interpolated HGT is below $h$. Wind components are
|
|
extracted via two more trilinear evaluations (at levels $\ell$ and
|
|
$\ell + 1$) and linearly interpolated in altitude:
|
|
\[
|
|
W(t, \varphi, \lambda, h) = \alpha \cdot W_\ell + (1 - \alpha) \cdot W_{\ell+1},
|
|
\qquad
|
|
\alpha = \frac{H_{\ell+1} - h}{H_{\ell+1} - H_\ell}.
|
|
\]
|
|
|
|
% =========================================================================
|
|
\section{Coverage and dataset selection}
|
|
\label{sec:coverage}
|
|
|
|
A loaded dataset $\mathcal{D}$ exposes its \emph{coverage}
|
|
$C_\mathcal{D} = (R_\mathcal{D}, [t_0, t_1])$ where $R_\mathcal{D}$ is a
|
|
geographic bounding box (possibly antimeridian-spanning) and
|
|
$[t_0, t_1]$ is the temporal extent. When more than one dataset is
|
|
loaded simultaneously, the predictor selects the first one whose
|
|
$C_\mathcal{D}$ contains the launch query. Regional / sub-range
|
|
datasets thus complement the global default.
|
|
|
|
% =========================================================================
|
|
\section{Deferrals and design notes}
|
|
\label{sec:deferrals}
|
|
|
|
\paragraph{Mass-aware drift.} The current model assumes the payload moves
|
|
horizontally at exactly the local wind velocity. A heavier payload
|
|
exhibits a velocity defect proportional to inertial coupling. A
|
|
plausible extension is the Stokes-style first-order lag model
|
|
\[
|
|
\dot{\mathbf{v}}_p = \frac{1}{\tau}\bigl(\mathbf{v}_{\text{wind}}(t,\mathbf{s}) - \mathbf{v}_p\bigr),
|
|
\]
|
|
introduced as an additional state variable $\mathbf{v}_p$ alongside the
|
|
existing $\mathbf{s}$. The Propagator interface already accepts
|
|
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.} 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
|
|
request, each picking a (member, parameter perturbation) pair. The
|
|
recommended architecture is to keep the perturbation inside the
|
|
predictor (so the same wind sample can serve many members and any
|
|
piecewise rate noise is correlated with the wind step), exposed as a
|
|
\verb|POST /api/v1/montecarlo| endpoint that returns one job per
|
|
sample and aggregates outcomes.
|
|
|
|
% =========================================================================
|
|
\section{Implementation notes}
|
|
\label{sec:impl}
|
|
|
|
The numerics library is intentionally small (under 300 lines of Go) and
|
|
uses no allocations on the hot path. The generic \verb|RK4Step| and
|
|
\verb|RefineTrigger| compile to per-type specialisations under Go's
|
|
generics, so a future C or Rust port can mirror the implementation
|
|
verbatim without changing the call sites in the trajectory engine.
|
|
|
|
\end{document}
|