Skip to main content

flux_verify_api/engine/
vm.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4use crate::api::response::TraceEntry;
5use crate::compiler::ConstraintProblem;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(tag = "opcode")]
9pub enum Bytecode {
10    #[serde(rename = "LOAD")]
11    Load { name: String, value: f64, desc: String },
12    #[serde(rename = "SONAR_SVP")]
13    SonarSvp { depth_m: f64, temp_c: f64, salinity_ppt: f64 },
14    #[serde(rename = "SONAR_ABSORPTION")]
15    SonarAbsorption { frequency_khz: f64, depth_m: f64, temp_c: f64, salinity_ppt: f64 },
16    #[serde(rename = "SONAR_TL")]
17    SonarTl { range_m: f64, frequency_khz: f64, depth_m: f64, temp_c: f64, salinity_ppt: f64 },
18    #[serde(rename = "THERMAL_BOUND")]
19    ThermalBound { temp_c: f64, min_safe: f64, max_safe: f64 },
20    #[serde(rename = "GENERIC_COMPARE")]
21    GenericCompare { left: f64, operator: String, right: f64, desc: String },
22    #[serde(rename = "GENERIC_BOUND")]
23    GenericBound { value: f64, min: f64, max: f64, desc: String },
24    #[serde(rename = "GENERIC_RANGE_CHECK")]
25    GenericRangeCheck { value: f64, min: f64, max: f64, desc: String },
26    #[serde(rename = "ASSERT_GT")]
27    Assert { assertion_type: String, expected: f64, desc: String },
28}
29
30#[derive(Debug, Clone)]
31pub struct FluxVm {
32    pub registers: HashMap<String, f64>,
33    pub sound_velocity: f64,
34    pub absorption_db_km: f64,
35    pub transmission_loss_db: f64,
36    pub signal_excess_db: f64,
37    pub last_result: f64,
38}
39
40impl FluxVm {
41    pub fn new() -> Self {
42        Self {
43            registers: HashMap::new(),
44            sound_velocity: 0.0,
45            absorption_db_km: 0.0,
46            transmission_loss_db: 0.0,
47            signal_excess_db: 0.0,
48            last_result: 0.0,
49        }
50    }
51
52    /// Execute a sequence of bytecodes and produce a trace.
53    pub fn execute(&mut self, bytecodes: &[Bytecode]) -> Vec<TraceEntry> {
54        let mut trace = Vec::new();
55
56        for bc in bytecodes {
57            match bc {
58                Bytecode::Load { name, value, desc } => {
59                    self.registers.insert(name.clone(), *value);
60                    self.last_result = *value;
61                    trace.push(TraceEntry {
62                        opcode: "LOAD".into(),
63                        value: Some(*value),
64                        result: None,
65                        expected: None,
66                        actual: None,
67                        desc: desc.clone(),
68                    });
69                }
70                Bytecode::SonarSvp { depth_m, temp_c, salinity_ppt } => {
71                    let sv = mackenzie_sound_velocity(*depth_m, *temp_c, *salinity_ppt);
72                    self.sound_velocity = sv;
73                    self.last_result = sv;
74                    self.registers.insert("sound_velocity_ms".into(), sv);
75                    trace.push(TraceEntry {
76                        opcode: "SONAR_SVP".into(),
77                        value: None,
78                        result: Some(sv),
79                        expected: None,
80                        actual: None,
81                        desc: "sound velocity (Mackenzie 1981)".into(),
82                    });
83                }
84                Bytecode::SonarAbsorption { frequency_khz, depth_m, temp_c, salinity_ppt } => {
85                    let abs_db_km = francois_garrison_absorption(*frequency_khz, *depth_m, *temp_c, *salinity_ppt);
86                    self.absorption_db_km = abs_db_km;
87                    self.last_result = abs_db_km;
88                    self.registers.insert("absorption_db_km".into(), abs_db_km);
89                    trace.push(TraceEntry {
90                        opcode: "SONAR_ABSORPTION".into(),
91                        value: None,
92                        result: Some(abs_db_km),
93                        expected: None,
94                        actual: None,
95                        desc: "absorption dB/km (FG 1982)".into(),
96                    });
97                }
98                Bytecode::SonarTl { range_m, frequency_khz, depth_m, temp_c, salinity_ppt } => {
99                    let sv = if self.sound_velocity > 0.0 {
100                        self.sound_velocity
101                    } else {
102                        mackenzie_sound_velocity(*depth_m, *temp_c, *salinity_ppt)
103                    };
104                    let abs_db_km = if self.absorption_db_km > 0.0 {
105                        self.absorption_db_km
106                    } else {
107                        francois_garrison_absorption(*frequency_khz, *depth_m, *temp_c, *salinity_ppt)
108                    };
109                    let tl = transmission_loss(*range_m, sv, abs_db_km);
110                    self.transmission_loss_db = tl;
111                    self.last_result = tl;
112                    self.registers.insert("transmission_loss_db".into(), tl);
113                    trace.push(TraceEntry {
114                        opcode: "SONAR_TL".into(),
115                        value: None,
116                        result: Some(tl),
117                        expected: None,
118                        actual: None,
119                        desc: "transmission loss (dB)".into(),
120                    });
121                }
122                Bytecode::ThermalBound { temp_c, min_safe, max_safe } => {
123                    let in_range = *temp_c >= *min_safe && *temp_c <= *max_safe;
124                    let margin = if *temp_c < *min_safe {
125                        *temp_c - *min_safe
126                    } else if *temp_c > *max_safe {
127                        *temp_c - *max_safe
128                    } else {
129                        (*temp_c - *min_safe).min(*max_safe - *temp_c)
130                    };
131                    self.last_result = margin;
132                    self.registers.insert("thermal_margin".into(), margin);
133                    trace.push(TraceEntry {
134                        opcode: "THERMAL_BOUND".into(),
135                        value: Some(*temp_c),
136                        result: Some(margin),
137                        expected: None,
138                        actual: Some(if in_range { 1.0 } else { 0.0 }),
139                        desc: format!("temp {}°C vs safe [{}, {}]", temp_c, min_safe, max_safe),
140                    });
141                }
142                Bytecode::GenericCompare { left, operator, right, desc } => {
143                    let result = match operator.as_str() {
144                        "gt" => left > right,
145                        "gte" => left >= right,
146                        "lt" => left < right,
147                        "lte" => left <= right,
148                        "eq" => (left - right).abs() < f64::EPSILON,
149                        _ => false,
150                    };
151                    let actual = left - right;
152                    self.last_result = actual;
153                    trace.push(TraceEntry {
154                        opcode: "GENERIC_COMPARE".into(),
155                        value: Some(*left),
156                        result: Some(if result { 1.0 } else { 0.0 }),
157                        expected: Some(*right),
158                        actual: Some(actual),
159                        desc: desc.clone(),
160                    });
161                }
162                Bytecode::GenericBound { value, min, max, desc } => {
163                    let in_range = *value >= *min && *value <= *max;
164                    let margin = if in_range {
165                        (*value - *min).min(*max - *value)
166                    } else if *value < *min {
167                        *value - *min
168                    } else {
169                        *value - *max
170                    };
171                    self.last_result = margin;
172                    trace.push(TraceEntry {
173                        opcode: "GENERIC_BOUND".into(),
174                        value: Some(*value),
175                        result: Some(margin),
176                        expected: None,
177                        actual: Some(if in_range { 1.0 } else { 0.0 }),
178                        desc: desc.clone(),
179                    });
180                }
181                Bytecode::GenericRangeCheck { value, min, max, desc } => {
182                    let in_range = *value >= *min && *value <= *max;
183                    let margin = if in_range {
184                        (*value - *min).min(*max - *value)
185                    } else if *value < *min {
186                        *value - *min
187                    } else {
188                        *value - *max
189                    };
190                    self.last_result = margin;
191                    trace.push(TraceEntry {
192                        opcode: "GENERIC_RANGE_CHECK".into(),
193                        value: Some(*value),
194                        result: Some(margin),
195                        expected: None,
196                        actual: Some(if in_range { 1.0 } else { 0.0 }),
197                        desc: desc.clone(),
198                    });
199                }
200                Bytecode::Assert { assertion_type, expected, desc } => {
201                    match assertion_type.as_str() {
202                        "gt" => {
203                            // Active sonar equation: SE = SL - 2*TL + TS - DT
204                            // Default source level 220 dB, detection threshold 5 dB
205                            let source_level_db = 220.0;
206                            let detection_threshold_db = 5.0;
207                            let target_strength = self.registers.get("target_strength_db").copied().unwrap_or(10.0);
208                            let tl = self.transmission_loss_db;
209                            let signal_excess = source_level_db - 2.0 * tl + target_strength - detection_threshold_db;
210                            self.signal_excess_db = signal_excess;
211                            self.registers.insert("source_level_db".into(), source_level_db);
212                            self.registers.insert("signal_excess_db".into(), signal_excess);
213                            trace.push(TraceEntry {
214                                opcode: "ASSERT_GT".into(),
215                                value: None,
216                                result: None,
217                                expected: Some(*expected),
218                                actual: Some(signal_excess),
219                                desc: desc.clone(),
220                            });
221                        }
222                        "in_range" => {
223                            let margin = self.last_result;
224                            trace.push(TraceEntry {
225                                opcode: "ASSERT_IN_RANGE".into(),
226                                value: None,
227                                result: Some(margin),
228                                expected: Some(0.0),
229                                actual: Some(margin),
230                                desc: desc.clone(),
231                            });
232                        }
233                        _ => {
234                            trace.push(TraceEntry {
235                                opcode: "ASSERT".into(),
236                                value: None,
237                                result: None,
238                                expected: Some(*expected),
239                                actual: Some(self.last_result),
240                                desc: desc.clone(),
241                            });
242                        }
243                    }
244                }
245            }
246        }
247
248        trace
249    }
250
251    /// Evaluate the trace and determine PROVEN/DISPROVEN with confidence.
252    pub fn evaluate(&self, trace: &[TraceEntry], problem: &ConstraintProblem) -> (String, f64, Option<serde_json::Value>) {
253        match problem.domain.as_str() {
254            "sonar" => {
255                let signal_excess = self.signal_excess_db;
256                let proven = signal_excess > 0.0;
257                let confidence = if proven {
258                    1.0 - (-signal_excess / 20.0).exp().min(0.05)
259                } else {
260                    1.0 - (signal_excess / 20.0).exp().min(0.05)
261                };
262
263                let counterexample = if !proven {
264                    Some(serde_json::json!({
265                        "depth_m": self.registers.get("depth_m").copied().unwrap_or(0.0),
266                        "frequency_hz": self.registers.get("frequency_hz").copied().unwrap_or(0.0),
267                        "range_m": self.registers.get("range_m").copied().unwrap_or(0.0),
268                        "sound_velocity_ms": self.sound_velocity,
269                        "absorption_db_km": self.absorption_db_km,
270                        "transmission_loss_db": self.transmission_loss_db,
271                        "signal_excess_db": signal_excess,
272                    }))
273                } else {
274                    None
275                };
276
277                (
278                    if proven { "PROVEN".into() } else { "DISPROVEN".into() },
279                    (confidence * 100.0).round() / 100.0,
280                    counterexample,
281                )
282            }
283            "thermal" => {
284                let temp_c = self.registers.get("temp_c").copied().unwrap_or(0.0);
285                let min_safe = self.registers.get("min_safe").copied().unwrap_or(0.0);
286                let max_safe = self.registers.get("max_safe").copied().unwrap_or(0.0);
287                let in_range = temp_c >= min_safe && temp_c <= max_safe;
288                let confidence = if in_range { 0.99 } else { 0.98 };
289
290                let counterexample = if !in_range {
291                    Some(serde_json::json!({
292                        "temp_c": temp_c,
293                        "min_safe": min_safe,
294                        "max_safe": max_safe,
295                        "violation": if temp_c < min_safe { "below_min" } else { "above_max" },
296                        "margin_c": temp_c - if temp_c < min_safe { min_safe } else { max_safe },
297                    }))
298                } else {
299                    None
300                };
301
302                (
303                    if in_range { "PROVEN".into() } else { "DISPROVEN".into() },
304                    confidence,
305                    counterexample,
306                )
307            }
308            "generic" => {
309                // Check the last compare/assert entry
310                let mut proven = false;
311                for entry in trace.iter().rev() {
312                    if entry.opcode == "GENERIC_COMPARE" || entry.opcode == "GENERIC_RANGE_CHECK" || entry.opcode == "GENERIC_BOUND" {
313                        proven = entry.result.map_or(false, |v| v > 0.0);
314                        break;
315                    }
316                    if entry.opcode == "ASSERT_IN_RANGE" {
317                        proven = entry.result.map_or(false, |v| v >= 0.0);
318                        break;
319                    }
320                }
321
322                let counterexample = if !proven {
323                    Some(serde_json::json!({
324                        "registers": self.registers,
325                    }))
326                } else {
327                    None
328                };
329
330                (
331                    if proven { "PROVEN".into() } else { "DISPROVEN".into() },
332                    0.95,
333                    counterexample,
334                )
335            }
336            _ => ("UNKNOWN".into(), 0.0, None),
337        }
338    }
339}
340
341// ── Sonar Physics ──
342
343/// Mackenzie 1981 equation for sound velocity in seawater.
344/// Valid for: 2°C ≤ T ≤ 30°C, 25‰ ≤ S ≤ 40‰, 0 ≤ D ≤ 8000m
345pub fn mackenzie_sound_velocity(depth_m: f64, temp_c: f64, salinity_ppt: f64) -> f64 {
346    let d = depth_m;
347    let t = temp_c;
348    let s = salinity_ppt;
349
350    1448.96
351        + 4.591 * t
352        - 5.304e-2 * t * t
353        + 2.374e-4 * t * t * t
354        + 1.340 * (s - 35.0)
355        + 1.630e-2 * d
356        + 1.675e-7 * d * d
357        - 1.025e-2 * t * (s - 35.0)
358        - 7.139e-13 * t * d * d * d
359}
360
361/// Francois-Garrison 1982 absorption model (dB/km).
362/// Uses the simplified Thorp (1967) / Francois-Garrison (1982) formulation.
363/// Valid for 0.4–100 kHz, temperature-corrected.
364pub fn francois_garrison_absorption(frequency_khz: f64, depth_m: f64, temp_c: f64, salinity_ppt: f64) -> f64 {
365    let f = frequency_khz; // kHz
366    let t = temp_c;
367    let s = salinity_ppt;
368    let d = depth_m;
369
370    // Temperature scaling factor (relative to 15°C reference)
371    let temp_factor = 1.0 - 0.02 * (t - 15.0);
372    // Salinity scaling (relative to 35‰)
373    let sal_factor = (s / 35.0).sqrt();
374    // Pressure relief at depth
375    let pressure_factor = 1.0 - 3.0e-5 * d;
376
377    // Thorp (1967) + FG (1982) simplified absorption formula (dB/km)
378    // Boric acid relaxation: 0.11*f²/(1+f²)
379    // MgSO4 relaxation: 44*f²/(4100+f²)  
380    // Viscous: 2.75e-4*f²
381    let alpha = (0.11 * f * f / (1.0 + f * f)
382        + 44.0 * f * f / (4100.0 + f * f)
383        + 2.75e-4 * f * f
384        + 0.003)
385        * temp_factor
386        * sal_factor
387        * pressure_factor;
388
389    alpha.max(0.001)
390}
391
392/// Transmission loss using spherical spreading + absorption.
393/// TL = 20*log10(range) + alpha*range/1000
394pub fn transmission_loss(range_m: f64, _sound_velocity_ms: f64, absorption_db_km: f64) -> f64 {
395    let spreading = 20.0 * range_m.log10();
396    let absorption = absorption_db_km * range_m / 1000.0;
397    spreading + absorption
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403
404    #[test]
405    fn test_mackenzie_surface_standard() {
406        // Standard seawater: 15°C, 35‰, 0m → ~1500 m/s
407        let sv = mackenzie_sound_velocity(0.0, 15.0, 35.0);
408        assert!((sv - 1500.0).abs() < 10.0, "Expected ~1500 m/s, got {}", sv);
409    }
410
411    #[test]
412    fn test_mackenzie_deep_cold() {
413        // Deep cold: 4°C, 35‰, 4000m → ~1517 m/s (pressure dominates)
414        let sv = mackenzie_sound_velocity(4000.0, 4.0, 35.0);
415        assert!(sv > 1500.0, "Expected > 1500 m/s at depth, got {}", sv);
416    }
417
418    #[test]
419    fn test_fg_absorption_low_freq() {
420        // Low frequency (~1 kHz) should have low absorption (~0.06 dB/km)
421        let abs = francois_garrison_absorption(1.0, 0.0, 15.0, 35.0);
422        assert!(abs < 1.0, "Expected < 1 dB/km at 1 kHz, got {}", abs);
423    }
424
425    #[test]
426    fn test_fg_absorption_high_freq() {
427        // High frequency (~100 kHz) should have high absorption
428        let abs = francois_garrison_absorption(100.0, 0.0, 15.0, 35.0);
429        assert!(abs > 10.0, "Expected > 10 dB/km at 100 kHz, got {}", abs);
430    }
431
432    #[test]
433    fn test_transmission_loss_short_range() {
434        let tl = transmission_loss(1000.0, 1500.0, 0.1);
435        // 20*log10(1000) = 60, plus 0.1 absorption
436        assert!((tl - 60.1).abs() < 0.5, "Expected ~60.1 dB, got {}", tl);
437    }
438}