Skip to main content

xlog_prob/
xgcf.rs

1//! XLOG GPU Circuit Format (XGCF) - host-side representation and CPU evaluator.
2
3use std::collections::HashMap;
4
5use xlog_core::{Result, XlogError};
6
7use crate::kc::ddnnf::{DdnnfEdge, DdnnfNodeKind, DecisionDnnf};
8use crate::logsumexp::{
9    circuit_logsumexp, validate_circuit_gradient_values, validate_circuit_log_weight_pair,
10    validate_circuit_value,
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[repr(u8)]
15/// Node opcode in the structure-of-arrays XLOG GPU Circuit Format.
16pub enum XgcfNodeType {
17    /// Boolean false constant.
18    Const0 = 0,
19    /// Boolean true constant.
20    Const1 = 1,
21    /// Signed literal whose variable is stored in [`Xgcf::lit`].
22    Lit = 2,
23    /// Conjunction over the node's child range.
24    And = 3,
25    /// Deterministic disjunction over the node's child range.
26    Or = 4,
27    /// Binary decision with explicit false and true children.
28    Decision = 5,
29}
30
31#[derive(Debug, Clone)]
32/// Host-side structure-of-arrays representation of a compiled probabilistic circuit.
33pub struct Xgcf {
34    /// Opcode for every node.
35    pub node_type: Vec<XgcfNodeType>,
36    /// CSR offsets into [`Self::child_indices`] for each node.
37    pub child_offsets: Vec<u32>,
38    /// Flattened children of conjunction and disjunction nodes.
39    pub child_indices: Vec<u32>,
40    /// Signed DIMACS literal for each literal node, or zero otherwise.
41    pub lit: Vec<i32>,
42    /// Decision variable for each decision node, or zero otherwise.
43    pub decision_var: Vec<u32>,
44    /// False-branch child for each decision node.
45    pub decision_child_false: Vec<u32>,
46    /// True-branch child for each decision node.
47    pub decision_child_true: Vec<u32>,
48    /// Circuit root node indices.
49    pub roots: Vec<u32>,
50    /// CSR offsets into [`Self::level_nodes`] for bottom-up evaluation levels.
51    pub level_offsets: Vec<u32>,
52    /// Node indices ordered by bottom-up evaluation level.
53    pub level_nodes: Vec<u32>,
54}
55
56impl Xgcf {
57    /// Converts a decision-DNNF circuit into XGCF and validates its topology.
58    pub fn from_ddnnf(ddnnf: &DecisionDnnf) -> Result<Self> {
59        XgcfBuilder::new(ddnnf).build()
60    }
61
62    /// Return a semantically equivalent circuit that is **smooth** with respect to
63    /// the subset of variables marked as random in `is_random_var`.
64    ///
65    /// Smoothness guarantees that, for any OR/DECISION node, all branches mention
66    /// the same set of random variables. This makes WMC evaluation and gradients
67    /// correct even when evidence/queries force random variables in non-smooth
68    /// Decision-DNNF outputs.
69    pub fn smooth_random_vars(&self, is_random_var: &[bool]) -> Result<Self> {
70        XgcfSmoother::new(self, is_random_var)?.smooth()
71    }
72
73    /// Evaluates the circuit's weighted model count in log space on the CPU.
74    pub fn eval_log_wmc<F>(&self, var_log_weights: F) -> Result<f64>
75    where
76        F: Fn(u32) -> (f64, f64),
77    {
78        if self.roots.len() != 1 {
79            return Err(XlogError::Compilation(format!(
80                "XGCF eval expects exactly 1 root, got {}",
81                self.roots.len()
82            )));
83        }
84
85        let n = self.node_type.len();
86        if self.child_offsets.len() != n + 1 {
87            return Err(XlogError::Compilation(format!(
88                "XGCF invariant violation: child_offsets len {} != num_nodes+1 ({})",
89                self.child_offsets.len(),
90                n + 1
91            )));
92        }
93        if self.lit.len() != n
94            || self.decision_var.len() != n
95            || self.decision_child_false.len() != n
96            || self.decision_child_true.len() != n
97        {
98            return Err(XlogError::Compilation(
99                "XGCF invariant violation: per-node arrays length mismatch".to_string(),
100            ));
101        }
102        if self.level_offsets.is_empty() || *self.level_offsets.first().unwrap() != 0 {
103            return Err(XlogError::Compilation(
104                "XGCF invariant violation: level_offsets must start at 0".to_string(),
105            ));
106        }
107        if *self.level_offsets.last().unwrap() != self.level_nodes.len() as u32 {
108            return Err(XlogError::Compilation(
109                "XGCF invariant violation: level_offsets last != level_nodes.len".to_string(),
110            ));
111        }
112
113        let mut values: Vec<f64> = vec![0.0; n];
114
115        let num_levels = self.level_offsets.len() - 1;
116        for level in 0..num_levels {
117            let start = self.level_offsets[level] as usize;
118            let end = self.level_offsets[level + 1] as usize;
119            for &node_u32 in &self.level_nodes[start..end] {
120                let idx = node_u32 as usize;
121                let v = match self.node_type[idx] {
122                    XgcfNodeType::Const0 => f64::NEG_INFINITY,
123                    XgcfNodeType::Const1 => 0.0,
124                    XgcfNodeType::Lit => {
125                        let lit = self.lit[idx];
126                        if lit == 0 {
127                            return Err(XlogError::Compilation(format!(
128                                "XGCF invariant violation: LIT node {} has lit=0",
129                                idx
130                            )));
131                        }
132                        let var = lit.unsigned_abs();
133                        let (t, f) = validate_circuit_log_weight_pair(var_log_weights(var))?;
134                        if lit > 0 {
135                            t
136                        } else {
137                            f
138                        }
139                    }
140                    XgcfNodeType::And => {
141                        let c0 = self.child_offsets[idx] as usize;
142                        let c1 = self.child_offsets[idx + 1] as usize;
143                        if c0 == c1 {
144                            return Err(XlogError::Compilation(format!(
145                                "XGCF eval error: AND node {} has no children",
146                                idx
147                            )));
148                        }
149                        let mut acc = 0.0;
150                        for &child in &self.child_indices[c0..c1] {
151                            acc += values[child as usize];
152                        }
153                        acc
154                    }
155                    XgcfNodeType::Or => {
156                        let c0 = self.child_offsets[idx] as usize;
157                        let c1 = self.child_offsets[idx + 1] as usize;
158                        if c0 == c1 {
159                            return Err(XlogError::Compilation(format!(
160                                "XGCF eval error: OR node {} has no children",
161                                idx
162                            )));
163                        }
164                        let mut branch_vals: Vec<f64> = Vec::with_capacity(c1 - c0);
165                        for &child in &self.child_indices[c0..c1] {
166                            branch_vals.push(values[child as usize]);
167                        }
168                        circuit_logsumexp(&branch_vals)?
169                    }
170                    XgcfNodeType::Decision => {
171                        let var = self.decision_var[idx];
172                        if var == 0 {
173                            return Err(XlogError::Compilation(format!(
174                                "XGCF invariant violation: DECISION node {} has var=0",
175                                idx
176                            )));
177                        }
178                        let child_false = self.decision_child_false[idx] as usize;
179                        let child_true = self.decision_child_true[idx] as usize;
180                        let (t, f) = validate_circuit_log_weight_pair(var_log_weights(var))?;
181                        circuit_logsumexp(&[f + values[child_false], t + values[child_true]])?
182                    }
183                };
184                values[idx] = validate_circuit_value(v)?;
185            }
186        }
187
188        Ok(values[self.roots[0] as usize])
189    }
190
191    /// Evaluates log weighted model count and literal log-weight gradients on the CPU.
192    pub fn eval_log_wmc_and_grads(
193        &self,
194        var_log_weights: &[(f64, f64)],
195    ) -> Result<(f64, Vec<f64>, Vec<f64>)> {
196        if self.roots.len() != 1 {
197            return Err(XlogError::Compilation(format!(
198                "XGCF eval expects exactly 1 root, got {}",
199                self.roots.len()
200            )));
201        }
202
203        let n = self.node_type.len();
204        if self.child_offsets.len() != n + 1 {
205            return Err(XlogError::Compilation(format!(
206                "XGCF invariant violation: child_offsets len {} != num_nodes+1 ({})",
207                self.child_offsets.len(),
208                n + 1
209            )));
210        }
211        if self.lit.len() != n
212            || self.decision_var.len() != n
213            || self.decision_child_false.len() != n
214            || self.decision_child_true.len() != n
215        {
216            return Err(XlogError::Compilation(
217                "XGCF invariant violation: per-node arrays length mismatch".to_string(),
218            ));
219        }
220        if self.level_offsets.is_empty() || *self.level_offsets.first().unwrap() != 0 {
221            return Err(XlogError::Compilation(
222                "XGCF invariant violation: level_offsets must start at 0".to_string(),
223            ));
224        }
225        if *self.level_offsets.last().unwrap() != self.level_nodes.len() as u32 {
226            return Err(XlogError::Compilation(
227                "XGCF invariant violation: level_offsets last != level_nodes.len".to_string(),
228            ));
229        }
230
231        let mut max_var: u32 = 0;
232        for (&ty, &lit) in self.node_type.iter().zip(self.lit.iter()) {
233            if ty == XgcfNodeType::Lit && lit != 0 {
234                max_var = max_var.max(lit.unsigned_abs());
235            }
236        }
237        for &var in &self.decision_var {
238            max_var = max_var.max(var);
239        }
240
241        let weights_len = (max_var as usize) + 1;
242        if var_log_weights.len() < weights_len {
243            return Err(XlogError::Compilation(format!(
244                "XGCF eval expects weight table len >= {}, got {}",
245                weights_len,
246                var_log_weights.len()
247            )));
248        }
249        let mut values: Vec<f64> = vec![0.0; n];
250
251        let num_levels = self.level_offsets.len() - 1;
252        for level in 0..num_levels {
253            let start = self.level_offsets[level] as usize;
254            let end = self.level_offsets[level + 1] as usize;
255            for &node_u32 in &self.level_nodes[start..end] {
256                let idx = node_u32 as usize;
257                let v = match self.node_type[idx] {
258                    XgcfNodeType::Const0 => f64::NEG_INFINITY,
259                    XgcfNodeType::Const1 => 0.0,
260                    XgcfNodeType::Lit => {
261                        let lit = self.lit[idx];
262                        if lit == 0 {
263                            return Err(XlogError::Compilation(format!(
264                                "XGCF invariant violation: LIT node {} has lit=0",
265                                idx
266                            )));
267                        }
268                        let var = lit.unsigned_abs();
269                        let (t, f) =
270                            validate_circuit_log_weight_pair(var_log_weights[var as usize])?;
271                        if lit > 0 {
272                            t
273                        } else {
274                            f
275                        }
276                    }
277                    XgcfNodeType::And => {
278                        let c0 = self.child_offsets[idx] as usize;
279                        let c1 = self.child_offsets[idx + 1] as usize;
280                        if c0 == c1 {
281                            return Err(XlogError::Compilation(format!(
282                                "XGCF eval error: AND node {} has no children",
283                                idx
284                            )));
285                        }
286                        let mut acc = 0.0;
287                        for &child in &self.child_indices[c0..c1] {
288                            acc += values[child as usize];
289                        }
290                        acc
291                    }
292                    XgcfNodeType::Or => {
293                        let c0 = self.child_offsets[idx] as usize;
294                        let c1 = self.child_offsets[idx + 1] as usize;
295                        if c0 == c1 {
296                            return Err(XlogError::Compilation(format!(
297                                "XGCF eval error: OR node {} has no children",
298                                idx
299                            )));
300                        }
301                        let mut branch_vals: Vec<f64> = Vec::with_capacity(c1 - c0);
302                        for &child in &self.child_indices[c0..c1] {
303                            branch_vals.push(values[child as usize]);
304                        }
305                        circuit_logsumexp(&branch_vals)?
306                    }
307                    XgcfNodeType::Decision => {
308                        let var = self.decision_var[idx];
309                        if var == 0 {
310                            return Err(XlogError::Compilation(format!(
311                                "XGCF invariant violation: DECISION node {} has var=0",
312                                idx
313                            )));
314                        }
315                        let child_false = self.decision_child_false[idx] as usize;
316                        let child_true = self.decision_child_true[idx] as usize;
317                        let (t, f) =
318                            validate_circuit_log_weight_pair(var_log_weights[var as usize])?;
319                        circuit_logsumexp(&[f + values[child_false], t + values[child_true]])?
320                    }
321                };
322                values[idx] = validate_circuit_value(v)?;
323            }
324        }
325
326        let root_idx = self.roots[0] as usize;
327        let log_z = values[root_idx];
328
329        let mut adj: Vec<f64> = vec![0.0; n];
330        adj[root_idx] = 1.0;
331
332        let mut grad_true: Vec<f64> = vec![0.0; weights_len];
333        let mut grad_false: Vec<f64> = vec![0.0; weights_len];
334
335        for level in (0..num_levels).rev() {
336            let start = self.level_offsets[level] as usize;
337            let end = self.level_offsets[level + 1] as usize;
338            for &node_u32 in &self.level_nodes[start..end] {
339                let idx = node_u32 as usize;
340                let a = adj[idx];
341                if a == 0.0 {
342                    continue;
343                }
344
345                match self.node_type[idx] {
346                    XgcfNodeType::Const0 | XgcfNodeType::Const1 => {}
347                    XgcfNodeType::Lit => {
348                        let lit = self.lit[idx];
349                        if lit == 0 {
350                            return Err(XlogError::Compilation(format!(
351                                "XGCF invariant violation: LIT node {} has lit=0",
352                                idx
353                            )));
354                        }
355                        let var = lit.unsigned_abs() as usize;
356                        if lit > 0 {
357                            grad_true[var] += a;
358                        } else {
359                            grad_false[var] += a;
360                        }
361                    }
362                    XgcfNodeType::And => {
363                        let c0 = self.child_offsets[idx] as usize;
364                        let c1 = self.child_offsets[idx + 1] as usize;
365                        for &child in &self.child_indices[c0..c1] {
366                            adj[child as usize] += a;
367                        }
368                    }
369                    XgcfNodeType::Or => {
370                        let parent_v = values[idx];
371                        if parent_v.is_infinite() && parent_v.is_sign_negative() {
372                            continue;
373                        }
374                        let c0 = self.child_offsets[idx] as usize;
375                        let c1 = self.child_offsets[idx + 1] as usize;
376                        for &child in &self.child_indices[c0..c1] {
377                            let child_idx = child as usize;
378                            let child_v = values[child_idx];
379                            if child_v.is_infinite() && child_v.is_sign_negative() {
380                                continue;
381                            }
382                            let ratio = (child_v - parent_v).exp();
383                            if ratio != 0.0 {
384                                adj[child_idx] += a * ratio;
385                            }
386                        }
387                    }
388                    XgcfNodeType::Decision => {
389                        let var = self.decision_var[idx] as usize;
390                        if var == 0 {
391                            return Err(XlogError::Compilation(format!(
392                                "XGCF invariant violation: DECISION node {} has var=0",
393                                idx
394                            )));
395                        }
396
397                        let parent_v = values[idx];
398                        if parent_v.is_infinite() && parent_v.is_sign_negative() {
399                            continue;
400                        }
401
402                        let child_false = self.decision_child_false[idx] as usize;
403                        let child_true = self.decision_child_true[idx] as usize;
404                        let (t, f) = var_log_weights[var];
405
406                        let vf = values[child_false];
407                        let vt = values[child_true];
408
409                        let mut p_false = 0.0;
410                        let mut p_true = 0.0;
411                        if !(vf.is_infinite() && vf.is_sign_negative()) {
412                            p_false = (f + vf - parent_v).exp();
413                        }
414                        if !(vt.is_infinite() && vt.is_sign_negative()) {
415                            p_true = (t + vt - parent_v).exp();
416                        }
417
418                        if p_false != 0.0 {
419                            adj[child_false] += a * p_false;
420                            grad_false[var] += a * p_false;
421                        }
422                        if p_true != 0.0 {
423                            adj[child_true] += a * p_true;
424                            grad_true[var] += a * p_true;
425                        }
426                    }
427                }
428            }
429        }
430
431        validate_circuit_gradient_values(&grad_true, &grad_false)?;
432        Ok((log_z, grad_true, grad_false))
433    }
434}
435
436fn max_var_in_circuit(circuit: &Xgcf) -> Result<u32> {
437    let mut max_var: u32 = 0;
438    for (&ty, &lit) in circuit.node_type.iter().zip(circuit.lit.iter()) {
439        if ty == XgcfNodeType::Lit {
440            if lit == 0 {
441                return Err(XlogError::Compilation(
442                    "XGCF invariant violation: LIT node has lit=0".to_string(),
443                ));
444            }
445            max_var = max_var.max(lit.unsigned_abs());
446        }
447    }
448    for (&ty, &var) in circuit.node_type.iter().zip(circuit.decision_var.iter()) {
449        if ty == XgcfNodeType::Decision {
450            if var == 0 {
451                return Err(XlogError::Compilation(
452                    "XGCF invariant violation: DECISION node has var=0".to_string(),
453                ));
454            }
455            max_var = max_var.max(var);
456        }
457    }
458    Ok(max_var)
459}
460
461fn merge_union_sorted(a: &[u32], b: &[u32], out: &mut Vec<u32>) {
462    out.clear();
463    let mut i = 0usize;
464    let mut j = 0usize;
465    while i < a.len() && j < b.len() {
466        let va = a[i];
467        let vb = b[j];
468        if va == vb {
469            out.push(va);
470            i += 1;
471            j += 1;
472        } else if va < vb {
473            out.push(va);
474            i += 1;
475        } else {
476            out.push(vb);
477            j += 1;
478        }
479    }
480    if i < a.len() {
481        out.extend_from_slice(&a[i..]);
482    }
483    if j < b.len() {
484        out.extend_from_slice(&b[j..]);
485    }
486}
487
488fn sorted_difference(a: &[u32], b: &[u32], out: &mut Vec<u32>) {
489    out.clear();
490    let mut i = 0usize;
491    let mut j = 0usize;
492    while i < a.len() {
493        let va = a[i];
494        while j < b.len() && b[j] < va {
495            j += 1;
496        }
497        if j < b.len() && b[j] == va {
498            i += 1;
499            j += 1;
500            continue;
501        }
502        out.push(va);
503        i += 1;
504    }
505}
506
507fn insert_sorted_unique(sorted: &mut Vec<u32>, var: u32) {
508    match sorted.binary_search(&var) {
509        Ok(_) => {}
510        Err(pos) => sorted.insert(pos, var),
511    }
512}
513
514fn compute_random_support(circuit: &Xgcf, is_random_var: &[bool]) -> Result<Vec<Vec<u32>>> {
515    let n = circuit.node_type.len();
516    let mut support: Vec<Vec<u32>> = vec![Vec::new(); n];
517
518    let num_levels = circuit.level_offsets.len().saturating_sub(1);
519    for level in 0..num_levels {
520        let start = circuit.level_offsets[level] as usize;
521        let end = circuit.level_offsets[level + 1] as usize;
522        for &node_u32 in &circuit.level_nodes[start..end] {
523            let idx = node_u32 as usize;
524            match circuit.node_type[idx] {
525                XgcfNodeType::Const0 | XgcfNodeType::Const1 => {}
526                XgcfNodeType::Lit => {
527                    let lit = circuit.lit[idx];
528                    if lit == 0 {
529                        return Err(XlogError::Compilation(format!(
530                            "XGCF invariant violation: LIT node {} has lit=0",
531                            idx
532                        )));
533                    }
534                    let var = lit.unsigned_abs() as usize;
535                    if var < is_random_var.len() && is_random_var[var] {
536                        support[idx].push(var as u32);
537                    }
538                }
539                XgcfNodeType::And | XgcfNodeType::Or => {
540                    let c0 = circuit.child_offsets[idx] as usize;
541                    let c1 = circuit.child_offsets[idx + 1] as usize;
542                    let mut acc: Vec<u32> = Vec::new();
543                    let mut tmp: Vec<u32> = Vec::new();
544                    for &child in &circuit.child_indices[c0..c1] {
545                        let child_idx = child as usize;
546                        merge_union_sorted(&acc, &support[child_idx], &mut tmp);
547                        std::mem::swap(&mut acc, &mut tmp);
548                    }
549                    support[idx] = acc;
550                }
551                XgcfNodeType::Decision => {
552                    let var = circuit.decision_var[idx];
553                    if var == 0 {
554                        return Err(XlogError::Compilation(format!(
555                            "XGCF invariant violation: DECISION node {} has var=0",
556                            idx
557                        )));
558                    }
559                    let child_false = circuit.decision_child_false[idx] as usize;
560                    let child_true = circuit.decision_child_true[idx] as usize;
561
562                    let mut acc: Vec<u32> = Vec::new();
563                    merge_union_sorted(&support[child_false], &support[child_true], &mut acc);
564
565                    let var_usize = var as usize;
566                    if var_usize < is_random_var.len() && is_random_var[var_usize] {
567                        insert_sorted_unique(&mut acc, var);
568                    }
569                    support[idx] = acc;
570                }
571            }
572        }
573    }
574
575    Ok(support)
576}
577
578struct XgcfSmoother<'a> {
579    input: &'a Xgcf,
580    is_random_var: &'a [bool],
581    support: Vec<Vec<u32>>,
582}
583
584impl<'a> XgcfSmoother<'a> {
585    fn new(input: &'a Xgcf, is_random_var: &'a [bool]) -> Result<Self> {
586        let n = input.node_type.len();
587        if input.child_offsets.len() != n + 1 {
588            return Err(XlogError::Compilation(format!(
589                "XGCF invariant violation: child_offsets len {} != num_nodes+1 ({})",
590                input.child_offsets.len(),
591                n + 1
592            )));
593        }
594        if input.lit.len() != n
595            || input.decision_var.len() != n
596            || input.decision_child_false.len() != n
597            || input.decision_child_true.len() != n
598        {
599            return Err(XlogError::Compilation(
600                "XGCF invariant violation: per-node arrays length mismatch".to_string(),
601            ));
602        }
603
604        let max_var = max_var_in_circuit(input)?;
605        if is_random_var.len() <= (max_var as usize) {
606            return Err(XlogError::Compilation(format!(
607                "XGCF smoothing expects is_random_var len >= {}, got {}",
608                (max_var as usize) + 1,
609                is_random_var.len()
610            )));
611        }
612
613        let support = compute_random_support(input, is_random_var)?;
614        Ok(Self {
615            input,
616            is_random_var,
617            support,
618        })
619    }
620
621    fn smooth(&self) -> Result<Xgcf> {
622        XgcfSmoothBuilder::new(self.input).smooth(self.is_random_var, &self.support)
623    }
624}
625
626struct XgcfSmoothBuilder<'a> {
627    input: &'a Xgcf,
628    node_type: Vec<XgcfNodeType>,
629    child_offsets: Vec<u32>,
630    child_indices: Vec<u32>,
631    lit: Vec<i32>,
632    decision_var: Vec<u32>,
633    decision_child_false: Vec<u32>,
634    decision_child_true: Vec<u32>,
635    old_to_new: Vec<Option<u32>>,
636    lit_cache: HashMap<i32, u32>,
637    tautology_cache: HashMap<u32, u32>,
638    const0: u32,
639    const1: u32,
640}
641
642impl<'a> XgcfSmoothBuilder<'a> {
643    fn new(input: &'a Xgcf) -> Self {
644        let mut b = Self {
645            input,
646            node_type: Vec::new(),
647            child_offsets: Vec::new(),
648            child_indices: Vec::new(),
649            lit: Vec::new(),
650            decision_var: Vec::new(),
651            decision_child_false: Vec::new(),
652            decision_child_true: Vec::new(),
653            old_to_new: Vec::new(),
654            lit_cache: HashMap::new(),
655            tautology_cache: HashMap::new(),
656            const0: 0,
657            const1: 0,
658        };
659
660        b.const0 = b.push_const(false);
661        b.const1 = b.push_const(true);
662        b
663    }
664
665    fn push_base_node(&mut self, ty: XgcfNodeType) -> u32 {
666        let idx = u32::try_from(self.node_type.len()).expect("XGCF node index overflow");
667        self.node_type.push(ty);
668        self.child_offsets.push(self.child_indices.len() as u32);
669        self.lit.push(0);
670        self.decision_var.push(0);
671        self.decision_child_false.push(0);
672        self.decision_child_true.push(0);
673        idx
674    }
675
676    fn push_const(&mut self, value: bool) -> u32 {
677        self.push_base_node(if value {
678            XgcfNodeType::Const1
679        } else {
680            XgcfNodeType::Const0
681        })
682    }
683
684    fn get_lit_node(&mut self, lit: i32) -> Result<u32> {
685        if lit == 0 {
686            return Err(XlogError::Compilation(
687                "Cannot create XGCF LIT for 0 literal".to_string(),
688            ));
689        }
690        if let Some(&idx) = self.lit_cache.get(&lit) {
691            return Ok(idx);
692        }
693        let idx = self.push_base_node(XgcfNodeType::Lit);
694        self.lit[idx as usize] = lit;
695        self.lit_cache.insert(lit, idx);
696        Ok(idx)
697    }
698
699    fn push_and(&mut self, mut children: Vec<u32>) -> Result<u32> {
700        if children.contains(&self.const0) {
701            return Ok(self.const0);
702        }
703        children.retain(|&c| c != self.const1);
704        children.sort();
705        children.dedup();
706        match children.as_slice() {
707            [] => Ok(self.const1),
708            [only] => Ok(*only),
709            _ => {
710                let idx = self.push_base_node(XgcfNodeType::And);
711                self.child_indices.extend_from_slice(&children);
712                Ok(idx)
713            }
714        }
715    }
716
717    fn push_or(&mut self, mut children: Vec<u32>) -> Result<u32> {
718        children.retain(|&c| c != self.const0);
719        children.sort();
720        children.dedup();
721        match children.as_slice() {
722            [] => Ok(self.const0),
723            [only] => Ok(*only),
724            _ => {
725                let idx = self.push_base_node(XgcfNodeType::Or);
726                self.child_indices.extend_from_slice(&children);
727                Ok(idx)
728            }
729        }
730    }
731
732    fn push_decision(&mut self, var: u32, child_false: u32, child_true: u32) -> Result<u32> {
733        if var == 0 {
734            return Err(XlogError::Compilation(
735                "Cannot create XGCF DECISION with var=0".to_string(),
736            ));
737        }
738        let idx = self.push_base_node(XgcfNodeType::Decision);
739        self.decision_var[idx as usize] = var;
740        self.decision_child_false[idx as usize] = child_false;
741        self.decision_child_true[idx as usize] = child_true;
742        Ok(idx)
743    }
744
745    fn tautology_decision(&mut self, var: u32) -> Result<u32> {
746        if let Some(&idx) = self.tautology_cache.get(&var) {
747            return Ok(idx);
748        }
749        let idx = self.push_decision(var, self.const1, self.const1)?;
750        self.tautology_cache.insert(var, idx);
751        Ok(idx)
752    }
753
754    fn smooth(mut self, is_random_var: &[bool], support: &[Vec<u32>]) -> Result<Xgcf> {
755        let n = self.input.node_type.len();
756        self.old_to_new = vec![None; n];
757
758        let num_levels = self.input.level_offsets.len().saturating_sub(1);
759        for level in 0..num_levels {
760            let start = self.input.level_offsets[level] as usize;
761            let end = self.input.level_offsets[level + 1] as usize;
762            for &node_u32 in &self.input.level_nodes[start..end] {
763                let idx = node_u32 as usize;
764
765                let new_idx = match self.input.node_type[idx] {
766                    XgcfNodeType::Const0 => self.const0,
767                    XgcfNodeType::Const1 => self.const1,
768                    XgcfNodeType::Lit => {
769                        let lit = self.input.lit[idx];
770                        self.get_lit_node(lit)?
771                    }
772                    XgcfNodeType::And => {
773                        let c0 = self.input.child_offsets[idx] as usize;
774                        let c1 = self.input.child_offsets[idx + 1] as usize;
775                        let mut children: Vec<u32> = Vec::with_capacity(c1 - c0);
776                        for &child in &self.input.child_indices[c0..c1] {
777                            let child_idx = child as usize;
778                            let mapped = self.old_to_new[child_idx].ok_or_else(|| {
779                                XlogError::Compilation(format!(
780                                    "XGCF smoothing error: missing mapped child {} for AND node {}",
781                                    child_idx, idx
782                                ))
783                            })?;
784                            children.push(mapped);
785                        }
786                        self.push_and(children)?
787                    }
788                    XgcfNodeType::Or => {
789                        let parent_support = &support[idx];
790                        let c0 = self.input.child_offsets[idx] as usize;
791                        let c1 = self.input.child_offsets[idx + 1] as usize;
792                        let mut wrapped_children: Vec<u32> = Vec::with_capacity(c1 - c0);
793                        let mut missing: Vec<u32> = Vec::new();
794                        for &child in &self.input.child_indices[c0..c1] {
795                            let child_idx = child as usize;
796                            let child_new = self.old_to_new[child_idx].ok_or_else(|| {
797                                XlogError::Compilation(format!(
798                                    "XGCF smoothing error: missing mapped child {} for OR node {}",
799                                    child_idx, idx
800                                ))
801                            })?;
802
803                            let child_support = &support[child_idx];
804                            sorted_difference(parent_support, child_support, &mut missing);
805
806                            if missing.is_empty() {
807                                wrapped_children.push(child_new);
808                            } else {
809                                let mut and_children: Vec<u32> =
810                                    Vec::with_capacity(1 + missing.len());
811                                and_children.push(child_new);
812                                for &var in &missing {
813                                    let var_usize = var as usize;
814                                    if var_usize < is_random_var.len() && is_random_var[var_usize] {
815                                        and_children.push(self.tautology_decision(var)?);
816                                    }
817                                }
818                                wrapped_children.push(self.push_and(and_children)?);
819                            }
820                        }
821                        self.push_or(wrapped_children)?
822                    }
823                    XgcfNodeType::Decision => {
824                        let var = self.input.decision_var[idx];
825                        let child_false_old = self.input.decision_child_false[idx] as usize;
826                        let child_true_old = self.input.decision_child_true[idx] as usize;
827
828                        let child_false_new = self.old_to_new[child_false_old].ok_or_else(|| {
829                            XlogError::Compilation(format!(
830                                "XGCF smoothing error: missing mapped decision false child {} for node {}",
831                                child_false_old, idx
832                            ))
833                        })?;
834                        let child_true_new = self.old_to_new[child_true_old].ok_or_else(|| {
835                            XlogError::Compilation(format!(
836                                "XGCF smoothing error: missing mapped decision true child {} for node {}",
837                                child_true_old, idx
838                            ))
839                        })?;
840
841                        let mut union_children: Vec<u32> = Vec::new();
842                        merge_union_sorted(
843                            &support[child_false_old],
844                            &support[child_true_old],
845                            &mut union_children,
846                        );
847
848                        if union_children.binary_search(&var).is_ok() {
849                            return Err(XlogError::Compilation(format!(
850                                "XGCF smoothing error: decision var {} appears in child support at node {}",
851                                var, idx
852                            )));
853                        }
854
855                        let mut missing: Vec<u32> = Vec::new();
856
857                        sorted_difference(&union_children, &support[child_false_old], &mut missing);
858                        let new_false = if missing.is_empty() {
859                            child_false_new
860                        } else {
861                            let mut and_children: Vec<u32> = Vec::with_capacity(1 + missing.len());
862                            and_children.push(child_false_new);
863                            for &v in &missing {
864                                let v_usize = v as usize;
865                                if v_usize < is_random_var.len() && is_random_var[v_usize] {
866                                    and_children.push(self.tautology_decision(v)?);
867                                }
868                            }
869                            self.push_and(and_children)?
870                        };
871
872                        sorted_difference(&union_children, &support[child_true_old], &mut missing);
873                        let new_true = if missing.is_empty() {
874                            child_true_new
875                        } else {
876                            let mut and_children: Vec<u32> = Vec::with_capacity(1 + missing.len());
877                            and_children.push(child_true_new);
878                            for &v in &missing {
879                                let v_usize = v as usize;
880                                if v_usize < is_random_var.len() && is_random_var[v_usize] {
881                                    and_children.push(self.tautology_decision(v)?);
882                                }
883                            }
884                            self.push_and(and_children)?
885                        };
886
887                        self.push_decision(var, new_false, new_true)?
888                    }
889                };
890
891                self.old_to_new[idx] = Some(new_idx);
892            }
893        }
894
895        // Finalize offsets (sentinel).
896        self.child_offsets.push(self.child_indices.len() as u32);
897
898        let mut roots: Vec<u32> = Vec::with_capacity(self.input.roots.len());
899        for &root in &self.input.roots {
900            let idx = root as usize;
901            let mapped = self.old_to_new[idx].ok_or_else(|| {
902                XlogError::Compilation(format!(
903                    "XGCF smoothing error: missing mapped root node {}",
904                    idx
905                ))
906            })?;
907            roots.push(mapped);
908        }
909
910        let (level_offsets, level_nodes) = XgcfBuilder::levelize(
911            &self.node_type,
912            &self.child_offsets,
913            &self.child_indices,
914            &self.decision_child_false,
915            &self.decision_child_true,
916            &roots,
917        )?;
918
919        Ok(Xgcf {
920            node_type: self.node_type,
921            child_offsets: self.child_offsets,
922            child_indices: self.child_indices,
923            lit: self.lit,
924            decision_var: self.decision_var,
925            decision_child_false: self.decision_child_false,
926            decision_child_true: self.decision_child_true,
927            roots,
928            level_offsets,
929            level_nodes,
930        })
931    }
932}
933
934struct XgcfBuilder<'a> {
935    ddnnf: &'a DecisionDnnf,
936    node_type: Vec<XgcfNodeType>,
937    child_offsets: Vec<u32>,
938    child_indices: Vec<u32>,
939    lit: Vec<i32>,
940    decision_var: Vec<u32>,
941    decision_child_false: Vec<u32>,
942    decision_child_true: Vec<u32>,
943    lit_cache: HashMap<i32, u32>,
944    ddnnf_cache: HashMap<u32, u32>,
945    const0: u32,
946    const1: u32,
947}
948
949impl<'a> XgcfBuilder<'a> {
950    fn new(ddnnf: &'a DecisionDnnf) -> Self {
951        let mut builder = Self {
952            ddnnf,
953            node_type: Vec::new(),
954            child_offsets: Vec::new(),
955            child_indices: Vec::new(),
956            lit: Vec::new(),
957            decision_var: Vec::new(),
958            decision_child_false: Vec::new(),
959            decision_child_true: Vec::new(),
960            lit_cache: HashMap::new(),
961            ddnnf_cache: HashMap::new(),
962            const0: 0,
963            const1: 0,
964        };
965
966        builder.const0 = builder.push_const(false);
967        builder.const1 = builder.push_const(true);
968        builder
969    }
970
971    fn push_base_node(&mut self, ty: XgcfNodeType) -> u32 {
972        let idx = u32::try_from(self.node_type.len()).expect("XGCF node index overflow");
973        self.node_type.push(ty);
974        self.child_offsets.push(self.child_indices.len() as u32);
975        self.lit.push(0);
976        self.decision_var.push(0);
977        self.decision_child_false.push(0);
978        self.decision_child_true.push(0);
979        idx
980    }
981
982    fn push_const(&mut self, value: bool) -> u32 {
983        self.push_base_node(if value {
984            XgcfNodeType::Const1
985        } else {
986            XgcfNodeType::Const0
987        })
988    }
989
990    fn get_lit_node(&mut self, lit: i32) -> Result<u32> {
991        if lit == 0 {
992            return Err(XlogError::Compilation(
993                "Cannot create XGCF LIT for 0 literal".to_string(),
994            ));
995        }
996        if let Some(&idx) = self.lit_cache.get(&lit) {
997            return Ok(idx);
998        }
999        let idx = self.push_base_node(XgcfNodeType::Lit);
1000        self.lit[idx as usize] = lit;
1001        self.lit_cache.insert(lit, idx);
1002        Ok(idx)
1003    }
1004
1005    fn push_and(&mut self, mut children: Vec<u32>) -> Result<u32> {
1006        if children.contains(&self.const0) {
1007            return Ok(self.const0);
1008        }
1009        children.retain(|&c| c != self.const1);
1010        children.sort();
1011        children.dedup();
1012        match children.as_slice() {
1013            [] => Ok(self.const1),
1014            [only] => Ok(*only),
1015            _ => {
1016                let idx = self.push_base_node(XgcfNodeType::And);
1017                self.child_indices.extend_from_slice(&children);
1018                Ok(idx)
1019            }
1020        }
1021    }
1022
1023    fn push_or(&mut self, mut children: Vec<u32>) -> Result<u32> {
1024        children.retain(|&c| c != self.const0);
1025        children.sort();
1026        children.dedup();
1027        match children.as_slice() {
1028            [] => Ok(self.const0),
1029            [only] => Ok(*only),
1030            _ => {
1031                let idx = self.push_base_node(XgcfNodeType::Or);
1032                self.child_indices.extend_from_slice(&children);
1033                Ok(idx)
1034            }
1035        }
1036    }
1037
1038    fn push_decision(&mut self, var: u32, child_false: u32, child_true: u32) -> Result<u32> {
1039        if var == 0 {
1040            return Err(XlogError::Compilation(
1041                "Cannot create XGCF DECISION with var=0".to_string(),
1042            ));
1043        }
1044        let idx = self.push_base_node(XgcfNodeType::Decision);
1045        self.decision_var[idx as usize] = var;
1046        self.decision_child_false[idx as usize] = child_false;
1047        self.decision_child_true[idx as usize] = child_true;
1048        Ok(idx)
1049    }
1050
1051    fn build(mut self) -> Result<Xgcf> {
1052        let root = self.convert_ddnnf_node(self.ddnnf.root())?;
1053
1054        // Finalize offsets (sentinel).
1055        self.child_offsets.push(self.child_indices.len() as u32);
1056
1057        let roots = vec![root];
1058        let (level_offsets, level_nodes) = Self::levelize(
1059            &self.node_type,
1060            &self.child_offsets,
1061            &self.child_indices,
1062            &self.decision_child_false,
1063            &self.decision_child_true,
1064            &roots,
1065        )?;
1066
1067        Ok(Xgcf {
1068            node_type: self.node_type,
1069            child_offsets: self.child_offsets,
1070            child_indices: self.child_indices,
1071            lit: self.lit,
1072            decision_var: self.decision_var,
1073            decision_child_false: self.decision_child_false,
1074            decision_child_true: self.decision_child_true,
1075            roots,
1076            level_offsets,
1077            level_nodes,
1078        })
1079    }
1080
1081    fn convert_ddnnf_node(&mut self, node_id: u32) -> Result<u32> {
1082        if let Some(&idx) = self.ddnnf_cache.get(&node_id) {
1083            return Ok(idx);
1084        }
1085        let kind = self.ddnnf.node_kind(node_id).ok_or_else(|| {
1086            XlogError::Compilation(format!("XGCF build error: unknown DDNNF node {}", node_id))
1087        })?;
1088
1089        let idx = match kind {
1090            DdnnfNodeKind::True => self.const1,
1091            DdnnfNodeKind::False => self.const0,
1092            DdnnfNodeKind::And => {
1093                let out = self.ddnnf.outgoing_edge_indices(node_id).ok_or_else(|| {
1094                    XlogError::Compilation(format!(
1095                        "XGCF build error: AND node {} has no outgoing edges",
1096                        node_id
1097                    ))
1098                })?;
1099                let mut child_nodes: Vec<u32> = Vec::with_capacity(out.len());
1100                for &edge_idx in out {
1101                    child_nodes.push(self.convert_ddnnf_edge_branch(edge_idx, None)?);
1102                }
1103                self.push_and(child_nodes)?
1104            }
1105            DdnnfNodeKind::Or => {
1106                let out = self.ddnnf.outgoing_edge_indices(node_id).ok_or_else(|| {
1107                    XlogError::Compilation(format!(
1108                        "XGCF build error: OR node {} has no outgoing edges",
1109                        node_id
1110                    ))
1111                })?;
1112                if out.len() == 2 {
1113                    let e0 = self.ddnnf.edge(out[0]).ok_or_else(|| {
1114                        XlogError::Compilation(format!("XGCF build error: missing edge {}", out[0]))
1115                    })?;
1116                    let e1 = self.ddnnf.edge(out[1]).ok_or_else(|| {
1117                        XlogError::Compilation(format!("XGCF build error: missing edge {}", out[1]))
1118                    })?;
1119
1120                    if let Some((var, edge_true, edge_false)) = infer_decision_var(e0, e1)? {
1121                        let edge_true = out[edge_true];
1122                        let edge_false = out[edge_false];
1123                        let child_true =
1124                            self.convert_ddnnf_edge_branch(edge_true, Some(var as i32))?;
1125                        let child_false =
1126                            self.convert_ddnnf_edge_branch(edge_false, Some(-(var as i32)))?;
1127                        self.push_decision(var, child_false, child_true)?
1128                    } else {
1129                        let mut child_nodes: Vec<u32> = Vec::with_capacity(out.len());
1130                        for &edge_idx in out {
1131                            child_nodes.push(self.convert_ddnnf_edge_branch(edge_idx, None)?);
1132                        }
1133                        self.push_or(child_nodes)?
1134                    }
1135                } else {
1136                    let mut child_nodes: Vec<u32> = Vec::with_capacity(out.len());
1137                    for &edge_idx in out {
1138                        child_nodes.push(self.convert_ddnnf_edge_branch(edge_idx, None)?);
1139                    }
1140                    self.push_or(child_nodes)?
1141                }
1142            }
1143        };
1144
1145        self.ddnnf_cache.insert(node_id, idx);
1146        Ok(idx)
1147    }
1148
1149    fn convert_ddnnf_edge_branch(&mut self, edge_idx: usize, drop_lit: Option<i32>) -> Result<u32> {
1150        let edge = self.ddnnf.edge(edge_idx).ok_or_else(|| {
1151            XlogError::Compilation(format!("XGCF build error: missing edge {}", edge_idx))
1152        })?;
1153
1154        let child = self.convert_ddnnf_node(edge.to)?;
1155
1156        let mut children: Vec<u32> = Vec::new();
1157        children.push(child);
1158
1159        let mut dropped = false;
1160        for &lit in &edge.lits {
1161            if let Some(dl) = drop_lit {
1162                if !dropped && lit == dl {
1163                    dropped = true;
1164                    continue;
1165                }
1166            }
1167            children.push(self.get_lit_node(lit)?);
1168        }
1169
1170        if let Some(dl) = drop_lit {
1171            if !dropped {
1172                return Err(XlogError::Compilation(format!(
1173                    "XGCF build error: expected to drop literal {} on edge {}->{} but not present",
1174                    dl, edge.from, edge.to
1175                )));
1176            }
1177        }
1178
1179        self.push_and(children)
1180    }
1181
1182    fn levelize(
1183        node_type: &[XgcfNodeType],
1184        child_offsets: &[u32],
1185        child_indices: &[u32],
1186        decision_child_false: &[u32],
1187        decision_child_true: &[u32],
1188        roots: &[u32],
1189    ) -> Result<(Vec<u32>, Vec<u32>)> {
1190        let n = node_type.len();
1191        let mut levels: Vec<Option<u32>> = vec![None; n];
1192        let mut visiting: Vec<bool> = vec![false; n];
1193
1194        #[expect(
1195            clippy::too_many_arguments,
1196            reason = "recursive levelization keeps the immutable circuit tables and its two mutable traversal-state tables explicit"
1197        )]
1198        fn level_of(
1199            idx: usize,
1200            node_type: &[XgcfNodeType],
1201            child_offsets: &[u32],
1202            child_indices: &[u32],
1203            decision_child_false: &[u32],
1204            decision_child_true: &[u32],
1205            levels: &mut [Option<u32>],
1206            visiting: &mut [bool],
1207        ) -> Result<u32> {
1208            if let Some(lvl) = levels[idx] {
1209                return Ok(lvl);
1210            }
1211            if visiting[idx] {
1212                return Err(XlogError::Compilation(format!(
1213                    "XGCF levelize error: cycle detected at node {}",
1214                    idx
1215                )));
1216            }
1217            visiting[idx] = true;
1218
1219            let lvl = match node_type[idx] {
1220                XgcfNodeType::Const0 | XgcfNodeType::Const1 | XgcfNodeType::Lit => 0,
1221                XgcfNodeType::And | XgcfNodeType::Or => {
1222                    let c0 = child_offsets[idx] as usize;
1223                    let c1 = child_offsets[idx + 1] as usize;
1224                    let mut max_child = 0u32;
1225                    for &child in &child_indices[c0..c1] {
1226                        max_child = max_child.max(level_of(
1227                            child as usize,
1228                            node_type,
1229                            child_offsets,
1230                            child_indices,
1231                            decision_child_false,
1232                            decision_child_true,
1233                            levels,
1234                            visiting,
1235                        )?);
1236                    }
1237                    max_child + 1
1238                }
1239                XgcfNodeType::Decision => {
1240                    let lf = level_of(
1241                        decision_child_false[idx] as usize,
1242                        node_type,
1243                        child_offsets,
1244                        child_indices,
1245                        decision_child_false,
1246                        decision_child_true,
1247                        levels,
1248                        visiting,
1249                    )?;
1250                    let lt = level_of(
1251                        decision_child_true[idx] as usize,
1252                        node_type,
1253                        child_offsets,
1254                        child_indices,
1255                        decision_child_false,
1256                        decision_child_true,
1257                        levels,
1258                        visiting,
1259                    )?;
1260                    lf.max(lt) + 1
1261                }
1262            };
1263
1264            visiting[idx] = false;
1265            levels[idx] = Some(lvl);
1266            Ok(lvl)
1267        }
1268
1269        for &root in roots {
1270            level_of(
1271                root as usize,
1272                node_type,
1273                child_offsets,
1274                child_indices,
1275                decision_child_false,
1276                decision_child_true,
1277                &mut levels,
1278                &mut visiting,
1279            )?;
1280        }
1281
1282        let max_level = levels.iter().flatten().copied().max().unwrap_or(0);
1283        let mut buckets: Vec<Vec<u32>> = vec![Vec::new(); (max_level as usize) + 1];
1284        for (i, lvl) in levels.iter().enumerate().take(n) {
1285            let Some(lvl) = lvl else {
1286                continue;
1287            };
1288            buckets[*lvl as usize].push(i as u32);
1289        }
1290
1291        let mut level_offsets: Vec<u32> = Vec::with_capacity(buckets.len() + 1);
1292        let mut level_nodes: Vec<u32> = Vec::new();
1293        level_offsets.push(0);
1294        for bucket in buckets {
1295            level_nodes.extend(bucket);
1296            level_offsets.push(level_nodes.len() as u32);
1297        }
1298        Ok((level_offsets, level_nodes))
1299    }
1300}
1301
1302fn infer_decision_var(e0: &DdnnfEdge, e1: &DdnnfEdge) -> Result<Option<(u32, usize, usize)>> {
1303    fn sign_map(lits: &[i32]) -> Result<HashMap<u32, bool>> {
1304        let mut map: HashMap<u32, bool> = HashMap::new();
1305        for &lit in lits {
1306            let var = lit.unsigned_abs();
1307            let sign = lit > 0;
1308            if let Some(prev) = map.insert(var, sign) {
1309                if prev != sign {
1310                    return Err(XlogError::Compilation(format!(
1311                        "XGCF build error: conflicting literals {} and {} in same branch",
1312                        var, lit
1313                    )));
1314                }
1315            }
1316        }
1317        Ok(map)
1318    }
1319
1320    let m0 = sign_map(&e0.lits)?;
1321    let m1 = sign_map(&e1.lits)?;
1322
1323    let mut candidates: Vec<u32> = Vec::new();
1324    for (var, &s0) in &m0 {
1325        if let Some(&s1) = m1.get(var) {
1326            if s0 != s1 {
1327                candidates.push(*var);
1328            }
1329        }
1330    }
1331
1332    if candidates.len() != 1 {
1333        return Ok(None);
1334    }
1335    let var = candidates[0];
1336
1337    let edge0_is_true = m0.get(&var).copied().unwrap_or(false);
1338    let (edge_true, edge_false) = if edge0_is_true {
1339        (0usize, 1usize)
1340    } else {
1341        (1usize, 0usize)
1342    };
1343
1344    Ok(Some((var, edge_true, edge_false)))
1345}
1346
1347#[cfg(test)]
1348mod tests {
1349    use super::*;
1350    use crate::kc::ddnnf::DecisionDnnf;
1351
1352    #[test]
1353    fn test_xgcf_matches_ddnnf_on_single_decision() {
1354        let nnf = r#"
1355o 1 0
1356t 2 0
1357f 3 0
13581 2 1 0
13591 3 -1 0
1360"#;
1361        let ddnnf = DecisionDnnf::parse_str(nnf).unwrap();
1362        let xgcf = Xgcf::from_ddnnf(&ddnnf).unwrap();
1363
1364        let p = 0.37_f64;
1365        let w = |var: u32| match var {
1366            1 => (p.ln(), (1.0 - p).ln()),
1367            _ => panic!("unexpected var {}", var),
1368        };
1369
1370        let a = ddnnf.eval_log_wmc(w).unwrap();
1371        let b = xgcf.eval_log_wmc(w).unwrap();
1372        assert!((a - b).abs() < 1e-9, "ddnnf={} xgcf={}", a, b);
1373    }
1374
1375    #[test]
1376    fn test_xgcf_matches_ddnnf_on_two_stage_decision() {
1377        // Formula: x1 OR x2, represented as a decision on x1, then x2.
1378        let nnf = r#"
1379o 1 0
1380o 2 0
1381t 3 0
1382f 4 0
13831 3 1 0
13841 2 -1 0
13852 3 2 0
13862 4 -2 0
1387"#;
1388        let ddnnf = DecisionDnnf::parse_str(nnf).unwrap();
1389        let xgcf = Xgcf::from_ddnnf(&ddnnf).unwrap();
1390
1391        let p1 = 0.2_f64;
1392        let p2 = 0.6_f64;
1393        let w = |var: u32| match var {
1394            1 => (p1.ln(), (1.0 - p1).ln()),
1395            2 => (p2.ln(), (1.0 - p2).ln()),
1396            _ => panic!("unexpected var {}", var),
1397        };
1398
1399        let a = ddnnf.eval_log_wmc(w).unwrap();
1400        let b = xgcf.eval_log_wmc(w).unwrap();
1401        assert!((a - b).abs() < 1e-9, "ddnnf={} xgcf={}", a, b);
1402    }
1403
1404    #[test]
1405    fn circuit_evaluators_share_logsumexp_contract() {
1406        let nnf = r#"
1407o 1 0
1408t 2 0
1409f 3 0
14101 2 1 0
14111 3 -1 0
1412"#;
1413        let ddnnf = DecisionDnnf::parse_str(nnf).unwrap();
1414        let xgcf = Xgcf::from_ddnnf(&ddnnf).unwrap();
1415
1416        let ddnnf_error = ddnnf
1417            .eval_log_wmc(|_| (f64::NAN, f64::NEG_INFINITY))
1418            .expect_err("Decision-DNNF must reject NaN weights");
1419        let xgcf_error = xgcf
1420            .eval_log_wmc(|_| (f64::NAN, f64::NEG_INFINITY))
1421            .expect_err("XGCF must reject NaN weights");
1422        let weights = vec![(0.0, 0.0), (f64::NAN, f64::NEG_INFINITY)];
1423        let gradient_error = xgcf
1424            .eval_log_wmc_and_grads(&weights)
1425            .expect_err("XGCF gradient evaluation must reject NaN weights");
1426
1427        for (name, error) in [
1428            ("Decision-DNNF", ddnnf_error),
1429            ("XGCF", xgcf_error),
1430            ("XGCF gradients", gradient_error),
1431        ] {
1432            assert!(
1433                matches!(&error, XlogError::Compilation(message) if message.contains("NaN")),
1434                "{name}: unexpected error: {error}"
1435            );
1436        }
1437
1438        let ddnnf_log_z = ddnnf.eval_log_wmc(|_| (f64::INFINITY, -1.0)).unwrap();
1439        let xgcf_log_z = xgcf.eval_log_wmc(|_| (f64::INFINITY, -1.0)).unwrap();
1440        let weights = vec![(0.0, 0.0), (f64::INFINITY, -1.0)];
1441        let gradient_error = xgcf
1442            .eval_log_wmc_and_grads(&weights)
1443            .expect_err("Decision normalization must reject non-finite gradients");
1444
1445        for (name, value) in [("Decision-DNNF", ddnnf_log_z), ("XGCF", xgcf_log_z)] {
1446            assert!(
1447                value.is_infinite() && value.is_sign_positive(),
1448                "{name}: expected positive infinity, got {value}"
1449            );
1450        }
1451        assert!(
1452            matches!(&gradient_error, XlogError::Compilation(message) if message.contains("gradient") && message.contains("non-finite")),
1453            "XGCF gradients: unexpected error: {gradient_error}"
1454        );
1455    }
1456
1457    #[test]
1458    fn deterministic_circuit_evaluators_reject_nan_weights() {
1459        let nnf = r#"
1460a 1 0
1461t 2 0
14621 2 1 0
1463"#;
1464        let ddnnf = DecisionDnnf::parse_str(nnf).unwrap();
1465        let xgcf = Xgcf::from_ddnnf(&ddnnf).unwrap();
1466
1467        let ddnnf_error = ddnnf
1468            .eval_log_wmc(|_| (f64::NAN, 0.0))
1469            .expect_err("deterministic Decision-DNNF must reject NaN weights");
1470        let xgcf_error = xgcf
1471            .eval_log_wmc(|_| (f64::NAN, 0.0))
1472            .expect_err("deterministic XGCF must reject NaN weights");
1473        let gradient_error = xgcf
1474            .eval_log_wmc_and_grads(&[(0.0, 0.0), (f64::NAN, 0.0)])
1475            .expect_err("deterministic XGCF gradients must reject NaN weights");
1476
1477        for (name, error) in [
1478            ("Decision-DNNF", ddnnf_error),
1479            ("XGCF", xgcf_error),
1480            ("XGCF gradients", gradient_error),
1481        ] {
1482            assert!(
1483                matches!(&error, XlogError::Compilation(message) if message.contains("NaN")),
1484                "{name}: unexpected error: {error}"
1485            );
1486        }
1487    }
1488
1489    #[test]
1490    fn deterministic_circuit_evaluators_reject_computed_nan() {
1491        let nnf = r#"
1492a 1 0
1493t 2 0
14941 2 1 -2 0
1495"#;
1496        let ddnnf = DecisionDnnf::parse_str(nnf).unwrap();
1497        let xgcf = Xgcf::from_ddnnf(&ddnnf).unwrap();
1498        let weights = [(0.0, 0.0), (f64::INFINITY, 0.0), (0.0, f64::NEG_INFINITY)];
1499        let weight = |var: u32| weights[var as usize];
1500
1501        let ddnnf_error = ddnnf
1502            .eval_log_wmc(weight)
1503            .expect_err("Decision-DNNF must reject +inf + -inf");
1504        let xgcf_error = xgcf
1505            .eval_log_wmc(weight)
1506            .expect_err("XGCF must reject +inf + -inf");
1507        let gradient_error = xgcf
1508            .eval_log_wmc_and_grads(&weights)
1509            .expect_err("XGCF gradients must reject computed NaN");
1510
1511        for (name, error) in [("Decision-DNNF", ddnnf_error), ("XGCF", xgcf_error)] {
1512            assert!(
1513                matches!(&error, XlogError::Compilation(message) if message.contains("NaN")),
1514                "{name}: unexpected error: {error}"
1515            );
1516        }
1517        assert!(
1518            matches!(&gradient_error, XlogError::Compilation(message) if message.contains("NaN")),
1519            "XGCF gradients: unexpected error: {gradient_error}"
1520        );
1521    }
1522
1523    #[test]
1524    fn gradient_evaluation_rejects_nan_when_another_weight_is_positive_infinity() {
1525        let nnf = r#"
1526a 1 0
1527t 2 0
15281 2 1 2 0
1529"#;
1530        let ddnnf = DecisionDnnf::parse_str(nnf).unwrap();
1531        let xgcf = Xgcf::from_ddnnf(&ddnnf).unwrap();
1532        let weights = [(0.0, 0.0), (f64::INFINITY, 0.0), (f64::NAN, 0.0)];
1533
1534        let error = xgcf
1535            .eval_log_wmc_and_grads(&weights)
1536            .expect_err("NaN must take precedence over positive infinity");
1537        assert!(
1538            matches!(&error, XlogError::Compilation(message) if message.contains("NaN")),
1539            "unexpected error: {error}"
1540        );
1541    }
1542
1543    #[test]
1544    fn reserved_weight_slot_zero_does_not_affect_value_or_gradients() {
1545        let nnf = r#"
1546a 1 0
1547t 2 0
15481 2 1 0
1549"#;
1550        let xgcf = Xgcf::from_ddnnf(&DecisionDnnf::parse_str(nnf).unwrap()).unwrap();
1551        let baseline_weights = [(0.0, 0.0), (-0.25, -0.75)];
1552        let reserved_non_finite = [(f64::NAN, f64::INFINITY), (-0.25, -0.75)];
1553
1554        let baseline_value = xgcf
1555            .eval_log_wmc(|var| baseline_weights[var as usize])
1556            .unwrap();
1557        let reserved_value = xgcf
1558            .eval_log_wmc(|var| reserved_non_finite[var as usize])
1559            .unwrap();
1560        assert_eq!(reserved_value, baseline_value);
1561
1562        let baseline_grads = xgcf.eval_log_wmc_and_grads(&baseline_weights).unwrap();
1563        let reserved_grads = xgcf
1564            .eval_log_wmc_and_grads(&reserved_non_finite)
1565            .expect("reserved slot 0 must not be validated or consumed");
1566        assert_eq!(reserved_grads, baseline_grads);
1567    }
1568
1569    #[test]
1570    fn deterministic_gradient_allows_selected_and_unused_positive_infinity() {
1571        let nnf = r#"
1572a 1 0
1573t 2 0
15741 2 3 0
1575"#;
1576        let xgcf = Xgcf::from_ddnnf(&DecisionDnnf::parse_str(nnf).unwrap()).unwrap();
1577        let weights = [
1578            (0.0, 0.0),
1579            (f64::INFINITY, f64::INFINITY),
1580            (f64::INFINITY, f64::INFINITY),
1581            (f64::INFINITY, f64::INFINITY),
1582        ];
1583
1584        let (log_z, grad_true, grad_false) = xgcf
1585            .eval_log_wmc_and_grads(&weights)
1586            .expect("+inf is valid when deterministic backward outputs stay finite");
1587        assert!(log_z.is_infinite() && log_z.is_sign_positive());
1588        assert_eq!(grad_true, vec![0.0, 0.0, 0.0, 1.0]);
1589        assert_eq!(grad_false, vec![0.0; 4]);
1590    }
1591}