22from collections import defaultdict , deque
33from random import choice
44
5- from aalpy .automata import MealyState , MealyMachine , MooreState , MooreMachine , DfaState , Dfa
5+ from aalpy .automata import MealyState , MealyMachine , MooreState , MooreMachine
66from aalpy .utils .HelperFunctions import all_suffixes , print_learning_info
77
88
@@ -147,12 +147,26 @@ def execute_sequence(self, seq_under_test: tuple):
147147 return (self ._unwrap_output (self .sul .step (None )),)
148148 return tuple (self .step_wrapper (i ) for i in seq_under_test )
149149
150+ def _add_shortest_new_suffix_to_W (self , sequence ):
151+ """
152+ Extend W with the shortest suffix of sequence not yet in W (the
153+ counterexample processing rule). Returns True if W changed.
154+ """
155+ for s in sorted ((tuple (s ) for s in all_suffixes (sequence )), key = len ):
156+ if self .add_to_W (s ):
157+ return True
158+ return False
159+
150160 def execute_conjecture_path (self , start_state , path ):
151161 """
152162 Walk path on the SUL while verifying outputs against the conjecture.
153163 On a mismatch, extend W with the failing prefix (or drop the stale transition
154164 data if the prefix is already in W) and return None.
155165 Returns (reached state, observed outputs) on success.
166+
167+ Adding the full prefix (instead of a suffix as for counterexamples) is
168+ deliberate: it benchmarks measurably better here, as the prefix starts at
169+ the very state pair that the conjecture confused.
156170 """
157171 state = start_state
158172 observed_outputs = []
@@ -171,6 +185,7 @@ def execute_conjecture_path(self, start_state, path):
171185 if key [0 ] == i :
172186 del state .transition_w_values [key ]
173187 return None
188+
174189 state = state .transitions [i ]
175190 return state , tuple (observed_outputs )
176191
@@ -308,6 +323,47 @@ def find_counterexample(self, hypothesis):
308323
309324 return None
310325
326+ def _trace_step_explained_by (self , state , letter , output ):
327+ """Target state if the hypothesis state reproduces this trace step, else None."""
328+ target = state .transitions .get (letter )
329+ if target is None :
330+ return None
331+ predicted = target .output if self .is_moore else state .output_fun .get (letter )
332+ return target if predicted == output else None
333+
334+ def find_counterexample_in_trace (self , hypothesis ):
335+ """
336+ Search the already-observed global trace for a sub-trace that no hypothesis
337+ state can explain (Sec. 6.1 of the hW paper); used as a free backstop when
338+ the random walk finds no counterexample. Returns the inputs of such a
339+ sub-trace, or None.
340+
341+ A single forward pass suffices: if some state explains trace[0..j], its
342+ intermediate states explain every sub-trace of it, so the alive set only
343+ empties if an unexplained sub-trace exists.
344+ """
345+ alive = set (hypothesis .states )
346+ for j , (letter , output ) in enumerate (self .global_trace ):
347+ alive = {t for q in alive
348+ if (t := self ._trace_step_explained_by (q , letter , output )) is not None }
349+ if not alive :
350+ return self ._shorten_trace_counterexample (hypothesis .states , j )
351+ return None
352+
353+ def _shorten_trace_counterexample (self , states , end ):
354+ """Inputs of trace [i..end] for the latest start i that no state can explain."""
355+ trace = self .global_trace
356+ explaining = set (states ) # states explaining trace[i+1..end]
357+ start = 0
358+ for i in range (end , - 1 , - 1 ):
359+ letter , output = trace [i ]
360+ explaining = {q for q in states
361+ if self ._trace_step_explained_by (q , letter , output ) in explaining }
362+ if not explaining :
363+ start = i
364+ break
365+ return [letter for letter , _ in trace [start :end + 1 ]]
366+
311367 def is_complete (self ):
312368 """True if every identified state has all transitions leading to identified states."""
313369 if not self .state_map :
@@ -798,19 +854,19 @@ def main_loop(self, print_level=2):
798854
799855 eq_start = time .time ()
800856 counter_example = self .find_counterexample (hypothesis )
857+ if counter_example is None :
858+ # backstop at zero SUL cost: the observed trace may refute a
859+ # hypothesis that the random walk failed to disprove
860+ counter_example = self .find_counterexample_in_trace (hypothesis )
801861 eq_query_time += time .time () - eq_start
802862
803863 if counter_example is None :
804864 break
805865
806866 # extend W with the shortest new suffix of the counterexample
807- added_suffix = None
808- for s in sorted ((tuple (s ) for s in all_suffixes (counter_example )), key = len ):
809- if self .add_to_W (s ):
810- added_suffix = s
811- break
867+ added_suffix = self ._add_shortest_new_suffix_to_W (counter_example )
812868
813- if added_suffix is None and not self .add_to_W (self .homing_sequence + tuple (counter_example )):
869+ if not added_suffix and not self .add_to_W (self .homing_sequence + tuple (counter_example )):
814870 # the counterexample distinguishes states that (h, W) could not
815871 # separate, so the recorded identification data must be wrong:
816872 # refine h, which also invalidates all cached query answers
0 commit comments