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 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 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 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 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
341pub 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
361pub fn francois_garrison_absorption(frequency_khz: f64, depth_m: f64, temp_c: f64, salinity_ppt: f64) -> f64 {
365 let f = frequency_khz; let t = temp_c;
367 let s = salinity_ppt;
368 let d = depth_m;
369
370 let temp_factor = 1.0 - 0.02 * (t - 15.0);
372 let sal_factor = (s / 35.0).sqrt();
374 let pressure_factor = 1.0 - 3.0e-5 * d;
376
377 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
392pub 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 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 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 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 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 assert!((tl - 60.1).abs() < 0.5, "Expected ~60.1 dB, got {}", tl);
437 }
438}