mfsk_core/msg/ap.rs
1//! A Priori (AP) hint for WSJT 77-bit message payloads.
2//!
3//! Known parts of the message (callsigns, grid, report) are converted to
4//! their packed bit representation and marked as "locked" so a downstream
5//! FEC decoder can clamp those LLRs to a high-confidence value. AP hints
6//! typically drop the decode threshold by a few dB when the caller knows
7//! the expected message format (e.g. "CQ from a specific DX call", or
8//! "RRR/RR73/73 as part of a QSO exchange").
9//!
10//! The 77-bit bit layout is shared across FT8, FT4, FT2 and FST4 — all WSJT
11//! Type-1 messages use the same `call1 / call2 / grid-or-report / i3` field
12//! positions — so `ApHint` lives in the protocol-agnostic message layer.
13
14use super::wsjt77::{pack_grid4, pack28};
15
16/// A Priori information to bias decoding.
17#[derive(Debug, Clone, Default)]
18pub struct ApHint {
19 /// Known first callsign (e.g. "CQ", "JA1ABC"). Locks message bits 0–28.
20 pub call1: Option<String>,
21 /// Known second callsign (e.g. "3Y0Z"). Locks message bits 29–57.
22 pub call2: Option<String>,
23 /// Known grid locator (e.g. "PM95"). Locks bits 58–73.
24 pub grid: Option<String>,
25 /// Known response token: "RRR", "RR73", or "73". Locks bits 58–73.
26 pub report: Option<String>,
27}
28
29impl ApHint {
30 pub fn new() -> Self {
31 Self::default()
32 }
33 pub fn with_call1(mut self, call: &str) -> Self {
34 self.call1 = Some(call.to_string());
35 self
36 }
37 pub fn with_call2(mut self, call: &str) -> Self {
38 self.call2 = Some(call.to_string());
39 self
40 }
41 pub fn with_grid(mut self, grid: &str) -> Self {
42 self.grid = Some(grid.to_string());
43 self
44 }
45 pub fn with_report(mut self, rpt: &str) -> Self {
46 self.report = Some(rpt.to_string());
47 self
48 }
49
50 /// True if any AP field is populated.
51 pub fn has_info(&self) -> bool {
52 self.call1.is_some() || self.call2.is_some()
53 }
54
55 /// Build the `(mask, bit_values)` bit vectors of length `n_codeword` for
56 /// a downstream FEC codec. Bits 0–76 (the message payload) are populated
57 /// from the hint fields; bits 77..N are left unmasked.
58 ///
59 /// `mask[i] == 1` means bit `i` is AP-locked; `values[i]` is the target
60 /// bit value (0 or 1). The FEC codec clamps its LLR at these positions
61 /// to `±apmag` accordingly.
62 pub fn build_bits(&self, n_codeword: usize) -> (Vec<u8>, Vec<u8>) {
63 let mut mask = vec![0u8; n_codeword];
64 let mut values = vec![0u8; n_codeword];
65
66 // Write 28-bit packed call + 1-bit flag (=0) starting at `start`.
67 let mut set_call_bits = |call: &str, start: usize| {
68 if let Some(n28) = pack28(call) {
69 for i in 0..28 {
70 let bit = ((n28 >> (27 - i)) & 1) as u8;
71 mask[start + i] = 1;
72 values[start + i] = bit;
73 }
74 // Flag bit (ipa/ipb) = 0 for standard calls.
75 mask[start + 28] = 1;
76 values[start + 28] = 0;
77 }
78 };
79
80 if let Some(ref c1) = self.call1 {
81 set_call_bits(c1, 0);
82 }
83 if let Some(ref c2) = self.call2 {
84 set_call_bits(c2, 29);
85 }
86
87 // Bits 58–73: grid or response field (15-bit value + 1-bit ir flag).
88 if let Some(ref grid) = self.grid
89 && let Some(igrid) = pack_grid4(grid)
90 {
91 mask[58] = 1;
92 values[58] = 0; // ir=0
93 for i in 0..15 {
94 let bit = ((igrid >> (14 - i)) & 1) as u8;
95 mask[59 + i] = 1;
96 values[59 + i] = bit;
97 }
98 }
99 if let Some(ref rpt) = self.report {
100 let igrid_val: Option<u32> = match rpt.as_str() {
101 "RRR" => Some(32_400 + 2),
102 "RR73" => Some(32_400 + 3),
103 "73" => Some(32_400 + 4),
104 _ => None,
105 };
106 if let Some(igrid) = igrid_val {
107 mask[58] = 1;
108 values[58] = 0;
109 for i in 0..15 {
110 let bit = ((igrid >> (14 - i)) & 1) as u8;
111 mask[59 + i] = 1;
112 values[59 + i] = bit;
113 }
114 }
115 }
116
117 // Lock message type i3 = 001 (Type 1 standard) when any call known.
118 if self.has_info() {
119 mask[74] = 1;
120 values[74] = 0;
121 mask[75] = 1;
122 values[75] = 0;
123 mask[76] = 1;
124 values[76] = 1;
125 }
126
127 (mask, values)
128 }
129
130 /// Number of AP-locked message bits (informational; callers use it to
131 /// scale per-pass confidence thresholds).
132 pub fn locked_bits(&self, n_codeword: usize) -> usize {
133 let (mask, _) = self.build_bits(n_codeword);
134 mask.iter().filter(|&&m| m != 0).count()
135 }
136}