1use crate::expr::Expression;
14use crate::search::Match;
15use crate::thresholds::{
16 ACCEPT_ERROR_RELAX_CAPACITY_FRACTION, ACCEPT_ERROR_TIGHTEN_FACTOR, BEST_ERROR_TIGHTEN_FACTOR,
17 EXACT_MATCH_TOLERANCE, NEWTON_TOLERANCE, STRICT_GATE_CAPACITY_FRACTION, STRICT_GATE_FACTOR,
18};
19use std::cmp::Ordering;
20use std::collections::{BinaryHeap, HashSet};
21
22#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
24pub enum RankingMode {
25 #[default]
27 Complexity,
28 Parity,
30}
31
32#[derive(Clone, PartialEq, Eq, Hash)]
37pub struct EqnKey {
38 lhs: Expression,
40 rhs: Expression,
42}
43
44impl EqnKey {
45 #[inline]
47 pub fn from_match(m: &Match) -> Self {
48 Self {
49 lhs: m.lhs.expr.clone(),
50 rhs: m.rhs.expr.clone(),
51 }
52 }
53}
54
55#[derive(Clone, PartialEq, Eq, Hash)]
59pub struct LhsKey {
60 lhs: Expression,
62}
63
64impl LhsKey {
65 #[inline]
67 pub fn from_match(m: &Match) -> Self {
68 Self {
69 lhs: m.lhs.expr.clone(),
70 }
71 }
72}
73
74#[derive(Clone, PartialEq, Eq, Hash)]
79pub struct SignatureKey {
80 key: Box<[u8]>,
82}
83
84impl SignatureKey {
85 pub fn from_match(m: &Match) -> Self {
86 let expected_len = m.lhs.expr.len() + m.rhs.expr.len() + 1;
88 let mut ops = Vec::with_capacity(expected_len);
89
90 for sym in m.lhs.expr.symbols() {
91 ops.push(*sym as u8);
92 }
93 ops.push(b'=');
94 for sym in m.rhs.expr.symbols() {
95 ops.push(*sym as u8);
96 }
97
98 Self {
99 key: ops.into_boxed_slice(),
100 }
101 }
102}
103
104pub fn legacy_parity_score_expr(expr: &Expression) -> i32 {
106 expr.symbols().iter().fold(0_i32, |acc, sym| {
107 acc.saturating_add(sym.legacy_parity_weight())
108 })
109}
110
111pub fn legacy_parity_score_match(m: &Match) -> i32 {
113 legacy_parity_score_expr(&m.lhs.expr).saturating_add(legacy_parity_score_expr(&m.rhs.expr))
114}
115
116#[inline]
117fn compare_expr(a: &Expression, b: &Expression) -> Ordering {
118 a.symbols()
119 .iter()
120 .map(|s| *s as u8)
121 .cmp(b.symbols().iter().map(|s| *s as u8))
122}
123
124pub fn compare_matches(a: &Match, b: &Match, ranking_mode: RankingMode) -> Ordering {
126 let a_exactness = if a.error.abs() < EXACT_MATCH_TOLERANCE {
127 0_u8
128 } else {
129 1_u8
130 };
131 let b_exactness = if b.error.abs() < EXACT_MATCH_TOLERANCE {
132 0_u8
133 } else {
134 1_u8
135 };
136
137 let a_eff_err = if a_exactness == 0 { 0.0 } else { a.error.abs() };
143 let b_eff_err = if b_exactness == 0 { 0.0 } else { b.error.abs() };
144
145 let mut ord = a_exactness
146 .cmp(&b_exactness)
147 .then_with(|| a_eff_err.partial_cmp(&b_eff_err).unwrap_or(Ordering::Equal));
148
149 if ord != Ordering::Equal {
150 return ord;
151 }
152
153 ord = match ranking_mode {
154 RankingMode::Complexity => a.complexity.cmp(&b.complexity),
155 RankingMode::Parity => legacy_parity_score_match(a)
156 .cmp(&legacy_parity_score_match(b))
157 .then_with(|| a.complexity.cmp(&b.complexity)),
158 };
159
160 if ord != Ordering::Equal {
161 return ord;
162 }
163
164 compare_expr(&a.lhs.expr, &b.lhs.expr).then_with(|| compare_expr(&a.rhs.expr, &b.rhs.expr))
165}
166
167#[derive(Clone)]
170struct PoolEntry {
171 m: Match,
172 rank_key: (u8, i64, i32, u32), }
174
175impl PoolEntry {
176 fn new(m: Match, ranking_mode: RankingMode) -> Self {
177 let is_exact = m.error.abs() < EXACT_MATCH_TOLERANCE;
178 let exactness_rank = if is_exact { 0 } else { 1 };
179 let error_abs = m.error.abs();
183 let error_bits = if is_exact {
184 0
187 } else if error_abs.is_nan() {
188 i64::MAX
190 } else if error_abs.is_infinite() {
191 i64::MAX - 1
193 } else {
194 error_abs.to_bits() as i64
196 };
197 let mode_tie = match ranking_mode {
198 RankingMode::Complexity => m.complexity as i32,
199 RankingMode::Parity => legacy_parity_score_match(&m),
200 };
201 Self {
202 rank_key: (exactness_rank, error_bits, mode_tie, m.complexity),
203 m,
204 }
205 }
206}
207
208impl PartialEq for PoolEntry {
209 fn eq(&self, other: &Self) -> bool {
210 self.rank_key == other.rank_key
211 && self.m.lhs.expr == other.m.lhs.expr
212 && self.m.rhs.expr == other.m.rhs.expr
213 }
214}
215
216impl Eq for PoolEntry {}
217
218impl PartialOrd for PoolEntry {
219 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
220 Some(self.cmp(other))
221 }
222}
223
224impl Ord for PoolEntry {
225 fn cmp(&self, other: &Self) -> Ordering {
226 self.rank_key
228 .cmp(&other.rank_key)
229 .then_with(|| compare_expr(&self.m.lhs.expr, &other.m.lhs.expr))
230 .then_with(|| compare_expr(&self.m.rhs.expr, &other.m.rhs.expr))
231 }
232}
233
234#[derive(Clone, Debug, Default)]
236pub struct PoolStats {
237 pub insertions: usize,
239 pub rejections_error: usize,
241 pub rejections_dedupe: usize,
243 pub evictions: usize,
245}
246
247pub struct TopKPool {
249 capacity: usize,
251 heap: BinaryHeap<PoolEntry>,
253 seen_eqn: HashSet<EqnKey>,
255 pub best_error: f64,
257 pub accept_error: f64,
259 pub stats: PoolStats,
261 show_db_adds: bool,
263 ranking_mode: RankingMode,
265}
266
267impl TopKPool {
268 #[allow(dead_code)]
270 pub fn new(capacity: usize, initial_max_error: f64) -> Self {
271 Self {
272 capacity,
273 heap: BinaryHeap::with_capacity(capacity + 1),
274 seen_eqn: HashSet::new(),
275 best_error: initial_max_error,
276 accept_error: initial_max_error,
277 stats: PoolStats::default(),
278 show_db_adds: false,
279 ranking_mode: RankingMode::Complexity,
280 }
281 }
282
283 pub fn new_with_diagnostics(
285 capacity: usize,
286 initial_max_error: f64,
287 show_db_adds: bool,
288 ranking_mode: RankingMode,
289 ) -> Self {
290 Self {
291 capacity,
292 heap: BinaryHeap::with_capacity(capacity + 1),
293 seen_eqn: HashSet::new(),
294 best_error: initial_max_error,
295 accept_error: initial_max_error,
296 stats: PoolStats::default(),
297 show_db_adds,
298 ranking_mode,
299 }
300 }
301
302 pub fn try_insert(&mut self, m: Match) -> bool {
305 let error = m.error.abs();
306 let is_exact = error < EXACT_MATCH_TOLERANCE;
307
308 if !is_exact && error > self.accept_error {
310 self.stats.rejections_error += 1;
311 return false;
312 }
313
314 let eqn_key = EqnKey::from_match(&m);
316 if self.seen_eqn.contains(&eqn_key) {
317 self.stats.rejections_dedupe += 1;
318 return false;
319 }
320
321 let entry = PoolEntry::new(m, self.ranking_mode);
323 self.seen_eqn.insert(eqn_key);
324
325 if self.show_db_adds {
327 eprintln!(
328 " [db add] lhs={:?} rhs={:?} error={:.6e} complexity={}",
329 entry.m.lhs.expr.to_postfix(),
330 entry.m.rhs.expr.to_postfix(),
331 entry.m.error,
332 entry.m.complexity
333 );
334 }
335
336 self.heap.push(entry);
337 self.stats.insertions += 1;
338
339 if is_exact {
341 self.best_error =
343 EXACT_MATCH_TOLERANCE.max(self.best_error * BEST_ERROR_TIGHTEN_FACTOR);
344 } else if error < self.best_error {
345 self.best_error = error * BEST_ERROR_TIGHTEN_FACTOR - NEWTON_TOLERANCE;
347 self.best_error = self.best_error.max(EXACT_MATCH_TOLERANCE);
348 }
349
350 if error < self.accept_error * ACCEPT_ERROR_TIGHTEN_FACTOR {
352 self.accept_error *= ACCEPT_ERROR_TIGHTEN_FACTOR;
353 }
354
355 if self.heap.len() > self.capacity {
357 if let Some(evicted) = self.heap.pop() {
358 self.seen_eqn.remove(&EqnKey::from_match(&evicted.m));
361 self.stats.evictions += 1;
362 self.relax_accept_error_after_eviction();
363 }
364 }
365
366 true
367 }
368
369 fn relax_accept_error_after_eviction(&mut self) {
371 if self.heap.is_empty() {
372 return;
373 }
374
375 let relax_threshold =
376 (self.capacity as f64 * ACCEPT_ERROR_RELAX_CAPACITY_FRACTION).ceil() as usize;
377 if self.heap.len() >= relax_threshold {
378 return;
379 }
380
381 if let Some(worst) = self.heap.peek() {
382 let worst_error = worst.m.error.abs();
383 if worst_error >= EXACT_MATCH_TOLERANCE {
384 self.accept_error = self
385 .accept_error
386 .max(worst_error)
387 .max(self.accept_error / ACCEPT_ERROR_TIGHTEN_FACTOR);
388 }
389 }
390 }
391
392 #[inline]
404 pub fn exact_complexity_ceiling(&self) -> Option<u32> {
405 if self.heap.len() < self.capacity {
406 return None;
407 }
408 let worst = self.heap.peek()?;
409 if worst.m.error.abs() < EXACT_MATCH_TOLERANCE {
410 Some(worst.m.complexity)
411 } else {
412 None
413 }
414 }
415
416 pub fn would_accept(&self, error: f64, is_exact: bool) -> bool {
418 if is_exact {
419 return true;
420 }
421 error <= self.accept_error
422 }
423
424 pub fn would_accept_strict(&self, coarse_error: f64, is_potentially_exact: bool) -> bool {
427 if is_potentially_exact {
429 return true;
430 }
431
432 if coarse_error > self.accept_error {
434 return false;
435 }
436
437 if self.heap.len() as f64 >= self.capacity as f64 * STRICT_GATE_CAPACITY_FRACTION {
440 if coarse_error > self.accept_error * STRICT_GATE_FACTOR {
443 return false;
444 }
445 }
446
447 true
448 }
449
450 pub fn into_sorted(self) -> Vec<Match> {
452 let ranking_mode = self.ranking_mode;
453 let mut matches: Vec<Match> = self.heap.into_iter().map(|e| e.m).collect();
454 matches.sort_by(|a, b| compare_matches(a, b, ranking_mode));
455 matches
456 }
457
458 pub fn len(&self) -> usize {
460 self.heap.len()
461 }
462
463 #[allow(dead_code)]
465 pub fn is_empty(&self) -> bool {
466 self.heap.is_empty()
467 }
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473 use crate::expr::{EvaluatedExpr, Expression};
474 use crate::symbol::NumType;
475
476 fn make_match(lhs: &str, rhs: &str, error: f64, complexity: u32) -> Match {
477 let lhs_expr = Expression::parse(lhs).unwrap();
478 let rhs_expr = Expression::parse(rhs).unwrap();
479 Match {
480 lhs: EvaluatedExpr::new(lhs_expr, 0.0, 1.0, NumType::Integer),
481 rhs: EvaluatedExpr::new(rhs_expr, 0.0, 0.0, NumType::Integer),
482 x_value: 2.5,
483 error,
484 complexity,
485 }
486 }
487
488 #[test]
489 fn test_pool_basic() {
490 let mut pool = TopKPool::new(5, 1.0);
491
492 assert!(pool.try_insert(make_match("2x*", "5", 0.0, 27)));
494 assert!(pool.try_insert(make_match("x1+", "35/", 0.01, 34)));
495
496 assert_eq!(pool.len(), 2);
497 }
498
499 #[test]
500 fn test_pool_eviction() {
501 let mut pool = TopKPool::new(2, 1.0);
502
503 pool.try_insert(make_match("xs", "64/", 0.1, 50));
508 pool.try_insert(make_match("2x*", "5", 0.0, 27));
509 pool.try_insert(make_match("x1+", "35/", 0.01, 34));
510
511 assert_eq!(pool.len(), 2);
513
514 let sorted = pool.into_sorted();
515 let remaining: Vec<_> = sorted.iter().map(|m| m.lhs.expr.to_postfix()).collect();
518 assert!(
519 remaining.contains(&"2x*".to_string()),
520 "Expected 2x* to remain, got: {:?}",
521 remaining
522 );
523 assert!(
524 remaining.contains(&"x1+".to_string()),
525 "Expected x1+ to remain, got: {:?}",
526 remaining
527 );
528 }
529
530 #[test]
531 fn test_pool_dedupe() {
532 let mut pool = TopKPool::new(10, 1.0);
533
534 assert!(pool.try_insert(make_match("2x*", "5", 0.0, 27)));
536 assert!(!pool.try_insert(make_match("2x*", "5", 0.0, 27)));
537
538 assert_eq!(pool.len(), 1);
539 }
540
541 #[test]
542 fn test_parity_score_prefers_operator_dense_form() {
543 let low_operator = make_match("2x*", "5", 1e-6, 10);
544 let high_operator = make_match("x1+", "3", 1e-6, 20);
545
546 let low_score = legacy_parity_score_match(&low_operator);
547 let high_score = legacy_parity_score_match(&high_operator);
548 assert!(
549 high_score < low_score,
550 "expected operator-dense form to have lower legacy parity score ({} vs {})",
551 high_score,
552 low_score
553 );
554 }
555
556 #[test]
557 fn test_parity_ranking_changes_ordering() {
558 let low_operator = make_match("2x*", "5", 1e-6, 10);
559 let high_operator = make_match("x1+", "3", 1e-6, 20);
560
561 let mut complexity_pool =
563 TopKPool::new_with_diagnostics(10, 1.0, false, RankingMode::Complexity);
564 complexity_pool.try_insert(low_operator.clone());
565 complexity_pool.try_insert(high_operator.clone());
566 let complexity_sorted = complexity_pool.into_sorted();
567 assert_eq!(complexity_sorted[0].lhs.expr.to_postfix(), "2x*");
568
569 let mut parity_pool = TopKPool::new_with_diagnostics(10, 1.0, false, RankingMode::Parity);
571 parity_pool.try_insert(low_operator);
572 parity_pool.try_insert(high_operator);
573 let parity_sorted = parity_pool.into_sorted();
574 assert_eq!(parity_sorted[0].lhs.expr.to_postfix(), "x1+");
575 }
576
577 #[test]
578 fn test_pool_handles_nan_and_infinity_errors() {
579 let mut pool = TopKPool::new(10, f64::INFINITY);
580
581 let normal = make_match("x", "1", 0.01, 25);
583 assert!(pool.try_insert(normal));
584
585 let infinite = make_match("x1+", "2", f64::INFINITY, 30);
587 assert!(pool.try_insert(infinite));
588
589 let nan_match = make_match("x2*", "3", f64::NAN, 35);
591 assert!(pool.try_insert(nan_match));
592
593 assert_eq!(pool.len(), 3);
595
596 let sorted = pool.into_sorted();
598 assert_eq!(sorted[0].lhs.expr.to_postfix(), "x");
599 }
600
601 #[test]
602 fn test_pool_relaxes_accept_error_after_eviction_when_under_half_capacity() {
603 let mut pool = TopKPool::new(4, 1.0);
604
605 pool.try_insert(make_match("x", "1", 0.5, 10));
606 pool.try_insert(make_match("x1+", "2", 0.4, 20));
607 pool.try_insert(make_match("x2*", "3", 0.3, 30));
608 pool.try_insert(make_match("x3/", "4", 0.2, 40));
609 pool.try_insert(make_match("x4-", "5", 0.1, 50));
610
611 assert_eq!(pool.len(), 4);
612 assert!(
613 pool.accept_error >= 0.2,
614 "expected accept_error to relax after eviction, got {}",
615 pool.accept_error
616 );
617 }
618
619 #[test]
620 fn test_exact_matches_rank_by_complexity_not_residual() {
621 let simpler = make_match("x", "2p*", 5e-15, 46);
625 let complex = make_match("x1+", "2p*1+", 1e-16, 80);
626
627 let mut pool = TopKPool::new(10, 1.0);
628 pool.try_insert(complex);
629 pool.try_insert(simpler);
630
631 let sorted = pool.into_sorted();
632 assert_eq!(
633 sorted[0].complexity, 46,
634 "simpler exact match should rank first"
635 );
636 }
637
638 #[test]
639 fn test_exact_complexity_ceiling_only_when_saturated_with_exacts() {
640 let mut pool = TopKPool::new(2, 1.0);
641
642 pool.try_insert(make_match("x", "1", 0.0, 30));
644 assert_eq!(pool.exact_complexity_ceiling(), None);
645
646 pool.try_insert(make_match("x1+", "2", 0.0, 50));
648 assert_eq!(pool.exact_complexity_ceiling(), Some(50));
649
650 pool.try_insert(make_match("x2*", "3", 0.0, 40));
652 assert_eq!(pool.exact_complexity_ceiling(), Some(40));
653 }
654
655 #[test]
656 fn test_exact_complexity_ceiling_absent_when_worst_is_inexact() {
657 let mut pool = TopKPool::new(2, 1.0);
658 pool.try_insert(make_match("x", "1", 0.0, 30));
659 pool.try_insert(make_match("x1+", "2", 0.01, 20));
660
661 assert_eq!(pool.exact_complexity_ceiling(), None);
664 }
665
666 #[test]
667 fn test_pool_entry_distinct_with_same_rank_key() {
668 let mut pool = TopKPool::new(10, 1.0);
671
672 let m1 = make_match("x", "1", 0.0, 25);
674 let m2 = make_match("x1-", "1", 0.0, 25);
675
676 assert!(pool.try_insert(m1));
677 assert!(pool.try_insert(m2));
678
679 assert_eq!(pool.len(), 2);
681 }
682}