1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
//! AV1 near-lossless codec for [`crate::TimedWindMap`] data on a regular
//! lat/lon grid.
//!
//! Encodes the time-varying `(u_east, v_north)` field as a 10-bit
//! 4:4:4 AV1 bitstream via the pure-Rust `rav1e` encoder, and decodes
//! it back with the matching `rav1d` decoder. No external tooling
//! (no ffmpeg / libaom build) involved on either side. The temporal +
//! spatial prediction inside AV1 is considerably more aggressive than
//! a generic compressor would manage on the same data, so the on-disk
//! artifact for a 720-hour global hourly GFS lands around **25 MB** —
//! small enough to `include_bytes!` into the GUI binary as a default
//! dataset.
//!
//! ## Pixel packing
//!
//! Each cell carries two scalars, `u_east` and `v_north`. We quantize
//! them linearly into 10-bit unsigned pixels:
//!
//! * `pixel = round((value + QUANT_MIN) / QUANT_RANGE * QUANT_LEVELS)`
//! * `value = pixel / QUANT_LEVELS * QUANT_RANGE - QUANT_MIN`
//!
//! with `QUANT_MIN = -60.0`, `QUANT_RANGE = 120.0`, `QUANT_LEVELS = 1023`.
//! `±60 m/s` brackets every observed surface wind (the strongest cyclonic
//! gusts in operational forecasts top out around 50 m/s), and 1024 buckets
//! over 120 m/s gives ~0.12 m/s per bucket — well under the AV1 codec
//! drift, so the pre-codec quantization is essentially noise-free.
//!
//! The 4:4:4 layout puts `u` on the Y plane, `v` on the Cb plane, and a
//! constant midpoint sentinel on the Cr plane. AV1's cross-component
//! prediction can use Cb when encoding Cr — feeding it a flat sentinel
//! collapses Cr's bitrate to almost nothing. Full-range (`color_range pc`)
//! is used both ways so the encoder doesn't clip to TV range.
//!
//! ## File layout
//!
//! The header has two on-disk versions:
//!
//! * **v1** (44-byte header, no datetime fields). The encoder no
//! longer emits v1 but the decoder still reads it for backward
//! compatibility with files produced before the v2 bump.
//! * **v2** (60-byte header, adds `start_unix_seconds` /
//! `end_unix_seconds` at offsets 44 and 52). Both fields are
//! `i64::MIN` when the dataset doesn't carry a UTC range
//! (synthetic generators, hand-rolled wind maps).
//!
//! | Offset | Size | Field | Notes |
//! |-------:|-----:|----------------|-------|
//! | 0 | 8 | `magic` | `b"WCAV\0\0\0\0"` |
//! | 8 | 4 | `version` | `u32`, current `2`; readers also accept `1` |
//! | 12 | 4 | `origin_lon` | `f32`, longitude of cell `i=0` (degrees) |
//! | 16 | 4 | `origin_lat` | `f32`, latitude of cell `j=0` (degrees) |
//! | 20 | 4 | `step_lon` | `f32`, degrees per cell along longitude |
//! | 24 | 4 | `step_lat` | `f32`, degrees per cell along latitude |
//! | 28 | 4 | `nx` | `u32`, longitude cell count |
//! | 32 | 4 | `ny` | `u32`, latitude cell count |
//! | 36 | 4 | `frame_count` | `u32`, number of time frames |
//! | 40 | 4 | `step_seconds` | `f32`, seconds between frames |
//! | 44 | 8 | `start_unix` | `i64` LE Unix seconds (v2 only); `i64::MIN` = unknown |
//! | 52 | 8 | `end_unix` | `i64` LE Unix seconds (v2 only); `i64::MIN` = unknown |
//! | 60 | — | IVF stream | AV1 frames (1 OBU per IVF frame, framerate=1) |
//!
//! ## Latitude axis convention
//!
//! GRIB2's column-major source layout puts `j = 0` at the southernmost
//! latitude — natural for the spec but upside-down for AV1 (which
//! expects pixel row 0 at the top of the frame). The encoder flips the
//! latitude axis on the way in and the decoder reverses the flip on the
//! way out, so the codec is invisible to the caller. Inside the AV1
//! stream, pixel row 0 is the highest latitude.
pub use ;
pub use ;
use crateGridLayout;
/// File magic prefix; same in every version.
pub const MAGIC: = *b"WCAV\0\0\0\0";
/// Current on-disk format version. The encoder always emits this; the
/// decoder also accepts older versions for backward compatibility.
pub const VERSION: u32 = 2;
/// Bytes the v1 header occupies, before the IVF payload starts. Kept
/// as a constant because the decoder still walks v1 files produced
/// before the v2 bump.
pub const HEADER_BYTES_V1: usize = 44;
/// Bytes the v2 header occupies, before the IVF payload starts. v2
/// adds two `i64` UTC timestamps at offsets 44 and 52.
pub const HEADER_BYTES_V2: usize = 60;
/// Bytes the *current* header occupies. Always equal to the latest
/// version's size.
pub const HEADER_BYTES: usize = HEADER_BYTES_V2;
/// Sentinel value for "the dataset's UTC time range is unknown" in
/// the v2 header's `start_unix` / `end_unix` slots. Distinct from
/// `0` (the actual Unix epoch) and `-1` (one second before epoch).
pub const UNKNOWN_TIME_SENTINEL: i64 = i64MIN;
/// Quantization origin: physical value (m/s) that maps to pixel `0`.
pub const QUANT_MIN: f32 = -60.0;
/// Quantization span: full physical range covered by the pixel space.
pub const QUANT_RANGE: f32 = 120.0;
/// Number of distinct pixel levels above zero — for 10-bit unsigned
/// this is `2^10 - 1 = 1023`. Stored as f32 because every arithmetic
/// site uses it as a multiplier rather than an index count.
pub const QUANT_LEVELS_F32: f32 = 1023.0;
/// Cr-plane sentinel: the constant midpoint pixel value, chosen so the
/// encoder doesn't see any signal in Cr at all.
pub const CR_SENTINEL: u16 = 512;
/// Quantize a physical value (m/s) to a 10-bit pixel, saturating at the ends.
///
/// The clamp guards against cyclonic outliers above the `±60 m/s`
/// modelled range — the small fraction of cells that saturate still
/// round-trip to whatever pixel value we picked, just with a clipped
/// magnitude.
/// Inverse of [`quantize`]. Lossless to the resolution of one pixel
/// (`QUANT_RANGE / QUANT_LEVELS_F32 ≈ 0.117 m/s`); AV1's reconstruction
/// drift dominates this in practice.
/// `(speed, direction)` → `(u_east, v_north)`.
///
/// Mirrors the convention `grib2` and `wind_codec` use: `direction` is
/// a "from"-bearing in degrees clockwise from north, so the wind
/// vector points the opposite way.
/// Inverse of [`sample_to_uv`], with the same convention.
/// Layout-equality check for encoder validation.
///
/// All frames in a `TimedWindMap` must share one grid. `f32` fields
/// are compared by bit pattern so the test is exact (no NaN ambiguity)
/// and serialises cleanly through the header.
/// Pixels per cell after packing both U and V into 10-bit values stored
/// in 16-bit little-endian containers. Exported because the encoder and
/// IVF parser both reason about it.
pub const BYTES_PER_PIXEL: usize = 2;