Skip to content

Commit 8b64acc

Browse files
authored
Add search for counterexample in the global trace
1 parent efd7f44 commit 8b64acc

1 file changed

Lines changed: 63 additions & 7 deletions

File tree

  • aalpy/learning_algs/resetless

aalpy/learning_algs/resetless/hW.py

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from collections import defaultdict, deque
33
from random import choice
44

5-
from aalpy.automata import MealyState, MealyMachine, MooreState, MooreMachine, DfaState, Dfa
5+
from aalpy.automata import MealyState, MealyMachine, MooreState, MooreMachine
66
from 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

Comments
 (0)