1use etdl_parser::ast::{EtlDocument, FaultTree, GateType};
2use etdl_parser::spanned::SpanKey;
3use std::collections::{BTreeMap, HashMap, VecDeque};
4
5use crate::validate::Diagnostic;
6
7pub type FaultTreeProbabilities = BTreeMap<String, f64>;
8
9pub fn resolve_fault_trees(
10 doc: &EtlDocument,
11 diagnostics: &mut Vec<Diagnostic>,
12) -> FaultTreeProbabilities {
13 let mut results = BTreeMap::new();
14
15 let fault_trees = match &doc.fault_trees {
16 Some(fts) => fts,
17 None => return results,
18 };
19
20 for (ft_id, ft) in fault_trees {
21 match compute_top_event_probability(ft) {
22 Ok(prob) => {
23 results.insert(ft_id.clone(), prob);
24 }
25 Err(e) => {
26 diagnostics.push(
27 Diagnostic::error(
28 "V-401",
29 format!("fault tree '{}': error computing probability: {}", ft_id, e),
30 )
31 .at(SpanKey::FaultTree {
32 tree: ft_id.clone(),
33 }),
34 );
35 }
36 }
37 }
38
39 results
40}
41
42fn compute_top_event_probability(ft: &FaultTree) -> Result<f64, String> {
43 let mut probs: HashMap<String, f64> = HashMap::new();
44
45 for (be_id, be) in &ft.basic_events {
46 let prob = compute_basic_event_probability(be)?;
47 probs.insert(be_id.clone(), prob);
48 }
49
50 let gates = match &ft.gates {
51 Some(g) => g,
52 None => {
53 let root_id = &ft.top_event.root_cause;
54 if let Some(&prob) = probs.get(root_id) {
55 return Ok(prob);
56 } else {
57 return Err(format!(
58 "topEvent.rootCause '{}' not found in basic events and no gates defined",
59 root_id
60 ));
61 }
62 }
63 };
64
65 let order = topological_sort_gates(gates, &ft.top_event.root_cause)?;
66
67 for gate_id in &order {
68 let gate = gates
69 .get(gate_id)
70 .ok_or_else(|| format!("gate '{}' not found during resolution", gate_id))?;
71
72 let input_probs: Vec<f64> = gate
73 .inputs
74 .iter()
75 .map(|input| {
76 probs
77 .get(input.as_str())
78 .copied()
79 .ok_or_else(|| format!("probability for '{}' not resolved", input))
80 })
81 .collect::<Result<Vec<_>, _>>()?;
82
83 let gate_prob = compute_gate_probability(&gate.gate_type, &input_probs, gate.k)?;
84 probs.insert(gate_id.clone(), gate_prob);
85 }
86
87 let root_id = &ft.top_event.root_cause;
88 probs
89 .get(root_id)
90 .copied()
91 .ok_or_else(|| format!("topEvent.rootCause '{}' probability not resolved", root_id))
92}
93
94fn compute_basic_event_probability(be: &etdl_parser::ast::BasicEvent) -> Result<f64, String> {
95 if let Some(ref failure_rate) = be.failure_rate {
96 let mission_time = be
97 .mission_time
98 .ok_or("failureRate set but missionTime missing")?;
99 Ok(1.0 - (-failure_rate * mission_time).exp())
100 } else if let Some(prob) = be.probability {
101 if !(0.0..=1.0).contains(&prob) {
102 return Err(format!("probability {} out of range [0, 1]", prob));
103 }
104 Ok(prob)
105 } else {
106 Err("basic event has neither probability nor failureRate".to_string())
107 }
108}
109
110fn compute_gate_probability(
111 gate_type: &GateType,
112 inputs: &[f64],
113 k: Option<u32>,
114) -> Result<f64, String> {
115 match gate_type {
116 GateType::And => Ok(inputs.iter().product()),
117 GateType::Or => {
118 let complement: f64 = inputs.iter().map(|p| 1.0 - p).product();
119 Ok(1.0 - complement)
120 }
121 GateType::Not => {
122 if inputs.len() != 1 {
123 return Err("NOT gate requires exactly 1 input".to_string());
124 }
125 if inputs[0] < 0.0 || inputs[0] > 1.0 {
126 return Err(format!(
127 "NOT gate input probability {} out of range",
128 inputs[0]
129 ));
130 }
131 Ok(1.0 - inputs[0])
132 }
133 GateType::Xor => {
134 if inputs.len() != 2 {
135 return Err("XOR gate requires exactly 2 inputs".to_string());
136 }
137 Ok(inputs[0] + inputs[1] - 2.0 * inputs[0] * inputs[1])
138 }
139 GateType::Voting => {
140 let k_val = k.ok_or("VOTING gate requires k")? as usize;
141 let n = inputs.len();
142
143 if k_val < 1 || k_val > n {
144 return Err(format!("VOTING gate: k={} out of range [1, {}]", k_val, n));
145 }
146
147 if inputs.iter().all(|&p| (p - inputs[0]).abs() < 1e-10) {
148 let p = inputs[0].clamp(0.0, 1.0);
149 let mut total = 0.0;
150 for j in k_val..=n {
151 total +=
152 binomial_coeff(n, j) * p.powi(j as i32) * (1.0 - p).powi((n - j) as i32);
153 }
154 Ok(total.clamp(0.0, 1.0))
155 } else {
156 let mut poly = vec![1.0];
157 for &p in inputs {
158 poly = multiply_polynomial(&poly, &[1.0 - p, p]);
159 }
160 let mut total = 0.0;
161 for j in k_val..=n {
162 if j < poly.len() {
163 total += poly[j];
164 }
165 }
166 Ok(total.clamp(0.0, 1.0))
167 }
168 }
169 GateType::Inhibit => {
170 if inputs.len() != 2 {
171 return Err("INHIBIT gate requires exactly 2 inputs".to_string());
172 }
173 Ok(inputs[0] * inputs[1])
174 }
175 GateType::PriorityAnd => {
176 let n = inputs.len();
177 if n < 2 {
178 return Err("PRIORITY_AND gate requires at least 2 inputs".to_string());
179 }
180 let mut log_p = 0.0;
184 for p in inputs {
185 let p = (*p).clamp(0.0, 1.0);
186 if p <= 0.0 {
187 return Ok(0.0);
188 }
189 log_p += p.ln();
190 }
191 log_p -= ln_factorial(n);
192 Ok(log_p.exp().clamp(0.0, 1.0))
193 }
194 }
195}
196
197fn ln_factorial(n: usize) -> f64 {
201 if n <= 170 {
202 let mut f = 1.0f64;
203 for i in 2..=n {
204 f *= i as f64;
205 }
206 f.ln()
207 } else {
208 ln_gamma((n as f64) + 1.0)
209 }
210}
211
212fn ln_gamma(x: f64) -> f64 {
216 const G: f64 = 7.0;
217 const P: [f64; 9] = [
218 0.999_999_999_999_809_9,
219 676.5203681218851,
220 -1259.1392167224028,
221 771.323_428_777_653_1,
222 -176.615_029_162_140_6,
223 12.507343278686905,
224 -0.13857109526572012,
225 9.984_369_578_019_572e-6,
226 1.5056327351493116e-7,
227 ];
228
229 if x < 0.5 {
230 return std::f64::consts::PI.ln()
231 - (std::f64::consts::PI * x).sin().ln()
232 - ln_gamma(1.0 - x);
233 }
234 let x_minus_one = x - 1.0;
235 let mut a = P[0];
236 let t = x_minus_one + G + 0.5;
237 for i in 1..9 {
238 a += P[i] / (x_minus_one + i as f64);
239 }
240 0.5 * (2.0 * std::f64::consts::PI).ln() + (x_minus_one + 0.5) * t.ln() - t + a.ln()
241}
242
243fn binomial_coeff(n: usize, k: usize) -> f64 {
244 if k > n {
245 return 0.0;
246 }
247 let ln = ln_factorial(n) - ln_factorial(k) - ln_factorial(n - k);
249 ln.exp().round()
250}
251
252fn multiply_polynomial(a: &[f64], b: &[f64]) -> Vec<f64> {
253 let mut result = vec![0.0; a.len() + b.len() - 1];
254 for (i, &coeff_a) in a.iter().enumerate() {
255 for (j, &coeff_b) in b.iter().enumerate() {
256 result[i + j] += coeff_a * coeff_b;
257 }
258 }
259 result
260}
261
262fn topological_sort_gates(
263 gates: &BTreeMap<String, etdl_parser::ast::Gate>,
264 root_id: &str,
265) -> Result<Vec<String>, String> {
266 let mut in_degree: HashMap<&str, usize> = HashMap::new();
267 let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
268
269 for gate_id in gates.keys() {
270 in_degree.entry(gate_id.as_str()).or_insert(0);
271 adj.entry(gate_id.as_str()).or_default();
272 }
273
274 for (gate_id, gate) in gates {
275 for input in &gate.inputs {
276 if gates.contains_key(input.as_str()) {
277 adj.entry(input.as_str())
278 .or_default()
279 .push(gate_id.as_str());
280 *in_degree.entry(gate_id.as_str()).or_insert(0) += 1;
281 }
282 }
283 }
284
285 let mut queue: VecDeque<&str> = VecDeque::new();
286 for gate_id in gates.keys() {
288 let deg = in_degree.get(gate_id.as_str()).copied().unwrap_or(0);
289 if deg == 0 {
290 queue.push_back(gate_id.as_str());
291 }
292 }
293
294 let mut order = Vec::new();
295 while let Some(id) = queue.pop_front() {
296 order.push(id.to_string());
297 if let Some(children) = adj.get(id) {
298 for &child in children {
299 if let Some(deg) = in_degree.get_mut(child) {
300 *deg -= 1;
301 if *deg == 0 {
302 queue.push_back(child);
303 }
304 }
305 }
306 }
307 }
308
309 if order.len() != gates.len() {
310 return Err("cycle detected in fault tree gates (V-403)".to_string());
311 }
312
313 if !order.contains(&root_id.to_string()) {
314 order.push(root_id.to_string());
315 }
316
317 Ok(order)
318}
319
320pub const MAX_CUT_SET_ROWS: usize = 100_000;
325
326pub fn enumerate_minimal_cut_sets(ft: &FaultTree) -> Result<Vec<Vec<String>>, String> {
327 let gates = match &ft.gates {
328 Some(g) => g,
329 None => {
330 return Ok(vec![vec![ft.top_event.root_cause.clone()]]);
331 }
332 };
333
334 for gate in gates.values() {
335 if matches!(gate.gate_type, GateType::Not | GateType::Xor) {
336 return Err(
337 "cannot enumerate cut sets for non-coherent fault tree (contains NOT or XOR gate)"
338 .to_string(),
339 );
340 }
341 }
342
343 let mut rows: Vec<Vec<String>> = vec![vec![ft.top_event.root_cause.clone()]];
344
345 let mut changed = true;
346 while changed {
347 changed = false;
348 let mut new_rows = Vec::new();
349
350 for row in &rows {
351 if new_rows.len() > MAX_CUT_SET_ROWS {
352 return Err(format!(
353 "cut set enumeration exceeded maximum row count {}; tree too large",
354 MAX_CUT_SET_ROWS
355 ));
356 }
357
358 let gate_positions: Vec<(usize, &str)> = row
359 .iter()
360 .enumerate()
361 .filter(|(_, item)| gates.contains_key(item.as_str()))
362 .map(|(i, item)| (i, item.as_str()))
363 .collect();
364
365 if gate_positions.is_empty() {
366 new_rows.push(row.clone());
367 continue;
368 }
369
370 changed = true;
371 let (pos, gate_id) = gate_positions[0];
372 let gate = &gates[gate_id];
373
374 match gate.gate_type {
375 GateType::Or => {
376 for input in &gate.inputs {
377 let mut new_row = row.clone();
378 new_row.remove(pos);
379 new_row.insert(pos, input.clone());
380 new_rows.push(new_row);
381 }
382 }
383 GateType::And | GateType::Inhibit | GateType::PriorityAnd => {
384 let mut new_row = row.clone();
385 new_row.remove(pos);
386 for (offset, input) in gate.inputs.iter().enumerate() {
387 new_row.insert(pos + offset, input.clone());
388 }
389 new_rows.push(new_row);
390 }
391 GateType::Voting => {
392 let k = gate.k.unwrap_or(1) as usize;
393 let combinations = generate_combinations(&gate.inputs, k);
394 for combo in &combinations {
395 let mut new_row = row.clone();
396 new_row.remove(pos);
397 for (offset, input) in combo.iter().enumerate() {
398 new_row.insert(pos + offset, input.clone());
399 }
400 new_rows.push(new_row);
401 }
402 }
403 _ => {
404 return Err(format!(
405 "unexpected gate type {:?} in cut set enumeration",
406 gate.gate_type
407 ));
408 }
409 }
410 }
411
412 rows = new_rows;
413 rows = minimize_rows(rows);
414 }
415
416 Ok(rows)
417}
418
419fn generate_combinations<T: Clone>(items: &[T], k: usize) -> Vec<Vec<T>> {
420 if k == 0 {
421 return vec![vec![]];
422 }
423 if items.is_empty() {
424 return vec![];
425 }
426
427 let mut result = Vec::new();
428 let first = &items[0];
429 let rest = &items[1..];
430
431 for mut combo in generate_combinations(rest, k - 1) {
432 let mut new_combo = vec![first.clone()];
433 new_combo.append(&mut combo);
434 result.push(new_combo);
435 }
436
437 for combo in generate_combinations(rest, k) {
438 result.push(combo);
439 }
440
441 result
442}
443
444fn minimize_rows(rows: Vec<Vec<String>>) -> Vec<Vec<String>> {
445 let mut sorted_rows: Vec<Vec<String>> = rows
446 .into_iter()
447 .map(|mut row| {
448 row.sort();
449 row.dedup();
450 row
451 })
452 .collect();
453
454 let mut i = 0;
455 while i < sorted_rows.len() {
456 let row_i = sorted_rows[i].clone();
457 sorted_rows.retain(|row_j| {
458 if std::ptr::eq(row_j, &row_i) {
459 return true;
460 }
461 let set_i: std::collections::BTreeSet<_> = row_i.iter().collect();
462 let set_j: std::collections::BTreeSet<_> = row_j.iter().collect();
463 !set_i.is_subset(&set_j)
464 });
465 i += 1;
466 }
467
468 sorted_rows
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474
475 #[test]
476 fn inhibit_gate_is_product() {
477 let p = compute_gate_probability(&GateType::Inhibit, &[0.1, 0.5], None).unwrap();
478 assert!((p - 0.05).abs() < 1e-12);
479 }
480
481 #[test]
482 fn voting_heterogeneous_matches_binomial() {
483 let p = compute_gate_probability(&GateType::Voting, &[0.5, 0.5, 0.5], Some(2)).unwrap();
485 let expected = 0.5; assert!((p - expected).abs() < 1e-9, "got {}", p);
487 }
488
489 #[test]
490 fn voting_heterogeneous_polynomial() {
491 let a = 0.1;
494 let b = 0.2;
495 let c = 0.3;
496 let p = compute_gate_probability(&GateType::Voting, &[a, b, c], Some(2)).unwrap();
497 let expected = a * b * (1.0 - c) + a * (1.0 - b) * c + (1.0 - a) * b * c + a * b * c;
498 assert!((p - expected).abs() < 1e-9, "got {}", p);
499 }
500
501 #[test]
502 fn binomial_coeff_does_not_overflow() {
503 let c = binomial_coeff(70, 35);
505 assert!(c > 0.0);
506 assert!(
508 (c - 1.121862778e20).abs() / 1.121862778e20 < 1e-6,
509 "got {}",
510 c
511 );
512 }
513
514 #[test]
515 fn priority_and_large_n_no_overflow() {
516 let twenty = vec![1.0; 20];
519 let p20 = compute_gate_probability(&GateType::PriorityAnd, &twenty, None).unwrap();
520 let exact_20 = (1u64..=20).fold(1.0f64, |acc, i| acc * i as f64);
521 assert!((p20 - 1.0 / exact_20).abs() < 1e-20, "got {}", p20);
522
523 let twenty_one = vec![1.0; 21];
524 let p21 = compute_gate_probability(&GateType::PriorityAnd, &twenty_one, None).unwrap();
525 let exact_21 = (1u64..=21).fold(1.0f64, |acc, i| acc * i as f64);
527 assert!((p21 - 1.0 / exact_21).abs() < 1e-20, "got {}", p21);
528 assert!(p20 > 0.0 && p20 < 1e-15);
530 assert!(p21 > 0.0 && p21 < 1e-15);
531 }
532
533 #[test]
534 fn ln_gamma_consistency() {
535 assert!((ln_factorial(6) - 720.0f64.ln()).abs() < 1e-9);
537 let small = binomial_coeff(10, 5);
539 assert!((small - 252.0).abs() < 1e-6);
540 }
541
542 #[test]
543 fn inhibit_requires_two_inputs() {
544 assert!(compute_gate_probability(&GateType::Inhibit, &[0.1], None).is_err());
545 }
546
547 #[test]
548 fn priority_and_uses_uniform_ordering() {
549 let p = compute_gate_probability(&GateType::PriorityAnd, &[0.2, 0.3], None).unwrap();
551 assert!((p - 0.03).abs() < 1e-12);
552 }
553
554 #[test]
555 fn priority_and_three_inputs() {
556 let p = compute_gate_probability(&GateType::PriorityAnd, &[0.2, 0.3, 0.4], None).unwrap();
558 assert!((p - 0.004).abs() < 1e-12);
559 }
560
561 #[test]
562 fn priority_and_requires_two_inputs() {
563 assert!(compute_gate_probability(&GateType::PriorityAnd, &[0.1], None).is_err());
564 }
565}