AIS decoder: NMEA tag blocks, multipart reassembly and bit-level fields
Exact prompt
# AIS message decoder (NMEA 0183 AIVDM/AIVDO with tag blocks) `input.json` names `ais.log`, a synthetic receiver log. Write `solve.py` (Python 3 standard library only); `python solve.py input.json answer.json`. Fresh logs are generated. The full grammar is below; nothing else is needed. ## Line grammar - Lines are numbered from 0 (every physical line counts, including blank ones); blank lines are skipped. - Optional **tag block** prefix: `\<tag>*hh\` before the sentence. `hh` = XOR of all characters of `<tag>`, two hex digits. No closing `\`, or a wrong checksum -> error `bad_tag_block` (line dropped). `<tag>` is comma-separated `key:value`; `c:<integer>` is a UNIX time. - **Time**: each line that reaches the sentence stage sets the current time to its valid tag block's `c:` value, or to null if it has none. A decoded message's `time` is the current time at the line that completed it. - Sentence `!AIVDM,...` or `!AIVDO,...` (anything else -> `not_ais`). Checksum: XOR of characters between `!` and `*`, two hex digits, either case; wrong or missing -> `bad_checksum`. It must have exactly 7 comma fields: talker+type, fragment count, fragment number, sequential id (may be empty), channel, payload, fill bits; non-integer count/number/fill, or not 1 <= number <= count <= 9, or fill not in 0..5 -> `bad_fields`. - **Multipart** (count > 1), keyed by (sequential id, channel): fragment 1 starts a new message (if one was already open under that key, count `incomplete_multipart` and replace it). Fragment n > 1 must match an open message with the same count that has exactly n-1 parts; otherwise count `orphan_fragment` (and, if a message was open under that key, drop it and also count `incomplete_multipart`). Messages still open at the end of the file count `incomplete_multipart`. Only the last fragment's fill bits are used. - **Armoring**: each payload character c gives v = ord(c) - 48, minus 8 more if that is > 40; valid characters are `0`-`W` and `` ` ``-`w` only (else `bad_payload_char`). Concatenate 6-bit values (MSB first), then drop `fill` bits from the end. ## Messages (bit offsets, length; U = unsigned, I = two's-complement signed; MSB first) - All: type 0,6 U; MMSI 8,30 U. - Types 1, 2, 3 (need >= 168 bits, else `short_message`): status 38,4 U; rot_raw 42,8 I; SOG 50,10 U (0.1 kn; 1023 = null); lon 61,28 I and lat 89,27 I in 1/10000 minute (= /600000 degrees, round to 6 decimals; lon 181° and lat 91° = null); COG 116,12 U (0.1°, 3600 = null); heading 128,9 U (511 = null); second 137,6 U. - Type 18 (>= 168 bits): SOG 46,10; lon 57,28 I; lat 85,27 I; COG 112,12; heading 124,9; second 133,6 (same scaling and nulls). - Type 5 (>= 420 bits): IMO 40,30 U; callsign 70,42 (7 chars); name 112,120 (20 chars); ship_type 232,8; length = to_bow 240,9 + to_stern 249,9; beam = to_port 258,6 + to_starboard 264,6; draught 294,8 (/10, metres); destination 302,120 (20 chars). Text: 6-bit value indexes `@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_ !"#$%&'()*+,-./0123456789:;<=>?`; strip trailing `@` then trailing spaces. - Any other type -> `unsupported_type`. ## answer.json - `messages`: decoded messages in completion order. Each has `type`, `mmsi`, the fields above (keys `status`, `rot_raw`, `sog`, `lon`, `lat`, `cog`, `heading`, `second`; type 5: `imo`, `callsign`, `name`, `ship_type`, `length`, `beam`, `draught`, `destination`), plus `line` (the completing line), `time`, and `own` (true for `AIVDO`). - `errors`: {kind: count}, only kinds with a count above 0. - `vessels`: {mmsi as a string (sorted numerically): {"positions": number of type 1/2/3/18 messages, then from the latest such message with non-null lat and lon: "last_lat", "last_lon", "last_time"; "last_sog" from the latest position message with non-null SOG; type 5 fields (latest): "name", "callsign", "imo", "ship_type", "length", "beam", "draught", "destination"}} (keys present only if known). ## Scoring Hidden checks (partial credit; all must pass; floats within 1e-6): `checksums_and_framing` (bad_checksum, not_ais, bad_fields, bad_tag_block counts), `class_a_positions`, `class_b_positions`, `static_voyage_multipart`, `tag_block_times_and_own`, `all_error_counts`, `vessel_summary` on the given log; `fresh_run_1`, `fresh_run_2` (everything). 30-second limit per run.
Every rule is stated, but errors compound: a wrong armor offset or signedness breaks every position; mishandled fragment state shifts message order and counts. Original benchmark task written for usecaserank.com (not based on a specific public post). Headless, offline run on a reproducible fixture. Each run's notes say how its cost was measured (estimates are marked).
Success criteria: 1. checksums_and_framing 2. class_a_positions 3. class_b_positions 4. static_voyage_multipart 5. tag_block_times_and_own 6. all_error_counts 7. vessel_summary 8. fresh_run_1 9. fresh_run_2
Best model for this task so far: Claude Haiku 5.5 (effort default (no --effort flag) · thinking on) (success, $0.02, 2m).
All runs (4)
100% pass rate across 4 eligible runs: 4 pass, 0 partial, 0 fail. Partial and failed runs are listed below with everything else.ⓘ
| Model | Cost | Time | Step-insⓘ | Autonomy | Outcome | Trustⓘ | Date | Compare |
|---|---|---|---|---|---|---|---|---|
| OGPT-6.1 Soldefault · thinking on | $0.06 | 56s | 0 | Fully autonomous | pass | Trust 80% | 2026-10-22 | |
| AClaude Haiku 5.5default · thinking on | $0.02 | 2m | 0 | Fully autonomous | pass | Trust 80% | 2026-10-20 | |
| AClaude Opus 5.5default · thinking on | $0.39 | 1.1h | 0 | Fully autonomous | pass | Trust 80% | 2026-10-20 | |
| AClaude Sonnet 5.5default · thinking on | $0.10 | 40s | 0 | Fully autonomous | pass | Trust 80% | 2026-10-20 |
Embed this result
A live badge for this use case. It links back here and updates as new runs are published.
Get new runs and ranking changes weekly
Which models got better or cheaper on real tasks like this one.