1use 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)]
15pub enum XgcfNodeType {
17 Const0 = 0,
19 Const1 = 1,
21 Lit = 2,
23 And = 3,
25 Or = 4,
27 Decision = 5,
29}
30
31#[derive(Debug, Clone)]
32pub struct Xgcf {
34 pub node_type: Vec<XgcfNodeType>,
36 pub child_offsets: Vec<u32>,
38 pub child_indices: Vec<u32>,
40 pub lit: Vec<i32>,
42 pub decision_var: Vec<u32>,
44 pub decision_child_false: Vec<u32>,
46 pub decision_child_true: Vec<u32>,
48 pub roots: Vec<u32>,
50 pub level_offsets: Vec<u32>,
52 pub level_nodes: Vec<u32>,
54}
55
56impl Xgcf {
57 pub fn from_ddnnf(ddnnf: &DecisionDnnf) -> Result<Self> {
59 XgcfBuilder::new(ddnnf).build()
60 }
61
62 pub fn smooth_random_vars(&self, is_random_var: &[bool]) -> Result<Self> {
70 XgcfSmoother::new(self, is_random_var)?.smooth()
71 }
72
73 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 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 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 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 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}