1use std::collections::{BTreeMap, BTreeSet, VecDeque};
10
11use crate::grammar::{Grammar, GrammarExpr, GrammarFormat, GrammarRule};
12use crate::semantics::ProbabilisticTruthValue;
13
14pub type Symbol = String;
16
17#[derive(Clone, Debug, Default, PartialEq, Eq)]
19pub struct Sample {
20 pub positives: Vec<Vec<Symbol>>,
22 pub negatives: Vec<Vec<Symbol>>,
24}
25
26impl Sample {
27 #[must_use]
29 pub const fn new(positives: Vec<Vec<Symbol>>, negatives: Vec<Vec<Symbol>>) -> Self {
30 Self {
31 positives,
32 negatives,
33 }
34 }
35
36 #[must_use]
38 pub const fn positive_only(positives: Vec<Vec<Symbol>>) -> Self {
39 Self {
40 positives,
41 negatives: Vec::new(),
42 }
43 }
44}
45
46#[derive(Clone, Debug, Default, PartialEq, Eq)]
48pub struct AptaState {
49 pub accepting: bool,
51 pub rejecting: bool,
53 pub arrival_count: u64,
55 pub final_count: u64,
57}
58
59#[derive(Clone, Debug, PartialEq, Eq)]
61pub struct Apta {
62 pub states: Vec<AptaState>,
64 pub transitions: Vec<BTreeMap<Symbol, usize>>,
66 pub transition_counts: Vec<BTreeMap<Symbol, u64>>,
68}
69
70impl Apta {
71 #[must_use]
73 pub fn from_sample(sample: &Sample) -> Self {
74 let mut apta = Self::new();
75
76 for positive in &sample.positives {
77 apta.insert(positive, true);
78 }
79 for negative in &sample.negatives {
80 apta.insert(negative, false);
81 }
82
83 apta
84 }
85
86 fn new() -> Self {
87 Self {
88 states: vec![AptaState::default()],
89 transitions: vec![BTreeMap::new()],
90 transition_counts: vec![BTreeMap::new()],
91 }
92 }
93
94 fn insert(&mut self, symbols: &[Symbol], positive: bool) {
95 let mut state = 0usize;
96 if positive {
97 self.states[state].arrival_count = self.states[state].arrival_count.saturating_add(1);
98 }
99
100 for symbol in symbols {
101 let next = if let Some(next) = self.transitions[state].get(symbol).copied() {
102 next
103 } else {
104 let next = self.states.len();
105 self.states.push(AptaState::default());
106 self.transitions.push(BTreeMap::new());
107 self.transition_counts.push(BTreeMap::new());
108 self.transitions[state].insert(symbol.clone(), next);
109 next
110 };
111
112 if positive {
113 *self.transition_counts[state]
114 .entry(symbol.clone())
115 .or_default() += 1;
116 self.states[next].arrival_count = self.states[next].arrival_count.saturating_add(1);
117 }
118 state = next;
119 }
120
121 if positive {
122 self.states[state].accepting = true;
123 self.states[state].final_count = self.states[state].final_count.saturating_add(1);
124 } else {
125 self.states[state].rejecting = true;
126 }
127 }
128}
129
130#[derive(Clone, Copy, Debug, PartialEq)]
132pub enum MergeStrategy {
133 Rpni,
135 Edsm,
137 Alergia {
142 alpha: f64,
144 },
145}
146
147#[derive(Clone, Debug, PartialEq, Eq)]
149pub struct InferredState {
150 pub accepting: bool,
152 pub rejecting: bool,
154 pub arrival_count: u64,
156 pub final_count: u64,
158}
159
160#[derive(Clone, Debug, PartialEq, Eq)]
162pub struct MergeEvent {
163 pub target: usize,
165 pub source: usize,
167 pub evidence: usize,
169}
170
171#[derive(Clone, Debug, PartialEq, Eq)]
177pub struct InferredAutomaton {
178 pub states: Vec<InferredState>,
180 pub transitions: Vec<BTreeMap<Symbol, usize>>,
182 pub final_probabilities: Vec<Option<ProbabilisticTruthValue>>,
184 pub transition_probabilities: Vec<BTreeMap<Symbol, ProbabilisticTruthValue>>,
186 pub merge_history: Vec<MergeEvent>,
188}
189
190impl InferredAutomaton {
191 #[must_use]
193 pub fn accepts(&self, input: &[Symbol]) -> bool {
194 let mut state = 0usize;
195
196 for symbol in input {
197 let Some(next) = self
198 .transitions
199 .get(state)
200 .and_then(|transitions| transitions.get(symbol))
201 else {
202 return false;
203 };
204 state = *next;
205 }
206
207 self.states
208 .get(state)
209 .is_some_and(|state| state.accepting && !state.rejecting)
210 }
211
212 #[must_use]
214 pub fn accepts_text(&self, text: &str) -> bool {
215 let symbols = text
216 .chars()
217 .map(|value| value.to_string())
218 .collect::<Vec<_>>();
219 self.accepts(&symbols)
220 }
221
222 #[must_use]
224 pub fn to_grammar(&self) -> Grammar {
225 let mut grammar = Grammar::new().with_source_format(GrammarFormat::Inferred);
226 if self.states.is_empty() {
227 return grammar;
228 }
229
230 for state in 0..self.states.len() {
231 grammar.add_rule(GrammarRule::new(
232 state_name(state),
233 self.state_expression(state),
234 ));
235 }
236 grammar.set_start(state_name(0));
237 grammar
238 }
239
240 fn state_expression(&self, state: usize) -> GrammarExpr {
241 let mut alternatives = Vec::new();
242
243 if self.states[state].accepting && !self.states[state].rejecting {
244 alternatives.push(GrammarExpr::Empty);
245 }
246
247 for (symbol, target) in &self.transitions[state] {
248 alternatives.push(GrammarExpr::Sequence(vec![
249 GrammarExpr::Terminal(symbol.clone()),
250 GrammarExpr::NonTerminal(state_name(*target)),
251 ]));
252 }
253
254 match alternatives.as_slice() {
255 [only] => only.clone(),
256 _ => GrammarExpr::Choice {
257 ordered: false,
258 alternatives,
259 },
260 }
261 }
262}
263
264#[must_use]
266pub fn infer_dfa(sample: &Sample, strategy: MergeStrategy) -> InferredAutomaton {
267 let apta = Apta::from_sample(sample);
268 let mut machine = WorkAutomaton::from_apta(&apta);
269 let mut merge_history = Vec::new();
270 let mut red = BTreeSet::from([0usize]);
271
272 loop {
273 red = normalised_red(&machine, &red);
274 let blue = blue_states(&machine, &red);
275 if blue.is_empty() {
276 break;
277 }
278
279 match strategy {
280 MergeStrategy::Rpni => rpni_step(&mut machine, &mut red, &blue, &mut merge_history),
281 MergeStrategy::Edsm => edsm_step(&mut machine, &mut red, &blue, &mut merge_history),
282 MergeStrategy::Alergia { alpha } => {
283 alergia_step(&mut machine, &mut red, &blue, alpha, &mut merge_history);
284 }
285 }
286 }
287
288 machine.into_inferred(
289 matches!(strategy, MergeStrategy::Alergia { .. }),
290 merge_history,
291 )
292}
293
294#[derive(Clone, Debug, PartialEq, Eq)]
295struct WorkState {
296 accepting: bool,
297 rejecting: bool,
298 arrival_count: u64,
299 final_count: u64,
300 rank: usize,
301 active: bool,
302}
303
304#[derive(Clone, Debug, PartialEq, Eq)]
305struct WorkAutomaton {
306 states: Vec<WorkState>,
307 transitions: Vec<BTreeMap<Symbol, usize>>,
308 transition_counts: Vec<BTreeMap<Symbol, u64>>,
309 parent: Vec<usize>,
310}
311
312impl WorkAutomaton {
313 fn from_apta(apta: &Apta) -> Self {
314 let ranks = canonical_ranks(apta);
315 Self {
316 states: apta
317 .states
318 .iter()
319 .enumerate()
320 .map(|(index, state)| WorkState {
321 accepting: state.accepting,
322 rejecting: state.rejecting,
323 arrival_count: state.arrival_count,
324 final_count: state.final_count,
325 rank: ranks[index],
326 active: true,
327 })
328 .collect(),
329 transitions: apta.transitions.clone(),
330 transition_counts: apta.transition_counts.clone(),
331 parent: (0..apta.states.len()).collect(),
332 }
333 }
334
335 fn representative(&self, mut state: usize) -> usize {
336 while self.parent[state] != state {
337 state = self.parent[state];
338 }
339 state
340 }
341
342 fn active_representative(&self, state: usize) -> Option<usize> {
343 let representative = self.representative(state);
344 self.states
345 .get(representative)
346 .filter(|state| state.active)
347 .map(|_| representative)
348 }
349
350 fn active_sorted(&self) -> Vec<usize> {
351 let mut states = self
352 .states
353 .iter()
354 .enumerate()
355 .filter_map(|(index, state)| state.active.then_some(index))
356 .collect::<Vec<_>>();
357 states.sort_by_key(|state| (self.states[*state].rank, *state));
358 states
359 }
360
361 fn red_sorted(&self, red: &BTreeSet<usize>) -> Vec<usize> {
362 let mut states = red
363 .iter()
364 .filter_map(|state| self.active_representative(*state))
365 .collect::<BTreeSet<_>>()
366 .into_iter()
367 .collect::<Vec<_>>();
368 states.sort_by_key(|state| (self.states[*state].rank, *state));
369 states
370 }
371
372 fn try_merge(&self, target: usize, source: usize, alpha: Option<f64>) -> Option<MergeAttempt> {
373 if target == source {
374 return None;
375 }
376
377 if let Some(alpha) = alpha {
378 let mut seen = BTreeSet::new();
379 if !self.alergia_compatible(target, source, alpha, &mut seen) {
380 return None;
381 }
382 }
383
384 let mut candidate = self.clone();
385 let mut evidence = 0usize;
386 if !candidate.merge_into(target, source, &mut evidence) {
387 return None;
388 }
389 candidate.normalise_transitions();
390
391 Some(MergeAttempt {
392 machine: candidate,
393 event: MergeEvent {
394 target,
395 source,
396 evidence,
397 },
398 })
399 }
400
401 fn merge_into(&mut self, target: usize, source: usize, evidence: &mut usize) -> bool {
402 let target = self.representative(target);
403 let source = self.representative(source);
404 if target == source {
405 return true;
406 }
407 if !self.states[target].active || !self.states[source].active {
408 return false;
409 }
410
411 if (self.states[target].accepting && self.states[source].rejecting)
412 || (self.states[target].rejecting && self.states[source].accepting)
413 {
414 return false;
415 }
416 if self.states[target].accepting && self.states[source].accepting {
417 *evidence = evidence.saturating_add(1);
418 }
419 if self.states[target].rejecting && self.states[source].rejecting {
420 *evidence = evidence.saturating_add(1);
421 }
422
423 self.states[target].accepting |= self.states[source].accepting;
424 self.states[target].rejecting |= self.states[source].rejecting;
425 self.states[target].arrival_count = self.states[target]
426 .arrival_count
427 .saturating_add(self.states[source].arrival_count);
428 self.states[target].final_count = self.states[target]
429 .final_count
430 .saturating_add(self.states[source].final_count);
431
432 let source_transitions = self.transitions[source].clone();
433 let source_counts = self.transition_counts[source].clone();
434 self.states[source].active = false;
435 self.parent[source] = target;
436
437 for (symbol, source_next) in source_transitions {
438 let source_next = self.representative(source_next);
439 let source_count = source_counts.get(&symbol).copied().unwrap_or_default();
440
441 if let Some(target_next) = self.transitions[target].get(&symbol).copied() {
442 let target_next = self.representative(target_next);
443 *self.transition_counts[target]
444 .entry(symbol.clone())
445 .or_default() += source_count;
446
447 if target_next != source_next
448 && !self.merge_into(target_next, source_next, evidence)
449 {
450 return false;
451 }
452 let target_next = self.representative(target_next);
453 self.transitions[target].insert(symbol, target_next);
454 } else {
455 self.transitions[target].insert(symbol.clone(), source_next);
456 self.transition_counts[target].insert(symbol, source_count);
457 }
458 }
459
460 self.transitions[source].clear();
461 self.transition_counts[source].clear();
462 true
463 }
464
465 fn normalise_transitions(&mut self) {
466 for state in 0..self.states.len() {
467 if !self.states[state].active {
468 continue;
469 }
470 let transitions = self.transitions[state].clone();
471 self.transitions[state].clear();
472 for (symbol, target) in transitions {
473 let target = self.representative(target);
474 self.transitions[state].insert(symbol, target);
475 }
476 }
477 }
478
479 fn alergia_compatible(
480 &self,
481 left: usize,
482 right: usize,
483 alpha: f64,
484 seen: &mut BTreeSet<(usize, usize)>,
485 ) -> bool {
486 let left = self.representative(left);
487 let right = self.representative(right);
488 if left == right {
489 return true;
490 }
491
492 let key = if left < right {
493 (left, right)
494 } else {
495 (right, left)
496 };
497 if !seen.insert(key) {
498 return true;
499 }
500
501 let left_state = &self.states[left];
502 let right_state = &self.states[right];
503 if (left_state.accepting && right_state.rejecting)
504 || (left_state.rejecting && right_state.accepting)
505 {
506 return false;
507 }
508
509 if !proportions_compatible(
510 left_state.final_count,
511 left_state.arrival_count,
512 right_state.final_count,
513 right_state.arrival_count,
514 alpha,
515 ) {
516 return false;
517 }
518
519 for symbol in self.outgoing_symbols(left, right) {
520 let left_count = self.transition_counts[left]
521 .get(&symbol)
522 .copied()
523 .unwrap_or_default();
524 let right_count = self.transition_counts[right]
525 .get(&symbol)
526 .copied()
527 .unwrap_or_default();
528
529 if !proportions_compatible(
530 left_count,
531 left_state.arrival_count,
532 right_count,
533 right_state.arrival_count,
534 alpha,
535 ) {
536 return false;
537 }
538
539 if let (Some(left_target), Some(right_target)) = (
540 self.transitions[left].get(&symbol).copied(),
541 self.transitions[right].get(&symbol).copied(),
542 ) && !self.alergia_compatible(left_target, right_target, alpha, seen)
543 {
544 return false;
545 }
546 }
547
548 true
549 }
550
551 fn outgoing_symbols(&self, left: usize, right: usize) -> BTreeSet<Symbol> {
552 self.transitions[left]
553 .keys()
554 .chain(self.transitions[right].keys())
555 .chain(self.transition_counts[left].keys())
556 .chain(self.transition_counts[right].keys())
557 .cloned()
558 .collect()
559 }
560
561 fn productive_states(&self) -> BTreeSet<usize> {
562 let mut reverse = vec![Vec::<usize>::new(); self.states.len()];
563 for state in self.active_sorted() {
564 for target in self.transitions[state].values() {
565 let target = self.representative(*target);
566 if self.states[target].active {
567 reverse[target].push(state);
568 }
569 }
570 }
571
572 let mut productive = BTreeSet::new();
573 let mut queue = self
574 .active_sorted()
575 .into_iter()
576 .filter(|state| self.states[*state].accepting && !self.states[*state].rejecting)
577 .collect::<VecDeque<_>>();
578
579 while let Some(state) = queue.pop_front() {
580 if !productive.insert(state) {
581 continue;
582 }
583 for predecessor in &reverse[state] {
584 queue.push_back(*predecessor);
585 }
586 }
587
588 productive
589 }
590
591 fn into_inferred(
592 self,
593 include_probabilities: bool,
594 merge_history: Vec<MergeEvent>,
595 ) -> InferredAutomaton {
596 let productive = self.productive_states();
597 let mut active = self
598 .active_sorted()
599 .into_iter()
600 .filter(|state| productive.contains(state) || *state == self.representative(0))
601 .collect::<Vec<_>>();
602 if active.is_empty() {
603 active.push(self.representative(0));
604 }
605
606 let mut state_map = vec![None; self.states.len()];
607 for (new, old) in active.iter().enumerate() {
608 state_map[*old] = Some(new);
609 }
610
611 let mut states = Vec::new();
612 let mut transitions = Vec::new();
613 let mut final_probabilities = Vec::new();
614 let mut transition_probabilities = Vec::new();
615
616 for old in active {
617 let state = &self.states[old];
618 states.push(InferredState {
619 accepting: state.accepting && !state.rejecting,
620 rejecting: state.rejecting,
621 arrival_count: state.arrival_count,
622 final_count: state.final_count,
623 });
624
625 let mut remapped_transitions = BTreeMap::new();
626 let mut remapped_probabilities = BTreeMap::new();
627 for (symbol, target) in &self.transitions[old] {
628 let target = self.representative(*target);
629 let Some(target) = state_map[target] else {
630 continue;
631 };
632 remapped_transitions.insert(symbol.clone(), target);
633
634 if include_probabilities && state.arrival_count > 0 {
635 let count = self.transition_counts[old]
636 .get(symbol)
637 .copied()
638 .unwrap_or_default()
639 .min(state.arrival_count);
640 if let Some(probability) =
641 ProbabilisticTruthValue::from_ratio(count, state.arrival_count)
642 {
643 remapped_probabilities.insert(symbol.clone(), probability);
644 }
645 }
646 }
647 transitions.push(remapped_transitions);
648 transition_probabilities.push(remapped_probabilities);
649
650 let final_probability = if include_probabilities && state.arrival_count > 0 {
651 ProbabilisticTruthValue::from_ratio(
652 state.final_count.min(state.arrival_count),
653 state.arrival_count,
654 )
655 } else {
656 None
657 };
658 final_probabilities.push(final_probability);
659 }
660
661 InferredAutomaton {
662 states,
663 transitions,
664 final_probabilities,
665 transition_probabilities,
666 merge_history,
667 }
668 }
669}
670
671#[derive(Clone, Debug)]
672struct MergeAttempt {
673 machine: WorkAutomaton,
674 event: MergeEvent,
675}
676
677fn rpni_step(
678 machine: &mut WorkAutomaton,
679 red: &mut BTreeSet<usize>,
680 blue: &[usize],
681 merge_history: &mut Vec<MergeEvent>,
682) {
683 let blue_state = blue[0];
684 for red_state in machine.red_sorted(red) {
685 if let Some(attempt) = machine.try_merge(red_state, blue_state, None) {
686 *machine = attempt.machine;
687 merge_history.push(attempt.event);
688 return;
689 }
690 }
691
692 red.insert(blue_state);
693}
694
695fn edsm_step(
696 machine: &mut WorkAutomaton,
697 red: &mut BTreeSet<usize>,
698 blue: &[usize],
699 merge_history: &mut Vec<MergeEvent>,
700) {
701 let mut best = None::<MergeAttempt>;
702 let red_states = machine.red_sorted(red);
703
704 for blue_state in blue {
705 for red_state in &red_states {
706 let Some(attempt) = machine.try_merge(*red_state, *blue_state, None) else {
707 continue;
708 };
709
710 let is_better = match &best {
711 Some(current) => {
712 attempt.event.evidence > current.event.evidence
713 || (attempt.event.evidence == current.event.evidence
714 && merge_tie_key(&attempt.event) < merge_tie_key(¤t.event))
715 }
716 None => true,
717 };
718
719 if is_better {
720 best = Some(attempt);
721 }
722 }
723 }
724
725 if let Some(attempt) = best {
726 *machine = attempt.machine;
727 merge_history.push(attempt.event);
728 } else {
729 red.insert(blue[0]);
730 }
731}
732
733fn alergia_step(
734 machine: &mut WorkAutomaton,
735 red: &mut BTreeSet<usize>,
736 blue: &[usize],
737 alpha: f64,
738 merge_history: &mut Vec<MergeEvent>,
739) {
740 let blue_state = blue[0];
741 for red_state in machine.red_sorted(red) {
742 if let Some(attempt) = machine.try_merge(red_state, blue_state, Some(alpha)) {
743 *machine = attempt.machine;
744 merge_history.push(attempt.event);
745 return;
746 }
747 }
748
749 red.insert(blue_state);
750}
751
752const fn merge_tie_key(event: &MergeEvent) -> (usize, usize) {
753 (event.source, event.target)
754}
755
756fn normalised_red(machine: &WorkAutomaton, red: &BTreeSet<usize>) -> BTreeSet<usize> {
757 red.iter()
758 .filter_map(|state| machine.active_representative(*state))
759 .collect()
760}
761
762fn blue_states(machine: &WorkAutomaton, red: &BTreeSet<usize>) -> Vec<usize> {
763 let mut blue = BTreeSet::new();
764
765 for red_state in machine.red_sorted(red) {
766 for target in machine.transitions[red_state].values() {
767 if let Some(target) = machine.active_representative(*target)
768 && !red.contains(&target)
769 {
770 blue.insert(target);
771 }
772 }
773 }
774
775 let mut blue = blue.into_iter().collect::<Vec<_>>();
776 blue.sort_by_key(|state| (machine.states[*state].rank, *state));
777 blue
778}
779
780fn canonical_ranks(apta: &Apta) -> Vec<usize> {
781 let mut paths = vec![Vec::<Symbol>::new(); apta.states.len()];
782 let mut queue = VecDeque::from([0usize]);
783 let mut seen = BTreeSet::from([0usize]);
784
785 while let Some(state) = queue.pop_front() {
786 for (symbol, target) in &apta.transitions[state] {
787 if seen.insert(*target) {
788 paths[*target] = paths[state]
789 .iter()
790 .cloned()
791 .chain([symbol.clone()])
792 .collect();
793 queue.push_back(*target);
794 }
795 }
796 }
797
798 let mut ordered = (0..apta.states.len()).collect::<Vec<_>>();
799 ordered.sort_by_key(|state| (paths[*state].len(), paths[*state].clone(), *state));
800
801 let mut ranks = vec![0usize; apta.states.len()];
802 for (rank, state) in ordered.into_iter().enumerate() {
803 ranks[state] = rank;
804 }
805 ranks
806}
807
808fn proportions_compatible(
809 left_count: u64,
810 left_total: u64,
811 right_count: u64,
812 right_total: u64,
813 alpha: f64,
814) -> bool {
815 if left_total == 0 || right_total == 0 {
816 return left_count == right_count;
817 }
818
819 let left = ratio(left_count, left_total);
820 let right = ratio(right_count, right_total);
821 let bound = compatibility_bound(alpha, left_total, right_total);
822
823 (left - right).abs() <= bound
824}
825
826fn compatibility_bound(alpha: f64, left_total: u64, right_total: u64) -> f64 {
827 let confidence = normalised_alpha(alpha);
828 let significance = (1.0 - confidence).max(f64::MIN_POSITIVE);
829 let multiplier = (0.5 * (2.0 / significance).ln()).sqrt();
830
831 multiplier * (1.0 / count_to_f64(left_total).sqrt() + 1.0 / count_to_f64(right_total).sqrt())
832}
833
834const fn normalised_alpha(alpha: f64) -> f64 {
835 if alpha.is_finite() {
836 alpha.clamp(0.000_001, 0.999_999)
837 } else {
838 0.5
839 }
840}
841
842fn ratio(count: u64, total: u64) -> f64 {
843 count_to_f64(count) / count_to_f64(total)
844}
845
846fn count_to_f64(count: u64) -> f64 {
847 f64::from(u32::try_from(count).unwrap_or(u32::MAX))
848}
849
850fn state_name(state: usize) -> String {
851 format!("q{state}")
852}