Skip to main content

rust_lstar/
observation_table.rs

1use crate::automata::{Automata, State, Transition};
2use crate::knowledge_base::KnowledgeBaseTrait;
3use crate::letter::Letter;
4use crate::query::OutputQuery;
5use crate::word::Word;
6use indexmap::IndexMap;
7use itertools::Itertools;
8use std::collections::{HashMap, HashSet};
9use std::fmt;
10use std::sync::{Arc, Mutex};
11
12/// The Observation Table is the core data structure of the L* algorithm
13/// It maintains:
14/// - D: the set of distinguishing strings (suffixes)
15/// - S: the set of short prefixes
16/// - SA: the set of long prefixes
17/// - ot_content: the table content mapping (prefix, suffix) -> output
18#[allow(non_snake_case)]
19pub struct ObservationTable {
20    pub input_letters: Vec<Letter>,
21    knowledge_base: Arc<Mutex<dyn KnowledgeBaseTrait>>,
22    pub D: Vec<Word>,
23    pub S: Vec<Word>,
24    pub SA: Vec<Word>,
25    pub ot_content: HashMap<Word, HashMap<Word, Option<Letter>>>,
26    pub initialized: bool,
27}
28
29type RowSignature = Vec<Option<Letter>>;
30type Inconsistency = (((Word, Word), Word), Word);
31
32#[allow(non_snake_case)]
33impl ObservationTable {
34    /// Create a new observation table
35    pub fn new(
36        input_letters: Vec<Letter>,
37        knowledge_base: Arc<Mutex<dyn KnowledgeBaseTrait>>,
38    ) -> Self {
39        let capacity = input_letters.len() * 4;
40        ObservationTable {
41            input_letters,
42            knowledge_base,
43            D: Vec::with_capacity(capacity),
44            S: Vec::with_capacity(capacity * 2),
45            SA: Vec::with_capacity(capacity * 4),
46            ot_content: HashMap::with_capacity(capacity),
47            initialized: false,
48        }
49    }
50
51    /// Initialize the observation table with empty string in S and all input letters in D
52    pub fn initialize(&mut self) -> Result<(), String> {
53        if self.initialized {
54            return Err("Observation table already initialized".into());
55        }
56
57        self.initialized = true;
58        self.D.clear();
59        self.S.clear();
60        self.SA.clear();
61        self.ot_content.clear();
62
63        // Add each input letter to D
64        for letter in self.input_letters.clone() {
65            let word = Word::from_letters(vec![letter]);
66            self.add_word_in_D(word)?;
67        }
68
69        // Add empty word to S
70        let empty_word = Word::new();
71        self.add_word_in_S(empty_word)?;
72
73        Ok(())
74    }
75
76    /// Serialize the observation table to a string
77    pub fn serialize(&self) -> String {
78        let mut result = String::new();
79        result.push_str("=== Observation Table ===\n");
80        result.push_str(&format!("D: {:?}\n", self.D));
81        result.push_str(&format!("S: {:?}\n", self.S));
82        result.push_str(&format!("SA: {:?}\n", self.SA));
83        result.push_str(&format!("ot: {:?}\n", self.ot_content));
84        result.push_str("\n\n\n");
85        result
86    }
87
88    /// Find an inconsistency in the observation table
89    /// Returns: Some(((prefix1, prefix2), (suffix, distinguishing_word))) when
90    /// a pair of equivalent prefixes in `S` behave differently after appending
91    /// `suffix` for some distinguishing column in `D`.
92    pub fn find_inconsistency(&self) -> Option<Inconsistency> {
93        // group equivalent rows in S by their row content
94        let mut s_with_same_rows: HashMap<RowSignature, Vec<Word>> = HashMap::new();
95        for word_in_s in &self.S {
96            s_with_same_rows
97                .entry(self.get_row(word_in_s))
98                .or_default()
99                .push(word_in_s.clone());
100        }
101
102        // for each group of equivalent rows, check combinations for inconsistencies
103        for (_k, eq_word_in_s) in s_with_same_rows.iter() {
104            if eq_word_in_s.len() > 1 {
105                for pair_eq_words_in_S in eq_word_in_s
106                    .iter()
107                    .cloned()
108                    .tuple_combinations::<(Word, Word)>()
109                {
110                    if let Some((suffix, distinguishing)) =
111                        self.is_prefixes_equivalent(pair_eq_words_in_S.clone())
112                    {
113                        return Some(((pair_eq_words_in_S, suffix), distinguishing));
114                    }
115                }
116            }
117        }
118
119        None
120    }
121
122    /// Check whether two prefixes in S are still equivalent after appending any
123    /// input letter. If not, return the input-letter (as a Word) and the word in
124    /// D that exhibits the difference.
125    fn is_prefixes_equivalent(&self, eq_words_in_s: (Word, Word)) -> Option<(Word, Word)> {
126        for input_letter in &self.input_letters {
127            let suffix = Word::from_letters(vec![input_letter.clone()]);
128            let initial_suffixed = eq_words_in_s.0.concatenate(&suffix);
129            let eq_suffixed = eq_words_in_s.1.concatenate(&suffix);
130
131            // compare over all D columns
132            for word_in_d in &self.D {
133                let cell_map = self.ot_content.get(word_in_d);
134                let v1 = cell_map
135                    .and_then(|m| m.get(&initial_suffixed))
136                    .and_then(|o| o.as_ref());
137                let v2 = cell_map
138                    .and_then(|m| m.get(&eq_suffixed))
139                    .and_then(|o| o.as_ref());
140
141                if v1 != v2 {
142                    // return the suffix (single input letter) and the distinguishing word in D
143                    return Some((suffix, word_in_d.clone()));
144                }
145            }
146        }
147
148        None
149    }
150
151    /// Add a counterexample to the observation table
152    pub fn add_counterexample(
153        &mut self,
154        input_word: &Word,
155        _output_word: &Word,
156    ) -> Result<(), String> {
157        // Add all prefixes of the counterexample to S
158        for len_prefix in 1..=input_word.len() {
159            let prefix_input = input_word.prefix(len_prefix);
160            if !self.S.contains(&prefix_input) {
161                if self.SA.contains(&prefix_input) {
162                    self.remove_row(prefix_input.clone())?;
163                }
164                self.add_word_in_S(prefix_input)?;
165            }
166        }
167        Ok(())
168    }
169
170    fn remove_row(&mut self, word: Word) -> Result<(), String> {
171        // remove from S
172        self.S.retain(|w| w != &word);
173        // remove from SA
174        self.SA.retain(|w| w != &word);
175        // remove from observation table content
176        for (_key, row_map) in self.ot_content.iter_mut() {
177            row_map.remove(&word);
178        }
179        Ok(())
180    }
181
182    /// Make the observation table consistent
183    pub fn make_consistent(
184        &mut self,
185        inconsistency: Inconsistency,
186    ) -> Result<(), String> {
187        let ((_pair, suffix), inconsistent_suffix) = inconsistency;
188
189        // new column = suffix + inconsistent_suffix
190        let new_col_word = suffix.concatenate(&inconsistent_suffix);
191        if !self.D.contains(&new_col_word) {
192            self.add_word_in_D(new_col_word)?;
193        }
194
195        Ok(())
196    }
197
198    /// Check if the observation table is closed
199    pub fn is_closed(&self) -> bool {
200        let rows_in_s: HashSet<RowSignature> = self.S.iter().map(|s| self.get_row(s)).collect();
201
202        for sa in &self.SA {
203            if !rows_in_s.contains(&self.get_row(sa)) {
204                return false;
205            }
206        }
207        true
208    }
209
210    /// Close the observation table by moving rows from SA to S
211    pub fn close_table(&mut self) -> Result<(), String> {
212        let mut to_move = Vec::new();
213        let rows_in_s: HashSet<RowSignature> = self.S.iter().map(|s| self.get_row(s)).collect();
214
215        for sa in &self.SA {
216            if !rows_in_s.contains(&self.get_row(sa)) {
217                to_move.push(sa.clone());
218            }
219        }
220
221        for word in to_move {
222            self.SA.retain(|w| w != &word);
223            self.add_word_in_S(word)?;
224        }
225
226        Ok(())
227    }
228
229    /// Get a row in the observation table
230    fn get_row(&self, row_name: &Word) -> Vec<Option<Letter>> {
231        let mut row = Vec::with_capacity(self.D.len());
232        for word_in_D in &self.D {
233            let value = self
234                .ot_content
235                .get(word_in_D)
236                .and_then(|cell_content| cell_content.get(row_name))
237                .cloned()
238                .unwrap_or(None);
239            row.push(value);
240        }
241        row
242    }
243
244    fn add_word_in_D(&mut self, word: Word) -> Result<(), String> {
245        if self.D.contains(&word) {
246            return Err(format!("Word {:?} already in D", word));
247        }
248
249        if self.ot_content.contains_key(&word) {
250            return Err(format!(
251                "Word {:?} already in content of the observation table",
252                word
253            ));
254        }
255
256        self.D.push(word.clone());
257
258        // Execute queries for all S and SA with this new distinguishing word
259        let words_to_query: Vec<Word> = self.S.iter().chain(self.SA.iter()).cloned().collect();
260        let mut cell_content = HashMap::with_capacity(words_to_query.len());
261        
262        for word_in_S_or_SA in &words_to_query {
263            let query_input = word_in_S_or_SA.concatenate(&word);
264            let mut query = OutputQuery::new(query_input);
265            self.execute_query(&mut query)?;
266
267            if let Some(output_word) = query.output_word() {
268                if let Some(output_letter) = output_word.last_letter() {
269                    cell_content.insert(word_in_S_or_SA.clone(), Some(output_letter.clone()));
270                }
271            }
272        }
273        
274        self.ot_content.insert(word, cell_content);
275        Ok(())
276    }
277
278    fn add_word_in_S(&mut self, word: Word) -> Result<(), String> {
279        if self.S.contains(&word) {
280            return Err(format!("Word {:?} already in S", word));
281        }
282        if self.SA.contains(&word) {
283            return Err(format!("Word {:?} already in SA", word));
284        }
285
286        self.S.push(word.clone());
287
288        // Execute queries for all distinguishing words
289        let d_words: Vec<Word> = self.D.iter().cloned().collect();
290        for word_in_D in &d_words {
291            let query_input = word.concatenate(word_in_D);
292            let mut query = OutputQuery::new(query_input);
293            self.execute_query(&mut query)?;
294
295            if let Some(output_word) = query.output_word() {
296                if let Some(output_letter) = output_word.last_letter() {
297                    self.ot_content
298                        .entry(word_in_D.clone())
299                        .or_default()
300                        .insert(word.clone(), Some(output_letter.clone()));
301                }
302            }
303        }
304
305        // Add sa := word.input_letter for each input letter
306        let input_letters: Vec<Letter> = self.input_letters.iter().cloned().collect();
307        for input_letter in &input_letters {
308            let new_sa = word.concatenate(&Word::from_letters(vec![input_letter.clone()]));
309            if !self.S.contains(&new_sa) {
310                self.add_word_in_SA(new_sa)?;
311            }
312        }
313
314        Ok(())
315    }
316
317    fn add_word_in_SA(&mut self, word: Word) -> Result<(), String> {
318        if self.SA.contains(&word) {
319            return Err(format!("Word {:?} already in SA", word));
320        }
321        if self.S.contains(&word) {
322            return Err(format!("Word {:?} already in S", word));
323        }
324
325        self.SA.push(word.clone());
326
327        // Execute queries for all distinguishing words
328        let d_words: Vec<Word> = self.D.iter().cloned().collect();
329        for word_in_D in &d_words {
330            let query_input = word.concatenate(word_in_D);
331            let mut query = OutputQuery::new(query_input);
332            self.execute_query(&mut query)?;
333
334            if let Some(output_word) = query.output_word() {
335                if let Some(output_letter) = output_word.last_letter() {
336                    self.ot_content
337                        .entry(word_in_D.clone())
338                        .or_default()
339                        .insert(word.clone(), Some(output_letter.clone()));
340                }
341            }
342        }
343
344        Ok(())
345    }
346
347    fn execute_query(&mut self, query: &mut OutputQuery) -> Result<(), String> {
348        self.knowledge_base.lock().unwrap().resolve_query(query)
349    }
350    /// Check if the observation table is consistent
351    pub fn is_consistent(&self) -> bool {
352        self.find_inconsistency().is_none()
353    }
354
355    /// Build a hypothesis automaton from the current observation table
356    pub fn build_hypothesis(&self) -> Result<Automata, String> {
357        if !self.is_closed() {
358            return Err("Observation table must be closed".into());
359        }
360        if !self.is_consistent() {
361            return Err("Observation table must be consistent".into());
362        }
363
364        let mut transitions = Vec::new();
365        let mut words_and_states: Vec<(Word, State)> = Vec::new();
366        let mut long_state_name_to_states: HashMap<RowSignature, usize> = HashMap::new();
367
368        // Find unique rows in S
369        let mut unique_rows: IndexMap<RowSignature, Vec<Word>> = IndexMap::new();
370        for word in &self.S {
371            unique_rows
372                .entry(self.get_row(word))
373                .or_default()
374                .push(word.clone());
375        }
376
377        // Create states
378        let mut initial_state = None;
379        for (idx, (row_key, words_in_s)) in unique_rows.iter().enumerate() {
380            let state_name = idx.to_string();
381            let state = State::new(state_name.clone());
382
383            // Check if this is the initial state (contains empty word)
384            if words_in_s.iter().any(|w| w.is_empty()) {
385                if initial_state.is_some() {
386                    return Err("Multiple initial states found".into());
387                }
388                initial_state = Some(idx);
389            }
390
391            words_and_states.push((words_in_s[0].clone(), state.clone()));
392            long_state_name_to_states.insert(row_key.clone(), idx);
393        }
394
395        let initial_state_idx = initial_state.ok_or("No initial state found")?;
396
397        // Create transitions
398        let input_words: Vec<(Letter, Word)> = self
399            .input_letters
400            .iter()
401            .cloned()
402            .map(|letter| {
403                let one_letter_word = Word::from_letters(vec![letter.clone()]);
404                (letter, one_letter_word)
405            })
406            .collect();
407        for i in 0..words_and_states.len() {
408            let word = words_and_states[i].0.clone();
409            let source_state_name = words_and_states[i].1.name.clone();
410
411            for (input_letter, input_letter_word) in &input_words {
412                let new_word = word.concatenate(input_letter_word);
413                let output_row = self.get_row(&new_word);
414                let output_state_idx = long_state_name_to_states
415                    .get(&output_row)
416                    .copied()
417                    .ok_or_else(|| format!("Cannot find a state matching row: {:?}", output_row))?;
418
419                let output_letter = self
420                    .ot_content
421                    .get(input_letter_word)
422                    .and_then(|cell| cell.get(&word))
423                    .and_then(|opt| opt.as_ref())
424                    .cloned()
425                    .ok_or_else(|| {
426                        format!(
427                            "Missing output letter for input '{}' from word '{}'",
428                            input_letter, word
429                        )
430                    })?;
431
432                let transition_name = format!("t{}", transitions.len());
433                let transition = Transition::new_with_source(
434                    transition_name,
435                    source_state_name.clone(),
436                    words_and_states[output_state_idx].1.clone(),
437                    input_letter.clone(),
438                    output_letter,
439                );
440                words_and_states[i].1.add_transition(transition.clone());
441                transitions.push(transition);
442            }
443        }
444
445        let mut automata = Automata::new(
446            words_and_states[initial_state_idx].1.clone(),
447            "Automata".to_string(),
448        );
449        automata.transitions = transitions;
450
451        Ok(automata)
452    }
453}
454
455impl fmt::Display for ObservationTable {
456    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
457        let mut matrix: Vec<Vec<String>> = Vec::new();
458
459        // Header row
460        let mut header_row = vec![String::new()];
461        header_row.extend(self.D.iter().map(|d| d.to_string()));
462        matrix.push(header_row.clone());
463
464        // S rows
465        for row_name in &self.S {
466            let mut row = vec![row_name.to_string()];
467            row.extend(
468                self.get_row(row_name)
469                    .iter()
470                    .map(|w| w.as_ref().map(|l| l.to_string()).unwrap_or_default()),
471            );
472            matrix.push(row);
473        }
474
475        // Separator
476        matrix.push(vec!["~~~".to_string(); header_row.len()]);
477
478        // SA rows
479        for row_name in &self.SA {
480            let mut row = vec![row_name.to_string()];
481            row.extend(
482                self.get_row(row_name)
483                    .iter()
484                    .map(|w| w.as_ref().map(|l| l.to_string()).unwrap_or_default()),
485            );
486            matrix.push(row);
487        }
488
489        // Calculate column widths
490        let col_count = matrix[0].len();
491        let mut col_widths = vec![0; col_count];
492        for row in &matrix {
493            for (i, cell) in row.iter().enumerate() {
494                col_widths[i] = col_widths[i].max(cell.len());
495            }
496        }
497
498        // Format and write
499        for row in &matrix {
500            for (i, cell) in row.iter().enumerate() {
501                if i > 0 {
502                    write!(f, " | ")?;
503                }
504                write!(f, "{:width$}", cell, width = col_widths[i])?;
505            }
506            writeln!(f)?;
507        }
508
509        Ok(())
510    }
511}
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516    use crate::knowledge_base::KnowledgeBase;
517    use std::sync::{Arc, Mutex};
518
519    #[test]
520    fn test_observation_table_creation() {
521        let kb: Arc<Mutex<dyn crate::knowledge_base::KnowledgeBaseTrait>> =
522            Arc::new(Mutex::new(KnowledgeBase::new()));
523        let letters = vec![Letter::new("a"), Letter::new("b")];
524        let ot = ObservationTable::new(letters, kb);
525        assert!(!ot.initialized);
526    }
527}