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 ) {
543 if !self.alergia_compatible(left_target, right_target, alpha, seen) {
544 return false;
545 }
546 }
547 }
548
549 true
550 }
551
552 fn outgoing_symbols(&self, left: usize, right: usize) -> BTreeSet<Symbol> {
553 self.transitions[left]
554 .keys()
555 .chain(self.transitions[right].keys())
556 .chain(self.transition_counts[left].keys())
557 .chain(self.transition_counts[right].keys())
558 .cloned()
559 .collect()
560 }
561
562 fn productive_states(&self) -> BTreeSet<usize> {
563 let mut reverse = vec![Vec::<usize>::new(); self.states.len()];
564 for state in self.active_sorted() {
565 for target in self.transitions[state].values() {
566 let target = self.representative(*target);
567 if self.states[target].active {
568 reverse[target].push(state);
569 }
570 }
571 }
572
573 let mut productive = BTreeSet::new();
574 let mut queue = self
575 .active_sorted()
576 .into_iter()
577 .filter(|state| self.states[*state].accepting && !self.states[*state].rejecting)
578 .collect::<VecDeque<_>>();
579
580 while let Some(state) = queue.pop_front() {
581 if !productive.insert(state) {
582 continue;
583 }
584 for predecessor in &reverse[state] {
585 queue.push_back(*predecessor);
586 }
587 }
588
589 productive
590 }
591
592 fn into_inferred(
593 self,
594 include_probabilities: bool,
595 merge_history: Vec<MergeEvent>,
596 ) -> InferredAutomaton {
597 let productive = self.productive_states();
598 let mut active = self
599 .active_sorted()
600 .into_iter()
601 .filter(|state| productive.contains(state) || *state == self.representative(0))
602 .collect::<Vec<_>>();
603 if active.is_empty() {
604 active.push(self.representative(0));
605 }
606
607 let mut state_map = vec![None; self.states.len()];
608 for (new, old) in active.iter().enumerate() {
609 state_map[*old] = Some(new);
610 }
611
612 let mut states = Vec::new();
613 let mut transitions = Vec::new();
614 let mut final_probabilities = Vec::new();
615 let mut transition_probabilities = Vec::new();
616
617 for old in active {
618 let state = &self.states[old];
619 states.push(InferredState {
620 accepting: state.accepting && !state.rejecting,
621 rejecting: state.rejecting,
622 arrival_count: state.arrival_count,
623 final_count: state.final_count,
624 });
625
626 let mut remapped_transitions = BTreeMap::new();
627 let mut remapped_probabilities = BTreeMap::new();
628 for (symbol, target) in &self.transitions[old] {
629 let target = self.representative(*target);
630 let Some(target) = state_map[target] else {
631 continue;
632 };
633 remapped_transitions.insert(symbol.clone(), target);
634
635 if include_probabilities && state.arrival_count > 0 {
636 let count = self.transition_counts[old]
637 .get(symbol)
638 .copied()
639 .unwrap_or_default()
640 .min(state.arrival_count);
641 if let Some(probability) =
642 ProbabilisticTruthValue::from_ratio(count, state.arrival_count)
643 {
644 remapped_probabilities.insert(symbol.clone(), probability);
645 }
646 }
647 }
648 transitions.push(remapped_transitions);
649 transition_probabilities.push(remapped_probabilities);
650
651 let final_probability = if include_probabilities && state.arrival_count > 0 {
652 ProbabilisticTruthValue::from_ratio(
653 state.final_count.min(state.arrival_count),
654 state.arrival_count,
655 )
656 } else {
657 None
658 };
659 final_probabilities.push(final_probability);
660 }
661
662 InferredAutomaton {
663 states,
664 transitions,
665 final_probabilities,
666 transition_probabilities,
667 merge_history,
668 }
669 }
670}
671
672#[derive(Clone, Debug)]
673struct MergeAttempt {
674 machine: WorkAutomaton,
675 event: MergeEvent,
676}
677
678fn rpni_step(
679 machine: &mut WorkAutomaton,
680 red: &mut BTreeSet<usize>,
681 blue: &[usize],
682 merge_history: &mut Vec<MergeEvent>,
683) {
684 let blue_state = blue[0];
685 for red_state in machine.red_sorted(red) {
686 if let Some(attempt) = machine.try_merge(red_state, blue_state, None) {
687 *machine = attempt.machine;
688 merge_history.push(attempt.event);
689 return;
690 }
691 }
692
693 red.insert(blue_state);
694}
695
696fn edsm_step(
697 machine: &mut WorkAutomaton,
698 red: &mut BTreeSet<usize>,
699 blue: &[usize],
700 merge_history: &mut Vec<MergeEvent>,
701) {
702 let mut best = None::<MergeAttempt>;
703 let red_states = machine.red_sorted(red);
704
705 for blue_state in blue {
706 for red_state in &red_states {
707 let Some(attempt) = machine.try_merge(*red_state, *blue_state, None) else {
708 continue;
709 };
710
711 let is_better = match &best {
712 Some(current) => {
713 attempt.event.evidence > current.event.evidence
714 || (attempt.event.evidence == current.event.evidence
715 && merge_tie_key(&attempt.event) < merge_tie_key(¤t.event))
716 }
717 None => true,
718 };
719
720 if is_better {
721 best = Some(attempt);
722 }
723 }
724 }
725
726 if let Some(attempt) = best {
727 *machine = attempt.machine;
728 merge_history.push(attempt.event);
729 } else {
730 red.insert(blue[0]);
731 }
732}
733
734fn alergia_step(
735 machine: &mut WorkAutomaton,
736 red: &mut BTreeSet<usize>,
737 blue: &[usize],
738 alpha: f64,
739 merge_history: &mut Vec<MergeEvent>,
740) {
741 let blue_state = blue[0];
742 for red_state in machine.red_sorted(red) {
743 if let Some(attempt) = machine.try_merge(red_state, blue_state, Some(alpha)) {
744 *machine = attempt.machine;
745 merge_history.push(attempt.event);
746 return;
747 }
748 }
749
750 red.insert(blue_state);
751}
752
753const fn merge_tie_key(event: &MergeEvent) -> (usize, usize) {
754 (event.source, event.target)
755}
756
757fn normalised_red(machine: &WorkAutomaton, red: &BTreeSet<usize>) -> BTreeSet<usize> {
758 red.iter()
759 .filter_map(|state| machine.active_representative(*state))
760 .collect()
761}
762
763fn blue_states(machine: &WorkAutomaton, red: &BTreeSet<usize>) -> Vec<usize> {
764 let mut blue = BTreeSet::new();
765
766 for red_state in machine.red_sorted(red) {
767 for target in machine.transitions[red_state].values() {
768 if let Some(target) = machine.active_representative(*target) {
769 if !red.contains(&target) {
770 blue.insert(target);
771 }
772 }
773 }
774 }
775
776 let mut blue = blue.into_iter().collect::<Vec<_>>();
777 blue.sort_by_key(|state| (machine.states[*state].rank, *state));
778 blue
779}
780
781fn canonical_ranks(apta: &Apta) -> Vec<usize> {
782 let mut paths = vec![Vec::<Symbol>::new(); apta.states.len()];
783 let mut queue = VecDeque::from([0usize]);
784 let mut seen = BTreeSet::from([0usize]);
785
786 while let Some(state) = queue.pop_front() {
787 for (symbol, target) in &apta.transitions[state] {
788 if seen.insert(*target) {
789 paths[*target] = paths[state]
790 .iter()
791 .cloned()
792 .chain([symbol.clone()])
793 .collect();
794 queue.push_back(*target);
795 }
796 }
797 }
798
799 let mut ordered = (0..apta.states.len()).collect::<Vec<_>>();
800 ordered.sort_by_key(|state| (paths[*state].len(), paths[*state].clone(), *state));
801
802 let mut ranks = vec![0usize; apta.states.len()];
803 for (rank, state) in ordered.into_iter().enumerate() {
804 ranks[state] = rank;
805 }
806 ranks
807}
808
809fn proportions_compatible(
810 left_count: u64,
811 left_total: u64,
812 right_count: u64,
813 right_total: u64,
814 alpha: f64,
815) -> bool {
816 if left_total == 0 || right_total == 0 {
817 return left_count == right_count;
818 }
819
820 let left = ratio(left_count, left_total);
821 let right = ratio(right_count, right_total);
822 let bound = compatibility_bound(alpha, left_total, right_total);
823
824 (left - right).abs() <= bound
825}
826
827fn compatibility_bound(alpha: f64, left_total: u64, right_total: u64) -> f64 {
828 let confidence = normalised_alpha(alpha);
829 let significance = (1.0 - confidence).max(f64::MIN_POSITIVE);
830 let multiplier = (0.5 * (2.0 / significance).ln()).sqrt();
831
832 multiplier * (1.0 / count_to_f64(left_total).sqrt() + 1.0 / count_to_f64(right_total).sqrt())
833}
834
835fn normalised_alpha(alpha: f64) -> f64 {
836 if alpha.is_finite() {
837 alpha.clamp(0.000_001, 0.999_999)
838 } else {
839 0.5
840 }
841}
842
843fn ratio(count: u64, total: u64) -> f64 {
844 count_to_f64(count) / count_to_f64(total)
845}
846
847fn count_to_f64(count: u64) -> f64 {
848 f64::from(u32::try_from(count).unwrap_or(u32::MAX))
849}
850
851fn state_name(state: usize) -> String {
852 format!("q{state}")
853}