diff --git a/algorithms/active/lstar/pom.xml b/algorithms/active/lstar/pom.xml index c785db71ea..a361afdb0b 100644 --- a/algorithms/active/lstar/pom.xml +++ b/algorithms/active/lstar/pom.xml @@ -124,6 +124,26 @@ limitations under the License. org.testng testng + + net.automatalib + automata-serialization-dot + test + + + de.learnlib + learnlib-cache + test + + + de.learnlib + learnlib-symbol-filters + test + + + de.learnlib + learnlib-statistics + test + @@ -134,7 +154,9 @@ limitations under the License. maven-surefire-plugin - @{argLine} --add-reads=de.learnlib.algorithm.lstar=net.automatalib.util + + @{argLine} --add-reads=de.learnlib.algorithm.lstar=net.automatalib.util --add-reads=de.learnlib.algorithm.lstar=de.learnlib.filter.statistic + diff --git a/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/LStarLocalTimerMealy.java b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/LStarLocalTimerMealy.java new file mode 100644 index 0000000000..2622abf45c --- /dev/null +++ b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/LStarLocalTimerMealy.java @@ -0,0 +1,431 @@ +package de.learnlib.algorithm.lstar.mmlt; + + +import de.learnlib.acex.AcexAnalyzer; +import de.learnlib.acex.AcexAnalyzers; +import de.learnlib.algorithm.LocalTimerMealyModelParams; +import de.learnlib.algorithm.lstar.closing.ClosingStrategies; +import de.learnlib.algorithm.lstar.closing.ClosingStrategy; +import de.learnlib.algorithm.lstar.mmlt.cex.CexPreprocessor; +import de.learnlib.algorithm.lstar.mmlt.cex.LocalTimerMealyCounterexampleHandler; +import de.learnlib.algorithm.lstar.mmlt.cex.LocalTimerMealyOutputInconsistency; +import de.learnlib.algorithm.lstar.mmlt.cex.results.FalseIgnoreResult; +import de.learnlib.algorithm.lstar.mmlt.cex.results.MissingDiscriminatorResult; +import de.learnlib.algorithm.lstar.mmlt.cex.results.MissingOneShotResult; +import de.learnlib.algorithm.lstar.mmlt.cex.results.MissingResetResult; +import de.learnlib.algorithm.lstar.mmlt.hyp.LocalTimerMealyHypothesis; +import de.learnlib.datastructure.observationtable.OTLearner; +import de.learnlib.datastructure.observationtable.ObservationTable; +import de.learnlib.datastructure.observationtable.Row; +import de.learnlib.oracle.AbstractTimedQueryOracle; +import de.learnlib.query.DefaultQuery; +import de.learnlib.statistic.container.DummyStatsContainer; +import de.learnlib.statistic.container.LearnerStatsProvider; +import de.learnlib.statistic.container.StatsContainer; +import de.learnlib.symbol_filter.SymbolFilter; +import de.learnlib.symbol_filter.SymbolFilterResponse; +import net.automatalib.alphabet.Alphabet; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealyOutputSymbol; +import net.automatalib.alphabet.time.mmlt.NonDelayingInput; +import net.automatalib.alphabet.time.mmlt.TimeStepSequence; +import net.automatalib.automaton.time.mmlt.LocalTimerMealy; +import net.automatalib.automaton.time.mmlt.MealyTimerInfo; +import net.automatalib.word.Word; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +/** + * The MMLT learner. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class LStarLocalTimerMealy implements OTLearner, LocalTimerMealySemanticInputSymbol, Word>>, LearnerStatsProvider { + + private static final Logger logger = LoggerFactory.getLogger(LStarLocalTimerMealy.class); + private StatsContainer stats = new DummyStatsContainer(); + + private final ClosingStrategy, ? super Word>> closingStrategy; + + private final AbstractTimedQueryOracle timeOracle; + private final SymbolFilter, NonDelayingInput> symbolFilter; + + private final LStarLocalTimerMealyHypDataContainer hypData; + + // ============================ + + + private final List>> initialSuffixes; + private final LocalTimerMealyCounterexampleHandler cexAnalyzer; + + /** + * Instantiates a new Rivest-Schapire learner for MMLTs. + *

+ * Uses the close-shortest strategy for closing the observation table and + * binary-backwards search for decomposing counterexamples. + * + * @param alphabet Input alphabet for the semantic automaton + * @param modelParams LocalTimerMealyModel parameters + * @param initialSuffixes Initial set of suffixes. May be empty. + * @param timeOracle The output query oracle for MMLTs. + * @param symbolFilter The symbol filter. If no filter should be used, use the AcceptAll filter. + */ + public LStarLocalTimerMealy(Alphabet> alphabet, + LocalTimerMealyModelParams modelParams, + @NonNull + List>> initialSuffixes, + AbstractTimedQueryOracle timeOracle, + @NonNull SymbolFilter, NonDelayingInput> symbolFilter) { + this(alphabet, modelParams, initialSuffixes, ClosingStrategies.CLOSE_SHORTEST, timeOracle, symbolFilter, AcexAnalyzers.BINARY_SEARCH_BWD); + } + + /** + * Instantiates a new Rivest-Schapire learner for MMLTs. + * + * @param alphabet Input alphabet for the semantic automaton + * @param modelParams LocalTimerMealyModel parameters + * @param initialSuffixes Initial set of suffixes. May be empty. + * @param closingStrategy Closing strategy for the observation table. + * @param timeOracle The output query oracle for MMLTs. + * @param symbolFilter The symbol filter. If no filter should be used, use the AcceptAll filter. + * @param analyzer The strategy for decomposing counterexamples. + */ + public LStarLocalTimerMealy(Alphabet> alphabet, + LocalTimerMealyModelParams modelParams, + @NonNull + List>> initialSuffixes, + ClosingStrategy, ? super Word>> closingStrategy, + AbstractTimedQueryOracle timeOracle, + @NonNull SymbolFilter, NonDelayingInput> symbolFilter, + AcexAnalyzer analyzer) { + this.closingStrategy = closingStrategy; + this.timeOracle = timeOracle; + this.initialSuffixes = initialSuffixes; + + // Prepare hyp data: + + // Init hypothesis data: + this.hypData = new LStarLocalTimerMealyHypDataContainer<>(alphabet, modelParams, + new LocalTimerMealyObservationTable<>(alphabet, modelParams.maxTimerQueryWaitingTime(), symbolFilter, modelParams.silentOutput())); + + this.cexAnalyzer = new LocalTimerMealyCounterexampleHandler<>(timeOracle, analyzer, symbolFilter); + this.symbolFilter = symbolFilter; + } + + /** + * Heuristically chooses a new one-shot timer from the provided timers: + * takes the timer with the highest initial value that a) does not exceed maxInitialValue and b) has not timer + * with a lower initial value that times out at the same time. + * + * @param sortedTimers Timers, sorted ascendingly by their initial value + * @param maxInitialValue Max. initial value to consider + * @param Output type + * @return New one-shot timer + */ + public static MealyTimerInfo selectOneShotTimer(List> sortedTimers, long maxInitialValue) { + + // Filter relevant timers: + // Start at timer with the highest initial value. + // Ignore all timers whose initial value exceeds the maximum value. + // Also ignore timers whose timeout is the multiple of another timer's initial value. + List> relevantTimers = new ArrayList<>(); + for (int i = sortedTimers.size() - 1; i >= 0; i--) { + MealyTimerInfo timer = sortedTimers.get(i); + + if (timer.initial() > maxInitialValue) { + continue; // could not have expired + } + + // Ignore timers whose initial value is a multiple of another one. + // When set to one-shot, these would expire at same time as periodic timer -> non-deterministic behavior! + boolean multiple = false; + for (int j = 0; j < i; j++) { + MealyTimerInfo otherTimer = sortedTimers.get(j); + if (timer.initial() % otherTimer.initial() == 0) { + multiple = true; + break; + } + } + if (multiple) { + continue; + } + + relevantTimers.add(timer); // not a multiple and within time + } + + if (relevantTimers.isEmpty()) { + throw new IllegalStateException("Max. initial value is too low; must include at least one timer."); + } + + // Return the candidate with the highest initial value one-shot: + return relevantTimers.get(0); // order is reversed -> first is last + } + + + /** + * Constructs an MMLT hypothesis. + * This updates all transition outputs, if required. + * + * @return MMLT hypothesis + */ + public LocalTimerMealy getHypothesisModel() { + return getInternalLocalTimerMealyHypothesis(); + } + + /** + * Like the construction above, but returns an LocalTimerMealyHypothesis object instead. + * This objects provides additional functions that are just intended for the learner but not the teacher. + * + * @return MMLT hypothesis + */ + private LocalTimerMealyHypothesis getInternalLocalTimerMealyHypothesis() { + this.updateOutputs(); + var hyp = LocalTimerMealyHypothesisBuilder.constructHypothesis(this.hypData); + + return new LocalTimerMealyHypothesis<>(hyp.automaton(), hyp.prefixMap()); + } + + protected List>> selectClosingRows(List>>> unclosed) { + return closingStrategy.selectClosingRows(unclosed, hypData.getTable(), timeOracle); + } + + + protected void updateOutputs() { + // Query output of newly-added transitions: + Stream.concat(this.hypData.getTable().getShortPrefixRows().stream(), this.hypData.getTable().getLongPrefixRows().stream()) + .forEach(row -> { + if (row.getLabel().isEmpty()) { + return; // initial state + } + + if (this.hypData.getTransitionOutputMap().containsKey(row.getLabel())) { + return; // already queried + } + + Word> prefix = row.getLabel().prefix(-1); + LocalTimerMealySemanticInputSymbol inputSym = row.getLabel().suffix(1).lastSymbol(); + + LocalTimerMealyOutputSymbol output = null; + if (inputSym instanceof TimeStepSequence ws) { + // Query timer output from table: + MealyTimerInfo timerInfo = this.hypData.getTable().getTimerInfo(prefix, ws.getTimeSteps()); + if (timerInfo == null) { + throw new AssertionError(); + } + output = new LocalTimerMealyOutputSymbol<>(timerInfo.output()); + } else { + output = this.timeOracle.querySuffixOutput(prefix, Word.fromLetter(inputSym)).lastSymbol(); + } + + if (output != null) { + this.hypData.getTransitionOutputMap().put(row.getLabel(), output); + } + }); + } + + // ========================== + + @Override + public void startLearning() { + List>>> initialUnclosed = this.hypData.getTable().initialize(Collections.emptyList(), this.initialSuffixes, timeOracle); + + // Ensure that closed: + this.completeConsistentTable(initialUnclosed); + } + + @Override + public boolean refineHypothesis(DefaultQuery, Word>> ceQuery) { + if (!refineHypothesisSingle(ceQuery)) { + return false; // no valid CEX + } + while (refineHypothesisSingle(ceQuery)) { + } + return true; + } + + + /** + * Transforms the provided counterexample to an inconsistency object: + * First, checks if still a counterexample. If so, cuts the cex after the first output deviation. + * + * @param ceQuery Counterexample + * @param hypothesis Current hypothesis + * @return The resulting inconsistency, or null, if the counterexample is not a counterexample. + */ + @Nullable + private LocalTimerMealyOutputInconsistency toOutputInconsistency(DefaultQuery, Word>> ceQuery, LocalTimerMealyHypothesis hypothesis) { + // 1. Cut example after first deviation: + Word> hypOutput = hypothesis.getSemantics().computeSuffixOutput(ceQuery.getPrefix(), ceQuery.getSuffix()); + DefaultQuery, Word>> shortQuery = CexPreprocessor.truncateCEX(ceQuery, hypOutput); + if (shortQuery == null) { + return null; + } + + // 2. Calculate shortened hypothesis output: + var shortHypOutput = hypothesis.getSemantics().computeSuffixOutput(shortQuery.getPrefix(), shortQuery.getSuffix()); + if (shortHypOutput.equals(shortQuery.getOutput())) { + throw new AssertionError("Deviation lost after shortening."); + } + + return new LocalTimerMealyOutputInconsistency<>(shortQuery.getPrefix(), + shortQuery.getSuffix(), + shortQuery.getOutput(), shortHypOutput); + } + + private boolean refineHypothesisSingle(DefaultQuery, Word>> ceQuery) { + // 1. Update hypothesis (may have changed since last refinement): + var hypothesis = this.getInternalLocalTimerMealyHypothesis(); + + // 2. Transform to output inconsistency: + var outputIncons = this.toOutputInconsistency(ceQuery, hypothesis); + if (outputIncons == null) { + return false; + } + + logger.debug(String.format("Refining with inconsistency %s", outputIncons)); + + // 3. Identify source of deviation: + stats.startOrResumeClock("clk_cex_analysis", "Total cex analysis time"); + stats.increaseCounter("cnt_cex_analysis", "Cex analyses"); + var analysisResult = this.cexAnalyzer.analyzeInconsistency(outputIncons, hypothesis); + stats.pauseClock("clk_cex_analysis"); + + // 4. Refine: + if (analysisResult instanceof MissingDiscriminatorResult locSplit) { + stats.increaseCounter("INACC_MISSING_DISC", + "Inaccuracies: missing discriminators"); + + // Add new discriminator as suffix: + if (hypData.getTable().getSuffixes().contains(locSplit.getDiscriminator())) throw new AssertionError(); + List>> suffixes = Collections.singletonList(locSplit.getDiscriminator()); + var unclosed = hypData.getTable().addSuffixes(suffixes, timeOracle); + + // Close transitions: + this.completeConsistentTable(unclosed); // no consistency check for RS + } else if (analysisResult instanceof MissingResetResult noReset) { + stats.increaseCounter("INACC_MISSING_RESETS", + "Inaccuracies: missing resets"); + + // Add missing reset: + var resetTrans = hypothesis.getPrefix(noReset.getLocation()).append(noReset.getInput()); + this.hypData.getTransitionResetSet().add(resetTrans); + } else if (analysisResult instanceof MissingOneShotResult noAperiodic) { + stats.increaseCounter("INACC_MISSING_OS", + "Inaccuracies: missing one-shot timers"); + + // Identify corresponding sp row: + Word> locPrefix = hypothesis.getPrefix(noAperiodic.getLocation()); + Row> spRow = hypData.getTable().getRow(locPrefix); + if (spRow == null || !spRow.isShortPrefixRow()) { + throw new AssertionError(); + } + + this.handleMissingTimeoutChange(spRow, noAperiodic.getTimeout()); + } else if (analysisResult instanceof FalseIgnoreResult falseIgnore) { + stats.increaseCounter("INACC_MISSING_FI", + "Inaccuracies: false ignores"); + + if (this.symbolFilter == null) { + throw new AssertionError("Cannot detect false ignores without symbol filter."); + } + + // Identify corresponding sp row: + Word> locPrefix = hypothesis.getPrefix(falseIgnore.getLocation()); + Row> spRow = hypData.getTable().getRow(locPrefix); + if (spRow == null || !spRow.isShortPrefixRow()) { + throw new AssertionError(); + } + + // Update filter: + this.symbolFilter.update(locPrefix, falseIgnore.getSymbol(), SymbolFilterResponse.ACCEPT); + + // Legalize symbol + close table: + var unclosed = hypData.getTable().addOutgoingTransition(spRow, falseIgnore.getSymbol(), this.timeOracle); + stats.increaseCounter("Count_legalized", "Legalized symbols"); + + this.completeConsistentTable(unclosed); + } else { + throw new IllegalStateException("Unknown inconsistency type."); + } + + return true; + } + + private void handleMissingTimeoutChange(Row> spRow, MealyTimerInfo timeout) { + var locationTimerInfo = hypData.getTable().getLocationTimerInfo(spRow); + if (locationTimerInfo == null) { + throw new AssertionError("Location with missing one-shot timer must have timers."); + } + + // Only timer with highest initial value can be one-shot. + // If location already has a one-shot timer, prefix of its timeout-transition might be core or fringe prefix. + // If it is a fringe prefix, we need to remove it: + var lastTimerTransPrefix = spRow.getLabel().append(new TimeStepSequence<>(locationTimerInfo.getLastTimer().initial())); + if (!locationTimerInfo.getLastTimer().periodic()) { + if (!hypData.getTable().getRow(lastTimerTransPrefix).isShortPrefixRow()) { + // Last timer is one-shot + has fringe prefix: + this.hypData.getTable().removeLpRow(lastTimerTransPrefix); + } + } + + // Prefix for timeout-transition of new one-shot timer: + Word> timerTransPrefix = spRow.getLabel().append(new TimeStepSequence<>(timeout.initial())); + if (this.hypData.getTable().getRow(timerTransPrefix) != null) { + throw new AssertionError("Timer already appears to be one-shot."); + } + + // Remove all timers with greater timeout (are now redundant): + var subsequentTimers = locationTimerInfo.getSortedTimers().stream() + .filter(t -> t.initial() > timeout.initial()) + .map(MealyTimerInfo::name).toList(); + subsequentTimers.forEach(locationTimerInfo::removeTimer); + + // Change from periodic to one-shot: + locationTimerInfo.setOneShotTimer(timeout.name()); + + // Update fringe prefixes + close table: + List>>> unclosed = this.hypData.getTable().addTimerTransition(spRow, timeout, this.timeOracle); + this.completeConsistentTable(unclosed); + } + + + @Override + public ObservationTable, Word>> getObservationTable() { + return this.hypData.getTable(); + } + + /** + * Iteratively checks for unclosedness and inconsistencies in the table, and fixes any occurrences thereof. This + * process is repeated until the observation table is both closed and consistent. + *

+ * Simplified version for RS learner: assumes that OT is always consistent. + * + * @param unclosed the unclosed rows (equivalence classes) to start with. + */ + protected void completeConsistentTable(List>>> unclosed) { + List>>> unclosedIter = unclosed; + while (!unclosedIter.isEmpty()) { + List>> closingRows = this.selectClosingRows(unclosedIter); + + // Add new states: + unclosedIter = hypData.getTable().toShortPrefixes(closingRows, timeOracle); + } + + } + + @Override + public void setStatsContainer(StatsContainer container) { + this.stats = container; + this.cexAnalyzer.setStatsContainer(container); + } + + +} diff --git a/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/LStarLocalTimerMealyHypDataContainer.java b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/LStarLocalTimerMealyHypDataContainer.java new file mode 100644 index 0000000000..14954492ac --- /dev/null +++ b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/LStarLocalTimerMealyHypDataContainer.java @@ -0,0 +1,72 @@ +package de.learnlib.algorithm.lstar.mmlt; + +import de.learnlib.algorithm.LocalTimerMealyModelParams; +import de.learnlib.datastructure.observationtable.Row; +import net.automatalib.alphabet.Alphabet; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealyOutputSymbol; +import net.automatalib.word.Word; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Stores various data used for describing the MMLT hypothesis. + * This includes the OT, a list of local resets, and a list of outputs. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +class LStarLocalTimerMealyHypDataContainer { + private final Alphabet> alphabet; + + private final LocalTimerMealyObservationTable table; + private final Map>, LocalTimerMealyOutputSymbol> transitionOutputMap; + private final Set>> transitionResetSet; // all transitions that trigger a reset + + private final LocalTimerMealyModelParams modelParams; + + public LStarLocalTimerMealyHypDataContainer(Alphabet> alphabet, LocalTimerMealyModelParams modelParams, LocalTimerMealyObservationTable table) { + this.alphabet = alphabet; + this.modelParams = modelParams; + this.table = table; + + this.transitionOutputMap = new HashMap<>(); + this.transitionResetSet = new HashSet<>(); + } + + @Nullable + protected LocalTimerMealyOutputSymbol getTransitionOutput(Row> stateRow, int inputIdx) { + Row> transRow = stateRow.getSuccessor(inputIdx); + if (transRow == null) { + return null; + } + + return this.transitionOutputMap.getOrDefault(transRow.getLabel(), null); + } + + + public LocalTimerMealyModelParams getModelParams() { + return modelParams; + } + + public Alphabet> getAlphabet() { + return alphabet; + } + + + public LocalTimerMealyObservationTable getTable() { + return table; + } + + public Map>, LocalTimerMealyOutputSymbol> getTransitionOutputMap() { + return transitionOutputMap; + } + + public Set>> getTransitionResetSet() { + return transitionResetSet; + } +} diff --git a/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/LocalTimerMealyHypothesisBuilder.java b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/LocalTimerMealyHypothesisBuilder.java new file mode 100644 index 0000000000..d23788ebaa --- /dev/null +++ b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/LocalTimerMealyHypothesisBuilder.java @@ -0,0 +1,127 @@ +package de.learnlib.algorithm.lstar.mmlt; + +import de.learnlib.datastructure.observationtable.Row; +import net.automatalib.alphabet.impl.GrowingMapAlphabet; +import net.automatalib.alphabet.time.mmlt.*; +import net.automatalib.automaton.time.impl.mmlt.CompactLocalTimerMealy; +import net.automatalib.automaton.time.mmlt.LocalTimerMealy; +import net.automatalib.word.Word; + +import java.util.HashMap; +import java.util.Map; + +class LocalTimerMealyHypothesisBuilder { + + record LocalTimerMealyHypothesisBuildResult(LocalTimerMealy automaton, + Map>> prefixMap) { + + } + + /** + * Constructs a hypothesis MMLT from an observation table, inferred local resets, and inferred local timers. + */ + static LocalTimerMealyHypothesisBuildResult constructHypothesis(LStarLocalTimerMealyHypDataContainer hypData) { + + // 1. Create map that stores link between contentID and short-prefix row: + final Map>> locationContentIdMap = new HashMap<>(); // contentId -> sp location + for (var spRow : hypData.getTable().getShortPrefixRows()) { + if (locationContentIdMap.containsKey(spRow.getRowContentId())) { + // Multiple sp rows may have same contentID. Thus, assign each id one location: + continue; + } + locationContentIdMap.put(spRow.getRowContentId(), spRow); + } + + // 2. Create untimed alphabet: + GrowingMapAlphabet> alphabet = new GrowingMapAlphabet<>(); + for (var symbol : hypData.getAlphabet()) { + if (symbol instanceof NonDelayingInput ndi) { + alphabet.add(ndi); + } + } + + // 3. Prepare objects for automaton, timers and resets: + int numLocations = hypData.getTable().numberOfShortPrefixRows(); + var hypothesis = new CompactLocalTimerMealy<>(alphabet, hypData.getModelParams().silentOutput(), hypData.getModelParams().outputCombiner()); + + final Map stateMap = new HashMap<>(numLocations); // row content id -> state id + + final Map>> prefixMap = new HashMap<>(numLocations); // state id -> location prefix + + // 4. Create one state per location: + for (var row : hypData.getTable().getShortPrefixRows()) { + int newStateId = hypothesis.addState(); + stateMap.putIfAbsent(row.getRowContentId(), newStateId); + prefixMap.put(newStateId, row.getLabel()); + + if (row.getLabel().equals(Word.epsilon())) { + hypothesis.setInitialState(newStateId); + } + } + // Ensure initial location: + if (hypothesis.getInitialState() == null) { + throw new IllegalArgumentException("Automaton must have an initial location."); + } + + // 5. Create outgoing transitions for non-delaying inputs: + for (var rowContentId : stateMap.keySet()) { + Row> spLocation = locationContentIdMap.get(rowContentId); + + for (var symbol : alphabet) { + int symIdx = hypData.getAlphabet().getSymbolIndex(symbol); + + var transOutput = hypData.getTransitionOutput(spLocation, symIdx); + O output = hypData.getModelParams().silentOutput(); // silent by default + if (transOutput != null) { + output = transOutput.getSymbol(); + } + + int successorId; + if (spLocation.getSuccessor(symIdx) == null) { + successorId = spLocation.getRowContentId(); // not in local alphabet -> self-loop + } else { + successorId = spLocation.getSuccessor(symIdx).getRowContentId(); + } + + // Add transition to automaton: + int sourceLocId = stateMap.get(rowContentId); + int successorLocId = stateMap.get(successorId); + hypothesis.addTransition(sourceLocId, symbol, output, successorLocId); + + // Check for local reset: + var targetTransition = spLocation.getLabel().append(symbol); + if (hypData.getTransitionResetSet().contains(targetTransition) && sourceLocId == successorLocId) { + hypothesis.addLocalReset(sourceLocId, symbol); + } + } + + } + + // 6. Add timeout transitions: + for (var rowContentId : stateMap.keySet()) { + Row> spLocation = locationContentIdMap.get(rowContentId); + + var timerInfo = hypData.getTable().getLocationTimerInfo(spLocation); + if (timerInfo == null) { + continue; // no timers + } + + for (var timer : timerInfo.getLocalTimers().values()) { + if (timer.periodic()) { + hypothesis.addPeriodicTimer(stateMap.get(rowContentId), timer.name(), timer.initial(), timer.output()); + } else { + // One-shot: use successor from table + LocalTimerMealySemanticInputSymbol symbol = new TimeStepSequence<>(timer.initial()); + + int symIdx = hypData.getAlphabet().getSymbolIndex(symbol); + int successorId = spLocation.getSuccessor(symIdx).getRowContentId(); + + hypothesis.addOneShotTimer(stateMap.get(rowContentId), timer.name(), timer.initial(), timer.output(), stateMap.get(successorId)); + } + } + } + + return new LocalTimerMealyHypothesisBuildResult<>(hypothesis, prefixMap); + } + +} diff --git a/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/LocalTimerMealyObservationTable.java b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/LocalTimerMealyObservationTable.java new file mode 100644 index 0000000000..163961d834 --- /dev/null +++ b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/LocalTimerMealyObservationTable.java @@ -0,0 +1,583 @@ +package de.learnlib.algorithm.lstar.mmlt; + +import de.learnlib.datastructure.observationtable.MutableObservationTable; +import de.learnlib.datastructure.observationtable.Row; +import de.learnlib.datastructure.observationtable.RowImpl; +import de.learnlib.oracle.AbstractTimedQueryOracle; +import de.learnlib.oracle.MembershipOracle; +import de.learnlib.symbol_filter.SymbolFilter; +import de.learnlib.symbol_filter.SymbolFilterResponse; +import net.automatalib.alphabet.Alphabet; +import net.automatalib.alphabet.time.mmlt.*; +import net.automatalib.automaton.time.mmlt.MealyTimerInfo; +import net.automatalib.word.Word; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * The observation table used by the MMLT learner. + *

+ * Unlike an OT for standard Mealy learning, includes prefixes for the timeout transitions of one-shot timers. + * Intended to be used with a symbol filter. The filter is queried before adding a new transition for a non-delaying input. + * If the filter considers the transition to be a silent self-loop, the output of the transition is first verified. + * If it is actually silent the learner considers the transition to be a silent self-loop. Consequently, + * it does not add a transition for it. Transitions may be added later if an input was falsely ignored. + *

+ * Assumes that all short prefixes lead to different locations (-> no need to make canonical) + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class LocalTimerMealyObservationTable implements MutableObservationTable, Word>> { + + private static final Logger logger = LoggerFactory.getLogger(LocalTimerMealyObservationTable.class); + + private final SymbolFilter, NonDelayingInput> symbolFilter; + + private final Map>, LocationTimerInfo> timerInfoMap; // prefix -> timer info + + private final Map>, RowImpl>> shortPrefixRowMap; // label -> row info + private final Map>, RowImpl>> longPrefixRowMap; // label -> row info + + private final List>> sortedShortPrefixes; // values of shortPrefixRowMap sorted by label, for faster access. + private final List>> longPrefixList; // values of longPrefixRowMap as list, for faster access. + + private final Map> rowContentMap; // contentID -> row content + private static final int NO_CONTENT = -1; + + private final List>> suffixes = new ArrayList<>(); + private final Set>> suffixSet = new HashSet<>(); + + private final Alphabet> alphabet; + private final long minTimerQueryWaitTime; + private final LocalTimerMealyOutputSymbol silentOutput; // used for symbol filtering + + public LocalTimerMealyObservationTable(Alphabet> alphabet, long minTimerQueryWaitTime, + @NonNull SymbolFilter, NonDelayingInput> symbolFilter, O silentOutput) { + this.alphabet = alphabet; + + this.symbolFilter = symbolFilter; + this.silentOutput = new LocalTimerMealyOutputSymbol<>(silentOutput); + this.minTimerQueryWaitTime = minTimerQueryWaitTime; + + this.timerInfoMap = new HashMap<>(); + + this.shortPrefixRowMap = new HashMap<>(); + this.sortedShortPrefixes = new ArrayList<>(); + + this.longPrefixRowMap = new HashMap<>(); + this.longPrefixList = new ArrayList<>(); + + this.rowContentMap = new HashMap<>(); + } + + /** + * Infers local timers for the provided location. + * + * @param location Source location. + */ + private void identifyLocalTimers(LocationTimerInfo location, AbstractTimedQueryOracle timeOracle) { + var timerQueryResponse = timeOracle.queryTimers(location.getPrefix(), this.minTimerQueryWaitTime); + + if (timerQueryResponse.aborted()) { + var newOneShot = LStarLocalTimerMealy.selectOneShotTimer(timerQueryResponse.timers(), Long.MAX_VALUE); + newOneShot.setOneShot(); + } + + // Add timers up to one-shot: + for (var timer : timerQueryResponse.timers()) { + location.addTimer(timer); + this.extendAlphabet(new TimeStepSequence<>(timer.initial())); + if (!timer.periodic()) { + break; + } + } + } + + /** + * Extends the global alphabet without adding new transitions. + * + * @param symbol New alphabet symbol + */ + private void extendAlphabet(TimeStepSequence symbol) { + if (!alphabet.containsSymbol(symbol)) { + alphabet.asGrowingAlphabetOrThrowException().addSymbol(symbol); + } + + for (RowImpl> prefix : this.shortPrefixRowMap.values()) { + prefix.ensureInputCapacity(alphabet.size()); + } + } + + /** + * Adds the initial location. + * + * @return Corresponding row in the OT + */ + private RowImpl> addInitialLocation() { + RowImpl> newRow = new RowImpl<>(Word.epsilon(), 0, alphabet.size()); + newRow.makeShort(alphabet.size()); + this.shortPrefixRowMap.put(Word.epsilon(), newRow); + this.sortedShortPrefixes.add(newRow); + this.sortedShortPrefixes.sort(Comparator.comparing(r -> r.getLabel().toString())); + + return newRow; + } + + + /** + * Adds a new location that belongs to the provided short-prefix row. + * Infers timers for this location and creates outgoing transitions. + * + * @param newRow Newly-added short prefix row + * @param timeOracle Time oracle + */ + private void initLocation(RowImpl> newRow, AbstractTimedQueryOracle timeOracle) { + LocationTimerInfo timerInfo = new LocationTimerInfo<>(newRow.getLabel()); + this.identifyLocalTimers(timerInfo, timeOracle); + + if (timerInfo.getLastTimer() != null) { // location has timer + this.timerInfoMap.put(newRow.getLabel(), timerInfo); + } + + // Add outgoing transitions: + List>> transitions = this.createOutgoingTransitions(newRow, timeOracle); + transitions.forEach(t -> this.queryAllSuffixes(t, timeOracle)); + } + + /** + * Creates transitions for the provided short-prefix row. Adds transitions for non-delaying inputs + * and a transition for the one-shot timer of the location, if present. + *

+ * If a symbol filter is provided, the filter is queried before adding a transition for a non-delaying input. + * If the filter considers the input a silent self-loop, no transition is explicitly created for the input. + * + * @param spRow Short prefix row + * @param timeOracle Time query oracle + * @return New transitions + */ + private List>> createOutgoingTransitions(RowImpl> spRow, AbstractTimedQueryOracle timeOracle) { + List>> transitions = new ArrayList<>(); + + Word> sp = spRow.getLabel(); + + // First, add transitions for non-delaying symbols: + for (int i = 0; i < alphabet.size(); i++) { + LocalTimerMealySemanticInputSymbol sym = alphabet.getSymbol(i); + if (sym instanceof TimeStepSequence || sym instanceof TimeoutSymbol) { + continue; + } + + Word> lp = sp.append(sym); + assert !this.shortPrefixRowMap.containsKey(lp); + + RowImpl> succRow = this.longPrefixRowMap.get(lp); + if (succRow == null) { + // Query symbol filter before adding transition: + var filterResponse = this.symbolFilter.query(sp, (NonDelayingInput) sym); + if (filterResponse == SymbolFilterResponse.IGNORE) { + // Verify that output is silent: + var response = timeOracle.querySuffixOutput(sp, Word.fromLetter(sym)); + assert response.size() == 1; + if (!response.firstSymbol().equals(silentOutput)) { + // Not silent -> cannot be silent self-loop: + filterResponse = SymbolFilterResponse.ACCEPT; + + // Update filter: + this.symbolFilter.update(sp, (NonDelayingInput) sym, SymbolFilterResponse.ACCEPT); + } + } + + if (filterResponse == SymbolFilterResponse.ACCEPT) { + // Treat as usual: + succRow = this.createLpRow(lp); + } + } + + spRow.setSuccessor(i, succRow); + if (succRow != null) { + transitions.add(succRow); + } + } + + // Second, add one-shot timer transition (if any): + var locTimers = timerInfoMap.getOrDefault(spRow.getLabel(), null); + if (locTimers != null && !locTimers.getLastTimer().periodic()) { + LocalTimerMealySemanticInputSymbol waitSym = new TimeStepSequence<>(locTimers.getLastTimer().initial()); + Word> lp = sp.append(waitSym); + assert !this.shortPrefixRowMap.containsKey(lp); + + RowImpl> succRow = this.longPrefixRowMap.get(lp); + if (succRow == null) { + succRow = this.createLpRow(lp); + } + spRow.setSuccessor(this.alphabet.getSymbolIndex(waitSym), succRow); + transitions.add(succRow); + } + + return transitions; + } + + private RowImpl> createLpRow(Word> prefix) { + RowImpl> newRow = new RowImpl<>(prefix, 0); + this.longPrefixRowMap.put(prefix, newRow); + this.longPrefixList.add(newRow); + if (this.longPrefixList.size() != this.longPrefixRowMap.size()) throw new AssertionError(); + + newRow.setLpIndex(0); // unused + + return newRow; + } + + /** + * Identify transitions that have not been closed. + * I.e., there is no state with the same suffix behavior. + * Also removes unused content ids. + *

+ * Guarantees that returned transition list order is deterministic. + */ + public List>>> findUnclosedTransitions() { + // Identify contentIds for locations: + Set spContentIds = this.shortPrefixRowMap.values().stream() + .map(RowImpl::getRowContentId) + .collect(Collectors.toSet()); + + // Group lp rows by their content id: + Map>>> lpContentMap = new HashMap<>(); + for (var lpRow : this.longPrefixRowMap.values()) { + lpContentMap.putIfAbsent(lpRow.getRowContentId(), new ArrayList<>()); + lpContentMap.get(lpRow.getRowContentId()).add(lpRow); + } + + // Identify ids that are not used by any SP: + List>>> unclosedRows = new ArrayList<>(); + List sortedLpIds = lpContentMap.keySet().stream().sorted().toList(); + for (var lpId : sortedLpIds) { + if (spContentIds.contains(lpId)) { + continue; + } + + // Sort row s.t. list order deterministic: + List>> unclosedWithId = lpContentMap.get(lpId); + unclosedWithId.sort(Comparator.comparing(r -> r.getLabel().toString())); + unclosedRows.add(unclosedWithId); + } + + // Remove unused content ids: + Set usedContentIds = Stream.concat(this.shortPrefixRowMap.values().stream(), this.longPrefixRowMap.values().stream()) + .map(RowImpl::getRowContentId).collect(Collectors.toSet()); + + List oldContentIds = this.rowContentMap.keySet().stream().toList(); + for (int oldId : oldContentIds) { + if (!usedContentIds.contains(oldId)) { + this.rowContentMap.remove(oldId); + } + } + + + return unclosedRows; + } + + @Override + public List>>> initialize(List>> initialShortPrefixes, + List>> initialSuffixes, + MembershipOracle, Word>> oracle) { + + if (isInitialized()) { + throw new IllegalStateException("Called initialize, but there are already rows present"); + } + if (!initialShortPrefixes.isEmpty()) { + throw new IllegalArgumentException("Init with short prefixes is not supported."); + } + if (!(oracle instanceof AbstractTimedQueryOracle timedOracle)) { + throw new IllegalArgumentException("Must use timed oracle!"); + } + + // Add initial suffixes: + for (Word> suffix : initialSuffixes) { + if (suffixSet.add(suffix)) { + suffixes.add(suffix); + } + } + + // 1. Create initial location: + var newLoc = this.addInitialLocation(); + this.initLocation(newLoc, timedOracle); + this.queryAllSuffixes(newLoc, timedOracle); + + // 2. Identify unclosed transitions: + return this.findUnclosedTransitions(); + } + + private void queryAllSuffixes(RowImpl> row, AbstractTimedQueryOracle timedOracle) { + Word> prefix = row.getLabel(); + + List>> suffixOutputs = new ArrayList<>(this.suffixes.size()); + for (Word> suffix : this.suffixes) { + Word> output = timedOracle.querySuffixOutput(prefix, suffix); + suffixOutputs.add(output); + } + + this.processSuffixOutputs(row, suffixOutputs); + } + + private void processSuffixOutputs(RowImpl> row, List>> rowContents) { + if (rowContents.isEmpty()) { + row.setRowContentId(NO_CONTENT); + return; + } + + RowContent content = new RowContent<>(rowContents); + int contentId = content.hashCode(); + this.rowContentMap.putIfAbsent(contentId, content); + row.setRowContentId(contentId); + } + + @Override + public boolean isInitialized() { + return !(shortPrefixRowMap.isEmpty() && longPrefixRowMap.isEmpty()); + } + + @Override + public boolean isInitialConsistencyCheckRequired() { + return false; + } + + @Override + public List>>> addSuffixes(Collection>> newSuffixes, MembershipOracle, Word>> oracle) { + if (!(oracle instanceof AbstractTimedQueryOracle timedOracle)) { + throw new IllegalArgumentException(); + } + + // 1. Extend current suffixes + identify new suffixes: + List>> newSuffixList = new ArrayList<>(); + for (Word> suffix : newSuffixes) { + if (this.suffixSet.add(suffix)) { + logger.debug(String.format("Adding new suffix '%s'", suffix)); + + newSuffixList.add(suffix); + this.suffixes.add(suffix); + } + } + if (newSuffixList.isEmpty()) { + return Collections.emptyList(); + } + + // 2. Update row content: + Stream.concat(shortPrefixRowMap.values().stream(), longPrefixRowMap.values().stream()).forEach(row -> { + List>> updatedOutputs = new ArrayList<>(); + if (row.getRowContentId() != NO_CONTENT) { + // Add existing suffix outputs: + updatedOutputs.addAll(this.rowContentMap.get(row.getRowContentId()).outputs()); + } + + for (Word> suffix : newSuffixList) { + Word> output = timedOracle.querySuffixOutput(row.getLabel(), suffix); + updatedOutputs.add(output); + } + + this.processSuffixOutputs(row, updatedOutputs); + }); + + return this.findUnclosedTransitions(); + } + + @Override + public List>>> addShortPrefixes(List>> shortPrefixes, MembershipOracle, Word>> oracle) { + throw new IllegalStateException("Not supported."); + } + + @Override + public List>>> toShortPrefixes(List>> lpRows, MembershipOracle, Word>> oracle) { + if (!(oracle instanceof AbstractTimedQueryOracle timedOracle)) { + throw new IllegalArgumentException(); + } + + for (Row> row : lpRows) { + logger.debug(String.format("Adding new location with prefix '%s'", row.getLabel())); + + final RowImpl> lpRow = (RowImpl>) row; + + // Delete from LP rows: + var removed = this.longPrefixRowMap.remove(row.getLabel()); + this.longPrefixList.remove(removed); + if (this.longPrefixList.size() != this.longPrefixRowMap.size()) throw new AssertionError(); + + // Add to SP rows: + this.shortPrefixRowMap.put(row.getLabel(), lpRow); + this.sortedShortPrefixes.add(lpRow); + this.sortedShortPrefixes.sort(Comparator.comparing(r -> r.getLabel().toString())); + + lpRow.makeShort(alphabet.size()); + + this.initLocation(lpRow, timedOracle); + } + return this.findUnclosedTransitions(); + } + + @Override + public List>>> addAlphabetSymbol(LocalTimerMealySemanticInputSymbol symbol, MembershipOracle, Word>> oracle) { + throw new IllegalStateException("Not supported."); + } + + @Override + public Alphabet> getInputAlphabet() { + return this.alphabet; + } + + @Override + public Collection>> getShortPrefixRows() { + if (this.sortedShortPrefixes.size() != this.shortPrefixRowMap.size()) throw new AssertionError(); + return Collections.unmodifiableList(this.sortedShortPrefixes); + } + + @Override + public Collection>> getLongPrefixRows() { + return Collections.unmodifiableList(this.longPrefixList); + } + + @Override + public Row> getRow(int idx) { + throw new IllegalStateException("Not supported. Use prefix to access rows instead."); + } + + @Override + public @Nullable Row> getRow(Word> prefix) { + if (this.shortPrefixRowMap.containsKey(prefix)) { + return this.shortPrefixRowMap.get(prefix); + } + if (this.longPrefixRowMap.containsKey(prefix)) { + return this.longPrefixRowMap.get(prefix); + } + return null; + } + + @Override + public int numberOfDistinctRows() { + return this.rowContentMap.size(); + } + + @Override + public List>> getSuffixes() { + return this.suffixes; + } + + @Override + @Nullable + public List>> rowContents(Row> row) { + if (this.rowContentMap.isEmpty()) { + // OT may be empty if only single location with timers: + if (!this.suffixes.isEmpty()) { + throw new AssertionError(); + } + return Collections.emptyList(); + } + + return this.rowContentMap.get(row.getRowContentId()).outputs(); + } + + @Override + public Word> transformAccessSequence(Word> word) { + throw new IllegalStateException("Not implemented."); + } + + + @Override + public boolean isAccessSequence(Word> word) { + throw new IllegalStateException("Not implemented."); + } + + public @Nullable MealyTimerInfo getTimerInfo(Word> prefix, long initial) { + var info = this.timerInfoMap.get(prefix); + if (info != null) { + return info.getTimerInfo(initial); + } + return null; + } + + @Nullable + public LocationTimerInfo getLocationTimerInfo(Row> sp) { + return this.timerInfoMap.getOrDefault(sp.getLabel(), null); + } + + /** + * Adds an outgoing transition for the given symbol to the given location + * and subsequently tests for unclosed transitions. + *

+ * Raises an error if this transition already exists. + * + * @param spRow Source location + * @param symbol Input symbol + * @param timeOracle Oracle + * @return List of unclosed rows. Empty, if none. + */ + public List>>> addOutgoingTransition(Row> spRow, LocalTimerMealySemanticInputSymbol symbol, AbstractTimedQueryOracle timeOracle) { + if (!this.alphabet.containsSymbol(symbol)) { + throw new IllegalArgumentException("Unknown symbol."); + } + + Word> transitionPrefix = spRow.getLabel().append(symbol); + + // Add long-prefix row: + if (this.getRow(transitionPrefix) != null) { + throw new AssertionError("Location already has an outgoing transition for the provided symbol"); + } + + RowImpl> succRow = this.createLpRow(transitionPrefix); + + // Set as successor: + int symIdx = this.alphabet.getSymbolIndex(symbol); + ((RowImpl>) spRow).setSuccessor(symIdx, succRow); + + // Update suffixes: + this.queryAllSuffixes(succRow, timeOracle); + + return this.findUnclosedTransitions(); + } + + public List>>> addTimerTransition(Row> spRow, MealyTimerInfo timeout, AbstractTimedQueryOracle timeOracle) { + return this.addOutgoingTransition(spRow, new TimeStepSequence<>(timeout.initial()), timeOracle); + } + + /** + * Removes a long prefix row. Should only be used when removing a transition of a former one-shot timer. + * When turning a long into a short prefix, use toShortPrefix instead, + * + * @param prefix Row prefix + */ + public void removeLpRow(Word> prefix) { + if (!this.longPrefixRowMap.containsKey(prefix)) { + throw new IllegalArgumentException("Attempting to remove lp row that does not exist."); + } + + // Remove lp row: + var removed = this.longPrefixRowMap.remove(prefix); + this.longPrefixList.remove(removed); + if (this.longPrefixList.size() != this.longPrefixRowMap.size()) throw new AssertionError(); + + // Unset as successor: + int symIdx = this.alphabet.getSymbolIndex(prefix.lastSymbol()); + RowImpl> spRow = this.shortPrefixRowMap.get(prefix.prefix(-1)); + assert spRow != null; + + spRow.setSuccessor(symIdx, null); + } + + // ============================= + + private record RowContent(List>> outputs) { + + @Override + public int hashCode() { + return outputs.toString().hashCode(); + } + } + + +} diff --git a/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/LocationTimerInfo.java b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/LocationTimerInfo.java new file mode 100644 index 0000000000..f0ab4616dc --- /dev/null +++ b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/LocationTimerInfo.java @@ -0,0 +1,119 @@ +package de.learnlib.algorithm.lstar.mmlt; + +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.automaton.time.mmlt.MealyTimerInfo; +import net.automatalib.word.Word; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; +import java.util.*; + +/** + * Stores information about local timers of a location. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class LocationTimerInfo implements Serializable { + + private static final Logger logger = LoggerFactory.getLogger(LocationTimerInfo.class); + + private final Map> timers; // name -> info + + // Keep a list of timers sorted by their initial value. This lets us avoid redundant sort operations. + private final List> sortedTimers; + + private final Word> prefix; + + public LocationTimerInfo(Word> prefix) { + this.prefix = prefix; + this.timers = new HashMap<>(); + this.sortedTimers = new ArrayList<>(); + } + + public Word> getPrefix() { + return prefix; + } + + // ==================== + + /** + * Adds a local timer to this location. + * + */ + public void addTimer(MealyTimerInfo timer) { + this.timers.put(timer.name(), timer); + this.sortedTimers.add(timer); + this.sortedTimers.sort(Comparator.comparingLong(MealyTimerInfo::initial)); + } + + public void removeTimer(String timerName) { + if (!this.timers.containsKey(timerName)) { + logger.warn("Attempted to remove an unknown timer."); + return; + } + MealyTimerInfo removedTimer = this.timers.remove(timerName); + this.sortedTimers.remove(removedTimer); + } + + @Nullable + public MealyTimerInfo getTimerInfo(long initial) { + Optional> timer = this.sortedTimers.stream().filter(t -> t.initial() == initial).findAny(); + return timer.orElse(null); + } + + + /** + * Returns the timer with the highest initial value + * + * @return Timer with maximum timeout. Null, if no timers defined. + */ + @Nullable + public MealyTimerInfo getLastTimer() { + if (this.timers.isEmpty()) { + return null; + } + return sortedTimers.get(sortedTimers.size() - 1); + } + + /** + * Sets the given timer to one-shot, ensuring that there is only one one-shot timer at a time. + * This is preferred over setting the timer property. + * + * @param name Name of the new one-shot timer + */ + public void setOneShotTimer(String name) { + var oneShotTimer = this.timers.get(name); + if (oneShotTimer == null) { + throw new IllegalArgumentException("Unknown one-shot timer name."); + } + if (!oneShotTimer.equals(sortedTimers.get(sortedTimers.size() - 1))) { + throw new IllegalArgumentException("Only the timer with maximum timeout can be one-shot."); + } + + oneShotTimer.setOneShot(); + } + + /** + * Returns a list of all timers defined in this location, sorted by their initial value. + * + * @return List of local timers. Empty, if none. + */ + public List> getSortedTimers() { + return Collections.unmodifiableList(sortedTimers); + } + + /** + * Returns an unmodifiable view of the timers defined for this location. + * Format: name -> info + * + * @return Map of local timers. Empty, if none defined. + */ + @NonNull + public Map> getLocalTimers() { + return Collections.unmodifiableMap(this.timers); + } +} diff --git a/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/CexPreprocessor.java b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/CexPreprocessor.java new file mode 100644 index 0000000000..d4ade30825 --- /dev/null +++ b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/CexPreprocessor.java @@ -0,0 +1,37 @@ +package de.learnlib.algorithm.lstar.mmlt.cex; + +import de.learnlib.query.DefaultQuery; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealyOutputSymbol; +import net.automatalib.word.Word; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CexPreprocessor { + + private static final Logger logger = LoggerFactory.getLogger(CexPreprocessor.class); + + /** + * Cuts-off a counterexample when its output deviates from the hypothesis output. + * + * @param cexQuery Counterexample + * @param hypAnswer Hypothesis response to suffix of the counterexample + * @return The shortened counterexample, or null, if hypothesis and SUL show identical behavior. + */ + public static DefaultQuery, Word>> truncateCEX(DefaultQuery, Word>> cexQuery, + Word> hypAnswer) { + + for (int i = 0; i < cexQuery.getSuffix().size(); i++) { + if (!hypAnswer.getSymbol(i).equals(cexQuery.getOutput().getSymbol(i))) { + // Cut after deviation: + return new DefaultQuery<>(cexQuery.getPrefix(), + cexQuery.getSuffix().prefix(i + 1), + cexQuery.getOutput().prefix(i + 1)); + } + } + return null; // no deviation found + } + +} + diff --git a/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/ExtendedDecomposition.java b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/ExtendedDecomposition.java new file mode 100644 index 0000000000..5fac3ec1e1 --- /dev/null +++ b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/ExtendedDecomposition.java @@ -0,0 +1,39 @@ +package de.learnlib.algorithm.lstar.mmlt.cex; + + +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.automaton.time.mmlt.semantics.LocalTimerMealyConfiguration; +import net.automatalib.word.Word; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * An extended decomposition represents a transition with an incorrect target or output in the expanded form of + * a hypothesis MMLT. + * + * @param state Source state in expanded form of hypothesis + * @param input Input of some transition with incorrect output or target source state + * @param discriminator If not null: transition has incorrect target + * @param Input type for non-delaying inputs + */ +record ExtendedDecomposition(LocalTimerMealyConfiguration state, + @NonNull LocalTimerMealySemanticInputSymbol input, + @Nullable Word> discriminator) { + + public ExtendedDecomposition(LocalTimerMealyConfiguration state, @NonNull LocalTimerMealySemanticInputSymbol input) { + this(state, input, null); + } + + public boolean isForIncorrectOutput() { + return this.discriminator == null; + } + + @Override + public String toString() { + if (this.isForIncorrectOutput()) { + return String.format("Incorrect output (%s|%s)", state, input); + } else { + return String.format("Incorrect target (%s|%s|%s)", state, input, discriminator); + } + } +} diff --git a/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/LocalTimerMealyCounterexampleDecompositor.java b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/LocalTimerMealyCounterexampleDecompositor.java new file mode 100644 index 0000000000..275bacfff7 --- /dev/null +++ b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/LocalTimerMealyCounterexampleDecompositor.java @@ -0,0 +1,139 @@ +package de.learnlib.algorithm.lstar.mmlt.cex; + +import de.learnlib.acex.AcexAnalyzer; +import de.learnlib.algorithm.lstar.mmlt.hyp.LocalTimerMealyHypothesis; +import de.learnlib.oracle.AbstractTimedQueryOracle; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.TimeStepSequence; +import net.automatalib.alphabet.time.mmlt.TimeStepSymbol; +import net.automatalib.alphabet.time.mmlt.TimeoutSymbol; +import net.automatalib.automaton.time.mmlt.semantics.LocalTimerMealyConfiguration; +import net.automatalib.word.Word; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implements the search for an extended decomposition of a truncated counterexample and the post-processing of an extended decomposition. + * + * @param Location type + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +class LocalTimerMealyCounterexampleDecompositor { + + private static final Logger logger = LoggerFactory.getLogger(LocalTimerMealyCounterexampleDecompositor.class); + + + private final AbstractTimedQueryOracle timeOracle; + private final AcexAnalyzer acexAnalyzer; + + public LocalTimerMealyCounterexampleDecompositor(AbstractTimedQueryOracle timeOracle, AcexAnalyzer acexAnalyzer) { + this.timeOracle = timeOracle; + this.acexAnalyzer = acexAnalyzer; + } + + ExtendedDecomposition findExtendedDecomposition(LocalTimerMealyOutputInconsistency outIncons, + LocalTimerMealyHypothesis hypothesis) { + + if (outIncons.suffix().length() == 1) { + // Incorrect output: + var prefixState = hypothesis.getSemantics().traceInputs(outIncons.prefix()); + return new ExtendedDecomposition<>(prefixState, outIncons.suffix().firstSymbol()); + } + + // Verify breakpoint condition: + LocalTimerMealyInconsPrefixTransformAcex acex = new LocalTimerMealyInconsPrefixTransformAcex<>(outIncons.suffix(), timeOracle, + w -> hypothesis.getPrefix(outIncons.prefix().concat(w))); + + if (acex.testEffects(0, acex.getLength() - 1)) { + // Breakpoint condition not met -> must be incorrect output: + var lastStatePrefix = outIncons.prefix().concat(outIncons.suffix().prefix(outIncons.suffix().length() - 1)); + var lastState = hypothesis.getSemantics().traceInputs(lastStatePrefix); + + return new ExtendedDecomposition<>(lastState, outIncons.suffix().lastSymbol()); + } + + // Breakpoint condition met -> find decomposition: + int breakpoint = this.acexAnalyzer.analyzeAbstractCounterexample(acex); + if (acex.testEffects(breakpoint, breakpoint + 1)) { + throw new AssertionError("Failed to find valid decomposition."); + } + + // Get components: + Word> prefix = outIncons.prefix().concat(outIncons.suffix().prefix(breakpoint)); + LocalTimerMealySemanticInputSymbol sym = outIncons.suffix().getSymbol(breakpoint); + Word> discriminator = outIncons.suffix().subWord(breakpoint + 1); + + var prefixState = hypothesis.getSemantics().traceInputs(prefix); + + logger.debug(String.format("Decomposing to %s|%s|%s %n" + "Output at %d: %s. %nOutput at %d: %s", prefixState, sym, discriminator, breakpoint, + acex.computeEffect(breakpoint), breakpoint + 1, acex.computeEffect(breakpoint + 1))); + + return new ExtendedDecomposition<>(prefixState, sym, discriminator); + } + + /** + * Post-processes an extended decomposition: if the decomposition corresponds to a transition with an incorrect target or output at a timeout symbol, + * transforms the decomposition so that the input is either a non-delaying input or a single time step. + * + * @param decomposition Extended decomposition + * @return Post-processed decomposition + */ + ExtendedDecomposition postProcessExtendedDecomposition(ExtendedDecomposition decomposition, + LocalTimerMealyHypothesis hypothesis) { + if (!(decomposition.input() instanceof TimeoutSymbol)) { + return decomposition; + } + + var statePrefix = hypothesis.getPrefix(decomposition.state()); + var hypOutput = hypothesis.getSemantics().computeSuffixOutput(statePrefix, Word.fromLetter(decomposition.input())); + var sulOutput = timeOracle.querySuffixOutput(statePrefix, Word.fromLetter(decomposition.input())); + + if (decomposition.isForIncorrectOutput()) { + // Incorrect output at tout: + long minWaitTime; + if (hypOutput.firstSymbol().getDelay() == 0 && sulOutput.firstSymbol().getDelay() != 0) { + throw new AssertionError(); + } else if (hypOutput.firstSymbol().getDelay() != 0 && sulOutput.firstSymbol().getDelay() == 0) { + // If there is no timeout in either hyp or sul, need to trigger next observable timeout: + minWaitTime = hypOutput.firstSymbol().getDelay(); + } else { + // If there is a timeout in hyp and sul, go to next timeout: + minWaitTime = Math.min(hypOutput.firstSymbol().getDelay(), sulOutput.firstSymbol().getDelay()); + } + + // if minimum time is zero (= no timeout) or one, need to append empty word to prefix: + LocalTimerMealyConfiguration newPrefixState; + if (minWaitTime <= 1) { + newPrefixState = decomposition.state(); + } else { + newPrefixState = hypothesis.getSemantics().traceInputs(statePrefix.append(new TimeStepSequence<>(minWaitTime - 1))); + } + + logger.debug("Updated incorrect output at tout during post-processing."); + return new ExtendedDecomposition<>(newPrefixState, new TimeStepSymbol<>()); + } else { + if (decomposition.state().isStableConfig() || hypOutput.equals(sulOutput)) { + // stable-configuration or same output at tout -> same wait time for output in hyp and SUL: + if (hypOutput.firstSymbol().getDelay() != sulOutput.firstSymbol().getDelay()) { + throw new AssertionError(); + } + + long waitTime = hypOutput.firstSymbol().getDelay(); + LocalTimerMealyConfiguration newPrefixState; + if (waitTime <= 1) { + newPrefixState = decomposition.state(); + } else { + newPrefixState = hypothesis.getSemantics().traceInputs(statePrefix.append(new TimeStepSequence<>(waitTime - 1))); + } + + logger.debug("Updated incorrect target at tout during post-processing."); + return new ExtendedDecomposition<>(newPrefixState, new TimeStepSymbol<>(), decomposition.discriminator()); + } else { + // different output at tout -> found incorrect output: + logger.debug("Found incorrect output through post-processing."); + return postProcessExtendedDecomposition(new ExtendedDecomposition<>(decomposition.state(), decomposition.input()), hypothesis); + } + } + } +} diff --git a/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/LocalTimerMealyCounterexampleHandler.java b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/LocalTimerMealyCounterexampleHandler.java new file mode 100644 index 0000000000..f2d4fdc707 --- /dev/null +++ b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/LocalTimerMealyCounterexampleHandler.java @@ -0,0 +1,177 @@ +package de.learnlib.algorithm.lstar.mmlt.cex; + +import de.learnlib.acex.AcexAnalyzer; +import de.learnlib.algorithm.lstar.mmlt.LStarLocalTimerMealy; +import de.learnlib.algorithm.lstar.mmlt.cex.results.*; +import de.learnlib.algorithm.lstar.mmlt.hyp.LocalTimerMealyHypothesis; +import de.learnlib.oracle.AbstractTimedQueryOracle; +import de.learnlib.statistic.container.DummyStatsContainer; +import de.learnlib.statistic.container.LearnerStatsProvider; +import de.learnlib.statistic.container.StatsContainer; +import de.learnlib.symbol_filter.SymbolFilter; +import de.learnlib.symbol_filter.SymbolFilterResponse; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.NonDelayingInput; +import net.automatalib.alphabet.time.mmlt.TimeStepSymbol; +import net.automatalib.alphabet.time.mmlt.TimeoutSymbol; +import net.automatalib.automaton.time.mmlt.MealyTimerInfo; +import net.automatalib.word.Word; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * Processes a truncated counterexample for a hypothesis MMLT: + * searches for an extended decomposition, post-processes it, and infers an inaccuracy from it. + * + * @param Location type + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class LocalTimerMealyCounterexampleHandler implements LearnerStatsProvider { + private static final Logger logger = LoggerFactory.getLogger(LocalTimerMealyCounterexampleHandler.class); + private final SymbolFilter, NonDelayingInput> symbolFilter; + private StatsContainer stats = new DummyStatsContainer(); + + protected final AbstractTimedQueryOracle timeOracle; + private final LocalTimerMealyCounterexampleDecompositor decompositor; + + public LocalTimerMealyCounterexampleHandler(AbstractTimedQueryOracle timeOracle, AcexAnalyzer acexAnalyzer, @NonNull SymbolFilter, NonDelayingInput> symbolFilter) { + this.timeOracle = timeOracle; + this.decompositor = new LocalTimerMealyCounterexampleDecompositor<>(timeOracle, acexAnalyzer); + this.symbolFilter = symbolFilter; + } + + @Override + public void setStatsContainer(StatsContainer container) { + this.stats = container; + } + + public CexAnalysisResult analyzeInconsistency(LocalTimerMealyOutputInconsistency outIncons, + LocalTimerMealyHypothesis hypothesis) { + + // Search for an extended decomposition: + var decomposition = decompositor.findExtendedDecomposition(outIncons, hypothesis); + logger.debug("Found an extended decomposition: {}", decomposition); + + // Post-process the decomposition: + decomposition = decompositor.postProcessExtendedDecomposition(decomposition, hypothesis); + logger.debug("Post-processed decomposition: {}", decomposition); + + if (decomposition.isForIncorrectOutput()) { + return handleIncorrectOutput(decomposition, hypothesis); + } else { + return handleIncorrectTarget(decomposition, hypothesis); + } + } + + private CexAnalysisResult handleIncorrectOutput(ExtendedDecomposition decomposition, LocalTimerMealyHypothesis hypothesis) { + // Transition with incorrect output always implies missing one-shot timer: + logger.debug("Found missing one-shot via incorrect output."); + return this.selectOneShotTimer(decomposition, hypothesis, decomposition.state().getEntryDistance()); + } + + private CexAnalysisResult handleIncorrectTarget(ExtendedDecomposition decomposition, LocalTimerMealyHypothesis hypothesis) { + if (decomposition.input() instanceof NonDelayingInput ndi) { + // If decomposition at non-delaying input + considered as self-loop, treat as false ignore: + if (symbolFilter.query(hypothesis.getLocationPrefix(decomposition.state()), ndi) == SymbolFilterResponse.IGNORE) { + return new FalseIgnoreResult<>(decomposition.state().getLocation(), ndi); + } + + return this.handleIncorrectTargetNonDelaying(decomposition, hypothesis); + + } else if (decomposition.input() instanceof TimeStepSymbol) { + return this.handleIncorrectTargetTimeStep(decomposition, hypothesis); + } else { + throw new AssertionError("Unexpected symbol type."); + } + } + + private CexAnalysisResult selectOneShotTimer(ExtendedDecomposition decomposition, LocalTimerMealyHypothesis hypothesis, long maxInitialValue) { + var newOneShot = LStarLocalTimerMealy.selectOneShotTimer(hypothesis.getSortedTimers(decomposition.state().getLocation()), maxInitialValue); + logger.debug("Missing one-shot: setting ({}|{}) to one-shot.", hypothesis.getLocationPrefix(decomposition.state()), newOneShot); + return new MissingOneShotResult<>(decomposition.state().getLocation(), newOneShot); + } + + private CexAnalysisResult handleIncorrectTargetTimeStep(ExtendedDecomposition decomposition, LocalTimerMealyHypothesis hypothesis) { + // Check if there is a one-shot timer expiring at the next time step: + + List> localTimers = hypothesis.getSortedTimers(decomposition.state().getLocation()); + assert !localTimers.isEmpty(); + + // If location has a one-shot timer, this is the one with the highest initial value: + var lastTimer = localTimers.get(localTimers.size() - 1); + if (!lastTimer.periodic()) { + if (lastTimer.initial() - 1 != decomposition.state().getEntryDistance()) { + throw new AssertionError("Incorrect target must be at timeout of non-periodic timer."); + } + logger.debug("Inferred missing discriminator at timeout."); + return new MissingDiscriminatorResult<>(decomposition.state().getLocation(), decomposition.input(), decomposition.discriminator()); + } else if (!decomposition.state().isStableConfig()) { + logger.debug("Found missing one-shot via incorrect target in non-stable config."); + return this.selectOneShotTimer(decomposition, hypothesis, decomposition.state().getEntryDistance()); + } else { + logger.debug("Found missing one-shot via incorrect target in stable config."); + return new MissingOneShotResult<>(decomposition.state().getLocation(), localTimers.get(0)); // lowest initial value + } + } + + private CexAnalysisResult handleIncorrectTargetNonDelaying(ExtendedDecomposition decomposition, LocalTimerMealyHypothesis hypothesis) { + // 1: can be a missing discriminator? + + // Check if correct target in entry w.r.t. discriminator: + var transPrefix = hypothesis.getLocationPrefix(decomposition.state()).append(decomposition.input()); + var succState = hypothesis.getSemantics().traceInputs(transPrefix); // successor state in hypothesis + + var actualSuffixOutput = this.timeOracle.querySuffixOutput(transPrefix, decomposition.discriminator()); + var expSuffixOutput = this.timeOracle.querySuffixOutput(hypothesis.getPrefix(succState), decomposition.discriminator()); + + if (!actualSuffixOutput.equals(expSuffixOutput)) { + logger.debug("Inferred missing discriminator at non-delaying input."); + return new MissingDiscriminatorResult<>(decomposition.state().getLocation(), decomposition.input(), decomposition.discriminator()); + } + + // 2: can be a local reset? + if (decomposition.state().isStableConfig()) { + logger.debug("Inferred missing reset in stable config."); + return new MissingResetResult<>(decomposition.state().getLocation(), (NonDelayingInput) decomposition.input()); + } + + // Non-stable -> explicitly test for missing reset: + var isLocalReset = hypothesis.isLocalReset(decomposition.state().getLocation(), (NonDelayingInput) decomposition.input()); + var trans = hypothesis.getTransition(decomposition.state().getLocation(), (NonDelayingInput) decomposition.input()); + if (trans == null) { + throw new AssertionError(); + } + + // Must loop without reset: + if (trans.successor().equals(decomposition.state().getLocation()) && (!isLocalReset)) { + // Must have at least two stable configs: + var firstTimer = hypothesis.getSortedTimers(decomposition.state().getLocation()).get(0); + if (firstTimer.initial() > 1) { + // Must not self-loop in at least one non-entry stable config: + var resetTransPrefix = hypothesis.getPrefix(decomposition.state()) + .append(new TimeStepSymbol<>()) // prefix of first stable config that is not entry config + .append(decomposition.input()); // successor at $i$ in that config + + Word> suffix = Word.fromLetter(new TimeoutSymbol<>()); + var transSuffixOutput = this.timeOracle.querySuffixOutput(resetTransPrefix, suffix); + var entryConfigSuffixOutput = this.timeOracle.querySuffixOutput(hypothesis.getLocationPrefix(decomposition.state()), suffix); + + if (transSuffixOutput.equals(entryConfigSuffixOutput)) { + logger.debug("Inferred missing reset in non-stable config."); + return new MissingResetResult<>(decomposition.state().getLocation(), (NonDelayingInput) decomposition.input()); + } + } + + } + + // 3: add missing local reset + logger.debug("Inferred missing one-shot timer from incorrect target at non-delaying input."); + return this.selectOneShotTimer(decomposition, hypothesis, decomposition.state().getEntryDistance()); + } + + +} diff --git a/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/LocalTimerMealyInconsPrefixTransformAcex.java b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/LocalTimerMealyInconsPrefixTransformAcex.java new file mode 100644 index 0000000000..2430aa3940 --- /dev/null +++ b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/LocalTimerMealyInconsPrefixTransformAcex.java @@ -0,0 +1,66 @@ +package de.learnlib.algorithm.lstar.mmlt.cex; + +import de.learnlib.acex.AbstractBaseCounterexample; +import de.learnlib.oracle.AbstractTimedQueryOracle; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealyOutputSymbol; +import net.automatalib.word.Word; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.function.Function; + +/** + * An abstract counterexample used by the MMLT learner. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class LocalTimerMealyInconsPrefixTransformAcex extends AbstractBaseCounterexample>> { + + private final static Logger logger = LoggerFactory.getLogger(LocalTimerMealyInconsPrefixTransformAcex.class); + + private final AbstractTimedQueryOracle timeOracle; + private final Word> suffix; + + private final Function>, Word>> asTransform; + + /** + * Constructor. + * + * @param suffix suffix of the counterexample (= the word that we analyze) + * @param timeOracle membership oracle + * @param asTransform retrieves the prefix of the system state in the hypothesis addressed by a word + */ + public LocalTimerMealyInconsPrefixTransformAcex(Word> suffix, AbstractTimedQueryOracle timeOracle, Function>, Word>> asTransform) { + super(suffix.length()); + this.timeOracle = timeOracle; + this.suffix = suffix; + this.asTransform = asTransform; + } + + public Function>, Word>> getAsTransform() { + return asTransform; + } + + @Override + public Word> computeEffect(int index) { + // Split the word at our index: + Word> prefix = this.suffix.prefix(index); // everything up to *index* (exclusive) + Word> suffix = this.suffix.subWord(index); // everything from *index* (inclusive) + + // Identify access sequence of system state for prefix: + Word> accessSequence = this.asTransform.apply(prefix); + + // Query *hypothesis state* + *suffix*: + return this.timeOracle.querySuffixOutput(accessSequence, suffix); + } + + + @Override + public boolean checkEffects(Word> eff1, Word> eff2) { + // Same behavior at different indices? + logger.debug(String.format("Comparing (%s) AND (%s): %s", eff1, eff2, eff2.isSuffixOf(eff1))); + return eff2.isSuffixOf(eff1); + } +} \ No newline at end of file diff --git a/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/LocalTimerMealyOutputInconsistency.java b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/LocalTimerMealyOutputInconsistency.java new file mode 100644 index 0000000000..a5a584f79d --- /dev/null +++ b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/LocalTimerMealyOutputInconsistency.java @@ -0,0 +1,22 @@ +package de.learnlib.algorithm.lstar.mmlt.cex; + + +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealyOutputSymbol; +import net.automatalib.word.Word; + +/** + * Represents an output inconsistency used by the MMLT learner. + * + * @param prefix Prefix + * @param suffix Suffix input + * @param targetOut Suffix output in SUL + * @param hypOut Suffix output in hypothesis + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public record LocalTimerMealyOutputInconsistency(Word> prefix, + Word> suffix, + Word> targetOut, + Word> hypOut) { +} diff --git a/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/results/CexAnalysisResult.java b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/results/CexAnalysisResult.java new file mode 100644 index 0000000000..cb990699d4 --- /dev/null +++ b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/results/CexAnalysisResult.java @@ -0,0 +1,11 @@ +package de.learnlib.algorithm.lstar.mmlt.cex.results; + +/** + * Outcome of a counterexample analysis. + * + * @param Location type + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public abstract class CexAnalysisResult { +} diff --git a/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/results/FalseIgnoreResult.java b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/results/FalseIgnoreResult.java new file mode 100644 index 0000000000..36c4eaa5b3 --- /dev/null +++ b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/results/FalseIgnoreResult.java @@ -0,0 +1,29 @@ +package de.learnlib.algorithm.lstar.mmlt.cex.results; + + +import net.automatalib.alphabet.time.mmlt.NonDelayingInput; + +/** + * The specified symbol is considered to be falsely ignored by the symbol filter. + * + * @param Location type + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class FalseIgnoreResult extends CexAnalysisResult { + private final S location; + private final NonDelayingInput symbol; + + public FalseIgnoreResult(S location, NonDelayingInput symbol) { + this.location = location; + this.symbol = symbol; + } + + public S getLocation() { + return location; + } + + public NonDelayingInput getSymbol() { + return symbol; + } +} diff --git a/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/results/MissingDiscriminatorResult.java b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/results/MissingDiscriminatorResult.java new file mode 100644 index 0000000000..9e2034f7db --- /dev/null +++ b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/results/MissingDiscriminatorResult.java @@ -0,0 +1,36 @@ +package de.learnlib.algorithm.lstar.mmlt.cex.results; + +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.word.Word; + +/** + * The target at the identified transition is incorrect due to a missing discriminator. + * + * @param Location type + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class MissingDiscriminatorResult extends CexAnalysisResult { + private final S location; + private final LocalTimerMealySemanticInputSymbol input; + private final Word> discriminator; + + public MissingDiscriminatorResult(S location, LocalTimerMealySemanticInputSymbol input, Word> discriminator) { + this.location = location; + this.input = input; + this.discriminator = discriminator; + } + + public S getLocation() { + return location; + } + + public LocalTimerMealySemanticInputSymbol getInput() { + return input; + } + + public Word> getDiscriminator() { + return discriminator; + } + +} diff --git a/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/results/MissingOneShotResult.java b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/results/MissingOneShotResult.java new file mode 100644 index 0000000000..3da5d5f01c --- /dev/null +++ b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/results/MissingOneShotResult.java @@ -0,0 +1,28 @@ +package de.learnlib.algorithm.lstar.mmlt.cex.results; + +import net.automatalib.automaton.time.mmlt.MealyTimerInfo; + +/** + * The provided timer should become one-shot. + * + * @param Location type + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class MissingOneShotResult extends CexAnalysisResult { + private final S location; + private final MealyTimerInfo timeout; + + public MissingOneShotResult(S location, MealyTimerInfo timeout) { + this.location = location; + this.timeout = timeout; + } + + public S getLocation() { + return location; + } + + public MealyTimerInfo getTimeout() { + return timeout; + } +} diff --git a/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/results/MissingResetResult.java b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/results/MissingResetResult.java new file mode 100644 index 0000000000..e6e538a258 --- /dev/null +++ b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/results/MissingResetResult.java @@ -0,0 +1,30 @@ +package de.learnlib.algorithm.lstar.mmlt.cex.results; + + +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.NonDelayingInput; + +/** + * There should be a local reset at the specified transition. + * + * @param Location type + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class MissingResetResult extends CexAnalysisResult { + private final S location; + private final NonDelayingInput input; + + public MissingResetResult(S location, NonDelayingInput input) { + this.location = location; + this.input = input; + } + + public S getLocation() { + return location; + } + + public LocalTimerMealySemanticInputSymbol getInput() { + return input; + } +} diff --git a/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/hyp/IInternalLocalTimerMealyHypothesis.java b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/hyp/IInternalLocalTimerMealyHypothesis.java new file mode 100644 index 0000000000..53c0228c47 --- /dev/null +++ b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/hyp/IInternalLocalTimerMealyHypothesis.java @@ -0,0 +1,55 @@ +package de.learnlib.algorithm.lstar.mmlt.hyp; + +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.automaton.time.mmlt.MealyTimerInfo; +import net.automatalib.automaton.time.mmlt.semantics.LocalTimerMealyConfiguration; +import net.automatalib.word.Word; + +import java.util.List; + +/** + * Defines several methods that the learner can use to interact with its hypothesis. + * These methods should not be used by the teacher, to maintain separation between both. + * + * @param Location type + * @param Input type for non-delaying inputs + */ +public interface IInternalLocalTimerMealyHypothesis { + + /** + * Returns the prefix assigned to the provided configuration. + * The assigned prefix is the concatenation of the prefix assigned to the active location + * and the minimal number of time steps needed to reach the configuration after entering its location + * (= entry distance). + * + * @param configuration Considered configuration + * @return Assigned prefix + */ + Word> getPrefix(LocalTimerMealyConfiguration configuration); + + Word> getPrefix(Word> prefix); + + /** + * Returns the prefix assigned to the location that is active in the provided configuration. + * + * @param configuration Considered configuration + * @return Assigned prefix + */ + Word> getLocationPrefix(LocalTimerMealyConfiguration configuration); + + /** + * Returns a prefix for the given location. + * This prefix is deterministic in the RS learner. + * + * @param location Location + * @return Location prefix + */ + Word> getPrefix(S location); + + /** + * Convenience method that sorts timers of the provided location by initial value. + * + * @return Sorted timers. Empty list if no timers. + */ + List> getSortedTimers(S location); +} diff --git a/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/hyp/LocalTimerMealyHypothesis.java b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/hyp/LocalTimerMealyHypothesis.java new file mode 100644 index 0000000000..5a751c0096 --- /dev/null +++ b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/hyp/LocalTimerMealyHypothesis.java @@ -0,0 +1,118 @@ +package de.learnlib.algorithm.lstar.mmlt.hyp; + +import net.automatalib.alphabet.Alphabet; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealyInputSymbol; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.NonDelayingInput; +import net.automatalib.alphabet.time.mmlt.TimeStepSequence; +import net.automatalib.automaton.time.mmlt.AbstractSymbolCombiner; +import net.automatalib.automaton.time.mmlt.LocalTimerMealy; +import net.automatalib.automaton.time.mmlt.MealyTimerInfo; +import net.automatalib.automaton.time.mmlt.semantics.LocalTimerMealyConfiguration; +import net.automatalib.automaton.time.mmlt.semantics.LocalTimerMealySemantics; +import net.automatalib.word.Word; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * An MMLT hypothesis that includes a prefix mapping. + * This mapping assigns a short prefix to each location. + * + * @param Location type + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class LocalTimerMealyHypothesis implements LocalTimerMealy, IInternalLocalTimerMealyHypothesis { + private final LocalTimerMealy automaton; + private final Map>> prefixMap; // location -> prefix + + public LocalTimerMealyHypothesis(LocalTimerMealy automaton, Map>> prefixMap) { + this.automaton = automaton; + this.prefixMap = prefixMap; + } + + @Override + public Word> getPrefix(LocalTimerMealyConfiguration configuration) { + var locPrefix = getLocationPrefix(configuration); + if (configuration.isEntryConfig()) { + return locPrefix; // entry distance = 0 + } else { + return locPrefix.append(new TimeStepSequence<>(configuration.getEntryDistance())); + } + } + + @Override + public Word> getPrefix(Word> prefix) { + var resultingConfig = getSemantics().traceInputs(prefix); + return getPrefix(resultingConfig); + } + + + @Override + public Word> getLocationPrefix(LocalTimerMealyConfiguration configuration) { + var locPrefix = this.prefixMap.get(configuration.getLocation()); + if (locPrefix == null) throw new AssertionError(); + return locPrefix; + } + + + @Override + public Word> getPrefix(S location) { + return prefixMap.get(location); + } + + @Override + public O getSilentOutput() { + return automaton.getSilentOutput(); + } + + @Override + public AbstractSymbolCombiner getOutputCombiner() { + return automaton.getOutputCombiner(); + } + + @Override + public Alphabet> getInputAlphabet() { + return automaton.getInputAlphabet(); + } + + @Override + public Alphabet> getUntimedAlphabet() { + return automaton.getUntimedAlphabet(); + } + + @Override + public S getInitialState() { + return automaton.getInitialState(); + } + + @Override + public Collection getStates() { + return automaton.getStates(); + } + + @Override + public @Nullable LocalTimerMealyTransition getTransition(S location, LocalTimerMealyInputSymbol input) { + return automaton.getTransition(location, input); + } + + @Override + public boolean isLocalReset(S location, NonDelayingInput input) { + return automaton.isLocalReset(location, input); + } + + @Override + public List> getSortedTimers(S location) { + return automaton.getSortedTimers(location); + } + + @Override + public LocalTimerMealySemantics getSemantics() { + return new net.automatalib.automaton.time.impl.mmlt.LocalTimerMealySemantics<>(this); + } + + +} diff --git a/algorithms/active/lstar/src/main/java/module-info.java b/algorithms/active/lstar/src/main/java/module-info.java index 5ec25fb05b..c4e6a3c508 100644 --- a/algorithms/active/lstar/src/main/java/module-info.java +++ b/algorithms/active/lstar/src/main/java/module-info.java @@ -54,4 +54,5 @@ exports de.learnlib.algorithm.lstar.moore; exports de.learnlib.algorithm.malerpnueli; exports de.learnlib.algorithm.rivestschapire; + exports de.learnlib.algorithm.lstar.mmlt; } diff --git a/algorithms/active/lstar/src/test/java/de/learnlib/algorithm/lstar/mmlt/LStarLocalTimerMealyBenchmarkTests.java b/algorithms/active/lstar/src/test/java/de/learnlib/algorithm/lstar/mmlt/LStarLocalTimerMealyBenchmarkTests.java new file mode 100644 index 0000000000..bd356ab47f --- /dev/null +++ b/algorithms/active/lstar/src/test/java/de/learnlib/algorithm/lstar/mmlt/LStarLocalTimerMealyBenchmarkTests.java @@ -0,0 +1,179 @@ +package de.learnlib.algorithm.lstar.mmlt; + + +import de.learnlib.algorithm.LocalTimerMealyModelParams; +import de.learnlib.driver.simulator.LocalTimerMealySimulatorSUL; +import de.learnlib.filter.cache.mmlt.TimeoutReducerSUL; +import de.learnlib.filter.statistic.sul.LocalTimerMealyStatsSUL; +import de.learnlib.oracle.EquivalenceOracle; +import de.learnlib.oracle.equivalence.mmlt.LocalTimerMealyEQOracleChain; +import de.learnlib.oracle.equivalence.mmlt.LocalTimerMealyRandomWpOracle; +import de.learnlib.oracle.equivalence.mmlt.LocalTimerMealySimulatorOracle; +import de.learnlib.oracle.equivalence.mmlt.ResetSearchOracle; +import de.learnlib.oracle.membership.TimedQueryOracle; +import de.learnlib.oracle.symbol_filters.AcceptAllSymbolFilter; +import de.learnlib.oracle.symbol_filters.CachedSymbolFilter; +import de.learnlib.oracle.symbol_filters.IgnoreAllSymbolFilter; +import de.learnlib.oracle.symbol_filters.mmlt.*; +import de.learnlib.query.DefaultQuery; +import de.learnlib.statistic.container.StatsContainer; +import de.learnlib.sul.LocalTimerMealySUL; +import de.learnlib.symbol_filter.SymbolFilter; +import de.learnlib.testsupport.example.mmlt.LocalTimerMealyExamples; +import de.learnlib.util.statistic.container.MapStatsContainer; +import net.automatalib.alphabet.impl.GrowingMapAlphabet; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealyOutputSymbol; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.NonDelayingInput; +import net.automatalib.alphabet.time.mmlt.TimeoutSymbol; +import net.automatalib.automaton.time.mmlt.LocalTimerMealy; +import net.automatalib.word.Word; +import org.testng.annotations.Test; + +import de.learnlib.filter.cache.mmlt.LocalTimerMealyTreeSULCache; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * Integration tests for the MMLT learner that uses several EQ oracles, symbol filters + * and a cache to learn different models. + */ +@Test +public class LStarLocalTimerMealyBenchmarkTests { + + private enum FilterMode { + none, random, ignore_all, perfect + } + + private static void runExperiment(LStarLocalTimerMealy learner, EquivalenceOracle.LocalTimerMealyEquivalenceOracle tester, StatsContainer stats, int maxRounds, + boolean printFinalResult) { + stats.startOrResumeClock("learningRt", "Processing time"); + learner.startLearning(); + + var hyp = learner.getHypothesisModel(); + DefaultQuery, Word>> cex = tester.findCounterExample(hyp, hyp.getSemantics().getInputAlphabet()); + stats.increaseCounter("roundCount", "CEX queries"); + + int roundCount = 1; + while (cex != null && roundCount < maxRounds) { + learner.refineHypothesis(cex); + hyp = learner.getHypothesisModel(); + cex = tester.findCounterExample(hyp, hyp.getSemantics().getInputAlphabet()); + stats.increaseCounter("roundCount", null); + roundCount += 1; + } + stats.pauseClock("learningRt"); + + final var finalHypothesis = learner.getHypothesisModel(); + + // Add some more stats: + stats.setCounter("result_locs", "Locations in result", finalHypothesis.getStates().size()); + + // Print stats: + stats.printStats(); + + if (printFinalResult) { + System.out.println("Final hypothesis:"); + LocalTimerMealyTestUtil.printModel(finalHypothesis); + //new ObservationTableASCIIWriter<>().write(learner.getObservationTable(), System.out); + } + } + + private static void learnModel(String name, LocalTimerMealy automaton, LocalTimerMealyModelParams params, + FilterMode symbolFilterMode, long seed, boolean printResults) { + + // Add some stats: + var stats = new MapStatsContainer(); + stats.addTextInfo("LocalTimerMealyModel", null, name); + stats.setCounter("original_locs", "Locations in original", automaton.getStates().size()); + stats.setCounter("original_inputs", "Untimed alphabet size in original", automaton.getUntimedAlphabet().size()); + + // Set up a pipeline: + GrowingMapAlphabet> alphabet = new GrowingMapAlphabet<>(); + alphabet.addAll(automaton.getUntimedAlphabet()); + + // Query oracle -> TimeoutReducer -> Cache -> Query stats -> SUL + LocalTimerMealySimulatorSUL sul = new LocalTimerMealySimulatorSUL<>(automaton); + LocalTimerMealyStatsSUL statsAfterCache = new LocalTimerMealyStatsSUL<>(sul, stats); + LocalTimerMealyTreeSULCache cacheSUL = new LocalTimerMealyTreeSULCache<>(statsAfterCache, params); + cacheSUL.setStatsContainer(stats); + LocalTimerMealySUL toReducerSul = new TimeoutReducerSUL<>(cacheSUL, params.maxTimeoutWaitingTime(), stats); + + TimedQueryOracle timeOracle = new TimedQueryOracle<>(toReducerSul, params); + + // Prepare cex oracle chain: + + LocalTimerMealyEQOracleChain chainOracle = new LocalTimerMealyEQOracleChain<>(); + chainOracle.addOracle(cacheSUL.createCacheConsistencyTest()); + chainOracle.addOracle(new ResetSearchOracle<>(timeOracle, seed, 1.0, 1.0)); + chainOracle.addOracle(new LocalTimerMealyRandomWpOracle<>(timeOracle, seed, 16, 0, 100)); + chainOracle.addOracle(new LocalTimerMealySimulatorOracle<>(automaton)); // ensure that we eventually find an accurate model + chainOracle.setStatsContainer(stats); + + // Create learner: + List>> suffixes = new ArrayList<>(); + alphabet.forEach(s -> suffixes.add(Word.fromLetter(s))); + suffixes.add(Word.fromLetter(new TimeoutSymbol<>())); + + // Configure symbol filter: + SymbolFilter, NonDelayingInput> filter = new AcceptAllSymbolFilter<>(); // pass-through + switch (symbolFilterMode) { + case perfect -> filter = new LocalTimerMealyPerfectSymbolFilter<>(automaton); + case random -> filter = new LocalTimerMealyRandomSymbolFilter<>(automaton, 0.1, new Random(seed)); + case ignore_all -> filter = new IgnoreAllSymbolFilter<>(); + } + + filter = new LocalTimerMealyStatisticsSymbolFilter<>(automaton, filter, stats); + filter = new CachedSymbolFilter<>(filter); // need to wrap to enable updates to responses + + var learner = new LStarLocalTimerMealy<>(alphabet, params, suffixes, timeOracle, filter); + learner.setStatsContainer(stats); + + // Start learning: + runExperiment(learner, chainOracle, stats, 100, printResults); + } + + + @Test + public void learnExamplesNoFilter() { + for (String modelFile : LocalTimerMealyTestUtil.listModelFiles()) { + var model = LocalTimerMealyTestUtil.automatonFromFile(modelFile); + learnModel(model.name(), model.automaton(), model.params(), FilterMode.none, 100, true); + } + LocalTimerMealyExamples.getAll().forEach(m -> + learnModel(m.name(), m.automaton(), m.params(), FilterMode.none, 100, true)); + } + + @Test + public void learnExamplesIgnoreAllFilter() { + for (String modelFile : LocalTimerMealyTestUtil.listModelFiles()) { + var model = LocalTimerMealyTestUtil.automatonFromFile(modelFile); + learnModel(modelFile, model.automaton(), model.params(), FilterMode.ignore_all, 100, true); + } + LocalTimerMealyExamples.getAll().forEach(m -> + learnModel(m.name(), m.automaton(), m.params(), FilterMode.ignore_all, 100, true)); + } + + @Test + public void learnExamplesPerfectFilter() { + for (String modelFile : LocalTimerMealyTestUtil.listModelFiles()) { + var model = LocalTimerMealyTestUtil.automatonFromFile(modelFile); + learnModel(modelFile, model.automaton(), model.params(), FilterMode.perfect, 100, true); + } + LocalTimerMealyExamples.getAll().forEach(m -> + learnModel(m.name(), m.automaton(), m.params(), FilterMode.perfect, 100, true)); + } + + @Test + public void learnExamplesRandomFilter() { + for (String modelFile : LocalTimerMealyTestUtil.listModelFiles()) { + var model = LocalTimerMealyTestUtil.automatonFromFile(modelFile); + learnModel(modelFile, model.automaton(), model.params(), FilterMode.random, 100, true); + } + LocalTimerMealyExamples.getAll().forEach(m -> + learnModel(m.name(), m.automaton(), m.params(), FilterMode.random, 100, true)); + } + +} diff --git a/algorithms/active/lstar/src/test/java/de/learnlib/algorithm/lstar/mmlt/LStarLocalTimerMealyCounterexampleTests.java b/algorithms/active/lstar/src/test/java/de/learnlib/algorithm/lstar/mmlt/LStarLocalTimerMealyCounterexampleTests.java new file mode 100644 index 0000000000..edd3bd9f11 --- /dev/null +++ b/algorithms/active/lstar/src/test/java/de/learnlib/algorithm/lstar/mmlt/LStarLocalTimerMealyCounterexampleTests.java @@ -0,0 +1,251 @@ +package de.learnlib.algorithm.lstar.mmlt; + +import de.learnlib.driver.simulator.LocalTimerMealySimulatorSUL; +import de.learnlib.oracle.equivalence.mmlt.LocalTimerMealySimulatorOracle; +import de.learnlib.oracle.membership.TimedQueryOracle; +import de.learnlib.oracle.symbol_filters.AcceptAllSymbolFilter; +import de.learnlib.query.DefaultQuery; +import de.learnlib.datastructure.observationtable.writer.ObservationTableASCIIWriter; +import de.learnlib.testsupport.example.mmlt.LocalTimerMealyExamples; +import de.learnlib.testsupport.example.mmlt.LocalTimerMealyModel; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.NonDelayingInput; +import net.automatalib.alphabet.time.mmlt.TimeStepSymbol; +import net.automatalib.alphabet.time.mmlt.TimeoutSymbol; +import net.automatalib.alphabet.GrowingAlphabet; +import net.automatalib.alphabet.impl.GrowingMapAlphabet; +import net.automatalib.word.Word; +import net.automatalib.word.WordBuilder; +import org.testng.annotations.Test; + + +import java.util.Collections; +import java.util.List; + +/** + * Tests several different cases of counterexamples. + */ +@Test +public class LStarLocalTimerMealyCounterexampleTests { + + private static void learnModel(LocalTimerMealyModel model, List>> counterexamples) { + + GrowingAlphabet> alphabet = new GrowingMapAlphabet<>(); + model.automaton().getUntimedAlphabet().forEach(alphabet::addSymbol); + + LocalTimerMealySimulatorSUL sul = new LocalTimerMealySimulatorSUL<>(model.automaton()); + TimedQueryOracle timeOracle = new TimedQueryOracle<>(sul, model.params()); + + var learner = new LStarLocalTimerMealy<>(alphabet, model.params(), Collections.emptyList(), + timeOracle, new AcceptAllSymbolFilter<>()); + + learner.startLearning(); + + System.out.println("Initial hypothesis:"); + LocalTimerMealyTestUtil.printModel(learner.getHypothesisModel()); + + // Trigger the expected cases with the provided counterexamples: + for (var cex : counterexamples) { + System.out.println(); + new ObservationTableASCIIWriter<>().write(learner.getObservationTable(), System.out); + System.out.println(); + + var output = timeOracle.querySuffixOutput(Word.epsilon(), cex); + learner.refineHypothesis(new DefaultQuery<>(cex, output)); + + System.out.println("Current hypothesis:"); + LocalTimerMealyTestUtil.printModel(learner.getHypothesisModel()); + } + + // Now continue until arriving at an accurate model: + System.out.println("Running to completion"); + LocalTimerMealySimulatorOracle simOracle = new LocalTimerMealySimulatorOracle<>(model.automaton()); + int round = 0; + while (round < 100) { + var hyp = learner.getHypothesisModel(); + var cex = simOracle.findCounterExample(hyp, hyp.getSemantics().getInputAlphabet()); + if (cex != null) { + learner.refineHypothesis(cex); + } else { + break; + } + round++; + } + + System.out.println("Took " + (round + 1) + " additional rounds."); + System.out.println("Final hypothesis:"); + LocalTimerMealyTestUtil.printModel(learner.getHypothesisModel()); + System.out.println(); + new ObservationTableASCIIWriter<>().write(learner.getObservationTable(), System.out); + System.out.println(); + + } + + + private Word> getTimeStepSequence(int timeSteps) { + WordBuilder> wbTimeStep = new WordBuilder<>(); + wbTimeStep.repeatAppend(timeSteps, new TimeStepSymbol<>()); + return wbTimeStep.toWord(); + } + + private Word> getTimeoutSequence(int timeouts) { + WordBuilder> wbTimeouts = new WordBuilder<>(); + wbTimeouts.repeatAppend(timeouts, new TimeoutSymbol<>()); + return wbTimeouts.toWord(); + } + + @Test + public void testOverApproxReset() { + // Infers a missing local reset instead of a missing discriminator first. + var model = LocalTimerMealyTestUtil.automatonFromFile("over_approx_reset.dot"); + + // Missing discriminator at non-del in stable config: + List>> cex1 = List.of( + Word.fromSymbols(new TimeStepSymbol<>(), new NonDelayingInput<>("i"), new TimeoutSymbol<>()) + ); + + learnModel(model, cex1); + } + + @Test + public void testRecursiveDecomp() { + // Triggers recursive decomposition + var model = LocalTimerMealyTestUtil.automatonFromFile("recursive_decomp.dot", 3); + + // Missing discriminator at non-del in stable config: + List>> cex1 = List.of( + // Initial hyp: + Word.fromSymbols(new NonDelayingInput<>("p"), new NonDelayingInput<>("f")), + + Word.fromSymbols(new NonDelayingInput<>("u"), + new TimeoutSymbol<>(), new TimeoutSymbol<>(), new TimeoutSymbol<>(), new TimeoutSymbol<>(), + new NonDelayingInput<>("f")), + + Word.fromSymbols(new NonDelayingInput<>("u"), + new TimeoutSymbol<>(), new TimeoutSymbol<>(), new TimeoutSymbol<>(), new TimeoutSymbol<>(), + new TimeoutSymbol<>()) + ); + + learnModel(model, cex1); + } + + @Test + public void testMissingDiscriminators() { + var model = LocalTimerMealyExamples.SensorCollector(); + + // Missing discriminator at non-del in stable config: + List>> cex1 = List.of( + // Initial hyp: + Word.fromSymbols(new NonDelayingInput<>("p1"), new NonDelayingInput<>("p1")), + Word.fromSymbols(new NonDelayingInput<>("p2"), new NonDelayingInput<>("abort")), + + Word.fromSymbols(new NonDelayingInput<>("p2"), new TimeStepSymbol<>(), new NonDelayingInput<>("abort"), new TimeoutSymbol<>()) + ); + + // Missing discriminator at one-shot: + List>> cex2 = List.of( + // Initial hyp: + Word.fromSymbols(new NonDelayingInput<>("p1"), new NonDelayingInput<>("p1")), + Word.fromSymbols(new NonDelayingInput<>("p2"), new NonDelayingInput<>("abort")), + + Word.fromSymbols(new NonDelayingInput<>("p2"), new TimeoutSymbol<>(), new TimeoutSymbol<>()) + ); + + learnModel(model, cex1); + learnModel(model, cex2); + } + + @Test + public void testMissingResets() { + var model = LocalTimerMealyExamples.SensorCollector(); + model.params().setMaxTimerQueryWaitingTime(40); + + // Missing reset in stable config: + List>> cex1 = List.of( + // Initial hyp: + Word.fromSymbols(new NonDelayingInput<>("p1"), new NonDelayingInput<>("p1")), + Word.fromSymbols(new NonDelayingInput<>("p2"), new NonDelayingInput<>("abort")), + + Word.fromSymbols(new NonDelayingInput<>("p1"), new TimeStepSymbol<>(), new NonDelayingInput<>("abort"), new TimeoutSymbol<>()) + ); + + // Missing reset in non-stable config: + List>> cex2 = List.of( + // Initial hyp: + Word.fromSymbols(new NonDelayingInput<>("p1"), new NonDelayingInput<>("p1")), + Word.fromSymbols(new NonDelayingInput<>("p2"), new NonDelayingInput<>("abort")), + + Word.fromSymbols(new NonDelayingInput<>("p1"), + new TimeStepSymbol<>(), new TimeStepSymbol<>(), new TimeStepSymbol<>(), + new NonDelayingInput<>("abort"), new TimeoutSymbol<>()) + ); + + learnModel(model, cex1); + learnModel(model, cex2); + + } + + @Test + public void testMissingOneShotModelB() { + // Setting max waiting = 6 -> all inferred timers are periodic: + var model = LocalTimerMealyExamples.SensorCollector(); + model.params().setMaxTimerQueryWaitingTime(6); + + // Missing one-shot via bad return to entry: + List>> cex1 = List.of( + // Initial hyp: + Word.fromSymbols(new NonDelayingInput<>("p1"), new NonDelayingInput<>("p1")), + Word.fromSymbols(new NonDelayingInput<>("p2"), new NonDelayingInput<>("abort")), + + Word.fromWords(Word.fromLetter(new NonDelayingInput<>("p1")), getTimeoutSequence(14) + ) + ); + + // Missing one-shot in location with single timer: + List>> cex2 = List.of( + // Initial hyp: + Word.fromSymbols(new NonDelayingInput<>("p1"), new NonDelayingInput<>("p1")), + Word.fromSymbols(new NonDelayingInput<>("p2"), new NonDelayingInput<>("abort")), + + Word.fromSymbols(new NonDelayingInput<>("p2"), new TimeoutSymbol<>(), new TimeoutSymbol<>()) + ); + + learnModel(model, cex1); + learnModel(model, cex2); + } + + @Test + public void testMissingOneShotModelA() { + var model = LocalTimerMealyExamples.SensorCollector(); + model.params().setMaxTimerQueryWaitingTime(40); + + // Missing one-shot via bad output: + List>> cex1 = List.of( + // Initial hyp: + Word.fromSymbols(new NonDelayingInput<>("p1"), new NonDelayingInput<>("p1")), + Word.fromSymbols(new NonDelayingInput<>("p2"), new NonDelayingInput<>("abort")), + + Word.fromWords(Word.fromLetter(new NonDelayingInput<>("p1")), + getTimeStepSequence(40), + Word.fromLetter(new TimeoutSymbol<>())) // alternatively: new NonDelayingInput<>("abort") + ); + + // Missing one-shot via bad target: + List>> cex2 = List.of( + // Initial hyp: + Word.fromSymbols(new NonDelayingInput<>("p1"), new NonDelayingInput<>("p1")), + Word.fromSymbols(new NonDelayingInput<>("p2"), new NonDelayingInput<>("abort")), + + Word.fromWords(Word.fromLetter(new NonDelayingInput<>("p1")), + getTimeoutSequence(14), + Word.fromLetter(new NonDelayingInput<>("collect")), + Word.fromLetter(new NonDelayingInput<>("p1")) + ) + ); + + learnModel(model, cex1); + learnModel(model, cex2); + } + + +} diff --git a/algorithms/active/lstar/src/test/java/de/learnlib/algorithm/lstar/mmlt/LocalTimerMealyTestUtil.java b/algorithms/active/lstar/src/test/java/de/learnlib/algorithm/lstar/mmlt/LocalTimerMealyTestUtil.java new file mode 100644 index 0000000000..1360c54f41 --- /dev/null +++ b/algorithms/active/lstar/src/test/java/de/learnlib/algorithm/lstar/mmlt/LocalTimerMealyTestUtil.java @@ -0,0 +1,77 @@ +package de.learnlib.algorithm.lstar.mmlt; + +import de.learnlib.algorithm.LocalTimerMealyModelParams; +import de.learnlib.testsupport.example.mmlt.LocalTimerMealyModel; +import net.automatalib.automaton.time.impl.mmlt.StringSymbolCombiner; +import net.automatalib.automaton.time.mmlt.LocalTimerMealy; +import net.automatalib.serialization.dot.GraphDOT; +import net.automatalib.serialization.dot.LocalTimerMealyGraphvizParser; +import net.automatalib.util.automaton.mmlt.LocalTimerMealyUtil; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +/** + * Utility class for loading MMLTs from resources and printing them. + */ +public class LocalTimerMealyTestUtil { + + /** + * Prints the provided MMLT to stdout. + */ + static void printModel(LocalTimerMealy model) { + try { + GraphDOT.write(model.transitionGraphView(true, true), System.out); + } catch (IOException ignored) { + } + } + + /** + * Lists all MMLT models in the resources directory. + */ + static List listModelFiles() { + var models = new ArrayList(); + try { + var modelFiles = LocalTimerMealyTestUtil.class.getResource("/mmlt"); + if (modelFiles != null) { + try (Stream paths = Files.list(Paths.get(modelFiles.toURI()))) { + paths.filter(p -> p.toString().endsWith(".dot")) + .map(p -> p.getFileName().toString()) + .forEach(models::add); + } + } + } catch (IOException | URISyntaxException e) { + throw new RuntimeException("Failed to list model files", e); + } + return models; + } + + static LocalTimerMealyModel automatonFromFile(String name) { + return automatonFromFile(name, -1); + } + + /** + * Loads the automaton model with the provided resource name. + * + * @param name Resource name + * @param maxTimerQueryWaiting Maximum timer query waiting time. If set to -1, the maximum initial timer value is used. + * @return The automaton model. + */ + static LocalTimerMealyModel automatonFromFile(String name, int maxTimerQueryWaiting) { + var modelResource = LocalTimerMealyTestUtil.class.getResource("/mmlt/" + name); + var automaton = LocalTimerMealyGraphvizParser.parseLocalTimerMealy(new File(modelResource.getFile()), "void", StringSymbolCombiner.getInstance()); + + long maxTimeoutDelay = LocalTimerMealyUtil.getMaximumTimeoutDelay(automaton); + long maxTimerQueryWaitingFinal = (maxTimerQueryWaiting > 0) ? maxTimerQueryWaiting : LocalTimerMealyUtil.getMaximumInitialTimerValue(automaton) * 2; + + return new LocalTimerMealyModel<>(name, automaton, new LocalTimerMealyModelParams<>("void", maxTimeoutDelay, maxTimerQueryWaitingFinal, StringSymbolCombiner.getInstance())); + } + +} diff --git a/algorithms/active/lstar/src/test/resources/mmlt/ambiguous_minimal.dot b/algorithms/active/lstar/src/test/resources/mmlt/ambiguous_minimal.dot new file mode 100644 index 0000000000..115f68259a --- /dev/null +++ b/algorithms/active/lstar/src/test/resources/mmlt/ambiguous_minimal.dot @@ -0,0 +1,21 @@ +// In this example, the learner infers an equivalent MMLT +// that uses more timers per location but still has the same number of locations. +digraph g { + + s0 [shape="circle"]; + s1 [shape="circle" timers="a=2"]; + s2 [shape="circle" timers="b=1"]; + s3 [shape="circle"]; + s4 [shape="circle"]; + + s1 -> s2 [label="to[a] / A"]; + s2 -> s3 [label="to[b] / B"]; + + s0 -> s1 [label="y / X"]; + s0 -> s4 [label="x / Y"]; + s4 -> s2 [label="x / Z"]; + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s0; + +} diff --git a/algorithms/active/lstar/src/test/resources/mmlt/greedy_fail.dot b/algorithms/active/lstar/src/test/resources/mmlt/greedy_fail.dot new file mode 100644 index 0000000000..21d0cf0eaf --- /dev/null +++ b/algorithms/active/lstar/src/test/resources/mmlt/greedy_fail.dot @@ -0,0 +1,14 @@ +// This is an example of a model where the greedy timer inference leads to an unnecessarily large model. +digraph g { + + s0 [timers="a=3" shape="circle"]; + s1 [timers="a=2,b=5" shape="circle"]; + + s0 -> s1 [label="to[a] / a"]; + s1 -> s1 [label="to[a] / b"]; + s1 -> s1 [label="to[b] / c"]; + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s0; + +} diff --git a/algorithms/active/lstar/src/test/resources/mmlt/initial_value_one.dot b/algorithms/active/lstar/src/test/resources/mmlt/initial_value_one.dot new file mode 100644 index 0000000000..3e3a799324 --- /dev/null +++ b/algorithms/active/lstar/src/test/resources/mmlt/initial_value_one.dot @@ -0,0 +1,15 @@ +// The location s0 of this MMLT has a timer with the initial value one and a local reset. +// We allow local resets only in locations with at least two stable configurations. +// Hence, the smallest accurate hypothesis MMLT for this model must have two locations. +digraph g { + + s0 [timers="x=1,y=2" shape="circle"]; + + s0 -> s0 [label="a / void" resets="x,y"]; + s0 -> s0 [label="to[x] / X"]; + s0 -> s0 [label="to[y] / Y"]; + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s0; + +} diff --git a/algorithms/active/lstar/src/test/resources/mmlt/isolated_loc_permanent.dot b/algorithms/active/lstar/src/test/resources/mmlt/isolated_loc_permanent.dot new file mode 100644 index 0000000000..1a6e3e0821 --- /dev/null +++ b/algorithms/active/lstar/src/test/resources/mmlt/isolated_loc_permanent.dot @@ -0,0 +1,19 @@ +// Example of location that becomes permanently isolated +digraph g { + + s0 [timers="x=3" shape="circle"]; + s1 [timers="y=2, z=3" shape="circle"]; + s2 [shape="circle"]; + + s0 -> s0 [label="a / A"]; + s1 -> s1 [label="a / B"]; + s2 -> s2 [label="a / C"]; + + s0 -> s1 [label="to[x] / X"]; + s1 -> s1 [label="to[y] / Y"]; + s1 -> s2 [label="to[z] / Z"]; + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s0; + +} diff --git a/algorithms/active/lstar/src/test/resources/mmlt/isolated_loc_temp.dot b/algorithms/active/lstar/src/test/resources/mmlt/isolated_loc_temp.dot new file mode 100644 index 0000000000..0b6e851b2e --- /dev/null +++ b/algorithms/active/lstar/src/test/resources/mmlt/isolated_loc_temp.dot @@ -0,0 +1,27 @@ +// Example of location that becomes temporarily isolated +// Tested with a whitebox oracle only. +digraph g { + + s0 [timers="x=3" shape="circle"]; + s1 [timers="y=2, z=3" shape="circle"]; + s2 [shape="circle"]; + + s4 [shape="circle"]; + s5 [timers="v=1" shape="circle"]; + + s0 -> s0 [label="a / A"]; + s1 -> s1 [label="a / B"]; + s2 -> s4 [label="a / C"]; + s4 -> s5 [label="a / C"]; + + s0 -> s1 [label="to[x] / X"]; + s1 -> s1 [label="to[y] / Y"]; + s1 -> s2 [label="to[z] / Z"]; + + s5 -> s5 [label="a / B"] + s5 -> s2 [label="to[v] / Z"] + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s0; + +} diff --git a/algorithms/active/lstar/src/test/resources/mmlt/missing_oneshot_high_waiting.dot b/algorithms/active/lstar/src/test/resources/mmlt/missing_oneshot_high_waiting.dot new file mode 100644 index 0000000000..bde3ff6111 --- /dev/null +++ b/algorithms/active/lstar/src/test/resources/mmlt/missing_oneshot_high_waiting.dot @@ -0,0 +1,17 @@ +// This is an example of a model where we infer a periodic timer b that cannot be periodic, even with a high maximum waiting time.l +digraph g { + + s0 [timers="a=3" shape="circle"]; + s1 [timers="b=1" shape="circle"]; + s2 [timers="c=2" shape="circle"]; + + s0 -> s1 [label="to[a] / A"]; + s1 -> s2 [label="to[b] / B"]; + s2 -> s2 [label="to[c] / C"]; + + s1 -> s1 [label="x / X"]; + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s0; + +} diff --git a/algorithms/active/lstar/src/test/resources/mmlt/over_approx_reset.dot b/algorithms/active/lstar/src/test/resources/mmlt/over_approx_reset.dot new file mode 100644 index 0000000000..3e08b3fbe3 --- /dev/null +++ b/algorithms/active/lstar/src/test/resources/mmlt/over_approx_reset.dot @@ -0,0 +1,17 @@ +// Make sure to not initialize the suffixes with the input alphabet in this example +// In this example, the learner will infer a missing local reset, although there is actually a missing discriminator. +digraph g { + + s0 [timers="a=3" shape="circle"]; + s1 [timers="b=3" shape="circle"]; + + s0 -> s0 [label="to[a] / A"]; + s0 -> s1 [label="i / void"]; + + s1 -> s1 [label="to[b] / A"]; + s1 -> s1 [label="x / X"]; + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s0; + +} diff --git a/algorithms/active/lstar/src/test/resources/mmlt/recursive_decomp.dot b/algorithms/active/lstar/src/test/resources/mmlt/recursive_decomp.dot new file mode 100644 index 0000000000..67120a67d8 --- /dev/null +++ b/algorithms/active/lstar/src/test/resources/mmlt/recursive_decomp.dot @@ -0,0 +1,37 @@ +// This is an example of a model where the post-processing discovers an incorrect output while processing an incorrect target. +// You need to set maximum the query time to three. +// Use "TestDissExample" to send counterexamples that trigger the expected behavior. +digraph g { + + s0 [shape="circle"]; + s1 [timers="a=2,b=3" shape="circle"]; + s2 [shape="circle"]; + s3 [timers="c=2" shape="circle"]; + s4 [timers="d=1" shape="circle"]; + s5 [timers="e=1" shape="circle"]; + s6 [timers="f=2" shape="circle"]; + + s0 -> s0 [label="f / F"] + + s0 -> s1 [label="p / P"]; + s1 -> s1 [label="to[a] / A"]; + s1 -> s2 [label="to[b] / B"]; + + s1 -> s1 [label="f / G"] + s2 -> s2 [label="f / H"] + + // ---- + + s0 -> s3 [label="u / U"]; + s3 -> s4 [label="to[c] / A"]; + s4 -> s5 [label="to[d] / B"]; + s5 -> s6 [label="to[e] / A"]; + s6 -> s7 [label="to[f] / A|B"]; + + s3 -> s3 [label="f / G"] + + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s0; + +} diff --git a/algorithms/active/lstar/src/test/resources/mmlt/same_initial_value.dot b/algorithms/active/lstar/src/test/resources/mmlt/same_initial_value.dot new file mode 100644 index 0000000000..17ec7ede6f --- /dev/null +++ b/algorithms/active/lstar/src/test/resources/mmlt/same_initial_value.dot @@ -0,0 +1,13 @@ +// In this example, the learner will infer a timer with a combined output. +digraph g { + + s0 [timers="a=3,b=3,c=6" shape="circle"]; + + s0 -> s0 [label="to[a] / A"]; + s0 -> s0 [label="to[b] / C"]; + s0 -> s0 [label="to[c] / B"]; + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s0; + +} diff --git a/algorithms/active/lstar/src/test/resources/mmlt/syntax_demo.dot b/algorithms/active/lstar/src/test/resources/mmlt/syntax_demo.dot new file mode 100644 index 0000000000..2379c825b1 --- /dev/null +++ b/algorithms/active/lstar/src/test/resources/mmlt/syntax_demo.dot @@ -0,0 +1,20 @@ +// This file demonstrates the syntax for defining a custom MMLT +digraph g { + s0 [label="L0" timers="a=2"] + s1 [label="L1" timers="b=4,c=6"] + s2 [label="L2" timers="d=2,e=3"] + + s0 -> s1 [label="to[a] / A"] // one-shot with location change + s1 -> s1 [label="to[b] / B"] // periodic + s1 -> s1 [label="to[c] / C" resets="b,c"] // one-shot with loop + + s2 -> s2 [label="to[d] / D" resets="d"] // periodic with explicit resets + s2 -> s2 [label="to[e] / E"] // periodic + + s1 -> s2 [label="x / void"] + s1 -> s1 [label="y / Y" resets="b,c"] // loop with local reset + s2 -> s2 [label="y / D"] // loop without reset + + __start0 [label="" shape="none" width="0" height="0"]; + __start0 -> s0; +} \ No newline at end of file diff --git a/algorithms/active/lstar/src/test/resources/mmlt/unique_minimal.dot b/algorithms/active/lstar/src/test/resources/mmlt/unique_minimal.dot new file mode 100644 index 0000000000..20749808c4 --- /dev/null +++ b/algorithms/active/lstar/src/test/resources/mmlt/unique_minimal.dot @@ -0,0 +1,18 @@ +// This example demonstrates that there is no unique minimal form for an MMLT. +// Learning this model with maxQueryTime=5 yields a model where s1 has two timers and s0 has one. +// Setting maxQueryTime to 10 instead yields a model where the initial location has two timers and the following has one. +// The total number of locations, timers, max. timers per location, and average timers per location are identical. +digraph g { + + s0 [shape="circle" timers="a=5"]; + s1 [shape="circle" timers="b=3"]; + s2 [shape="circle" timers="c=2"]; + + s0 -> s1 [label="to[a] / A"]; + s1 -> s2 [label="to[b] / B"]; + s2 -> s3 [label="to[c] / C"]; + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s0; + +} diff --git a/api/src/main/java/de/learnlib/algorithm/LocalTimerMealyModelParams.java b/api/src/main/java/de/learnlib/algorithm/LocalTimerMealyModelParams.java new file mode 100644 index 0000000000..01884f8d3b --- /dev/null +++ b/api/src/main/java/de/learnlib/algorithm/LocalTimerMealyModelParams.java @@ -0,0 +1,89 @@ +package de.learnlib.algorithm; + +import net.automatalib.automaton.time.mmlt.AbstractSymbolCombiner; + +import java.util.Objects; + +/** + * Model-specific parameters for the MMLT-learner. + * These are used by various filters, oracles, and the MMLT simulator. + * + * @param Output symbol type + */ +public final class LocalTimerMealyModelParams { + private final O silentOutput; + private final AbstractSymbolCombiner outputCombiner; + private final long maxTimeoutWaitingTime; + private long maxTimerQueryWaitingTime; + + /** + * @param silentOutput Silent output symbol + * @param maxTimeoutWaitingTime Maximum time to wait for a timeout in any configuration. + * If no timeout is observed after this time, the learner assumes that no timers are active. + * Hence, if this value is set too low, the learner will miss timeouts. This usually results in an + * incomplete model but can also trigger exceptions due to unsatisfied assumptions. + * @param maxTimerQueryWaitingTime Maximum waiting time to wait when inferring timers for a location. + * This must be at least the max. time for a timeout. + * We recommend setting this value to at least twice the highest value of any timer + * in the SUL, if these values are known or can be estimated. + * This increases the likelihood of detecting + * non-periodic behavior during timer inference, and thus reduces + * the need for equivalence queries. + * @param outputCombiner Function for combining simultaneously occurring outputs of timers + */ + public LocalTimerMealyModelParams(O silentOutput, + long maxTimeoutWaitingTime, + long maxTimerQueryWaitingTime, + AbstractSymbolCombiner outputCombiner) { + this.silentOutput = silentOutput; + this.maxTimeoutWaitingTime = maxTimeoutWaitingTime; + this.maxTimerQueryWaitingTime = maxTimerQueryWaitingTime; + this.outputCombiner = outputCombiner; + } + + public O silentOutput() { + return silentOutput; + } + + public long maxTimeoutWaitingTime() { + return maxTimeoutWaitingTime; + } + + public long maxTimerQueryWaitingTime() { + return maxTimerQueryWaitingTime; + } + + public AbstractSymbolCombiner outputCombiner() { + return outputCombiner; + } + + public void setMaxTimerQueryWaitingTime(long maxTimerQueryWaitingTime) { + this.maxTimerQueryWaitingTime = maxTimerQueryWaitingTime; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (LocalTimerMealyModelParams) obj; + return Objects.equals(this.silentOutput, that.silentOutput) && + this.maxTimeoutWaitingTime == that.maxTimeoutWaitingTime && + this.maxTimerQueryWaitingTime == that.maxTimerQueryWaitingTime && + Objects.equals(this.outputCombiner, that.outputCombiner); + } + + @Override + public int hashCode() { + return Objects.hash(silentOutput, maxTimeoutWaitingTime, maxTimerQueryWaitingTime, outputCombiner); + } + + @Override + public String toString() { + return "LocalTimerMealyModelParams[" + + "silentOutput=" + silentOutput + ", " + + "maxTimeoutWaitingTime=" + maxTimeoutWaitingTime + ", " + + "maxTimerQueryWaitingTime=" + maxTimerQueryWaitingTime + ", " + + "outputCombiner=" + outputCombiner + ']'; + } + +} diff --git a/api/src/main/java/de/learnlib/oracle/AbstractTimedQueryOracle.java b/api/src/main/java/de/learnlib/oracle/AbstractTimedQueryOracle.java new file mode 100644 index 0000000000..f36a3fbcc2 --- /dev/null +++ b/api/src/main/java/de/learnlib/oracle/AbstractTimedQueryOracle.java @@ -0,0 +1,82 @@ +package de.learnlib.oracle; + +import de.learnlib.query.DefaultQuery; +import de.learnlib.query.Query; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealyOutputSymbol; +import net.automatalib.automaton.time.mmlt.MealyTimerInfo; +import net.automatalib.word.Word; + +import java.util.Collection; +import java.util.List; + +/** + * Type of oracle used by an MMLT learner. + *

+ * Like a traditional query oracle, this answers output queries. + * In addition, it infers timers by observing timeouts. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public abstract class AbstractTimedQueryOracle implements MembershipOracle.MealyMembershipOracle, LocalTimerMealyOutputSymbol> { + + /** + * Response for a timer query. + * + * @param aborted True if query was aborted due to missing timeout. + * @param timers Identified timers + * @param Untimed output suffix type + */ + public record TimerQueryResult(boolean aborted, List> timers) { + + } + + @Override + public void processQueries(Collection, Word>>> collection) { + for (var q : collection) { + DefaultQuery, Word>> query = new DefaultQuery<>(q.getPrefix(), q.getSuffix()); + this.querySuffixOutput(query); + q.answer(query.getOutput()); + } + } + + /** + * Observes and aggregates any timeouts that occur after providing the given input to the SUL. + * Stops when observing inconsistent behavior. + * + * @param prefix Input to give to the SUL. + * @param maxTotalWaitingTime Maximum total time that is waited for timeouts. + * @return Observed timeouts. Empty, if none. + */ + public abstract TimerQueryResult queryTimers(Word> prefix, long maxTotalWaitingTime); + + /** + * Queries the suffix output for the provided query. + * + * @param query Input query. + */ + public void querySuffixOutput(DefaultQuery, Word>> query) { + this.querySuffixOutputInternal(query); + } + + /** + * Like querySuffixOutput but does not require a query object. + * + * @param prefix Prefix + * @param suffix Suffix + */ + public final Word> querySuffixOutput(Word> prefix, Word> suffix) { + DefaultQuery, Word>> query = new DefaultQuery<>(prefix, suffix); + this.querySuffixOutputInternal(query); + return query.getOutput(); + } + + /** + * Queries the output for the provided input sequence. + * + * @param query Input query. + */ + protected abstract void querySuffixOutputInternal(DefaultQuery, Word>> query); + +} diff --git a/api/src/main/java/de/learnlib/oracle/EquivalenceOracle.java b/api/src/main/java/de/learnlib/oracle/EquivalenceOracle.java index 065c2c7474..31acb80bd8 100644 --- a/api/src/main/java/de/learnlib/oracle/EquivalenceOracle.java +++ b/api/src/main/java/de/learnlib/oracle/EquivalenceOracle.java @@ -18,7 +18,10 @@ import java.util.Collection; import de.learnlib.query.DefaultQuery; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealyOutputSymbol; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; import net.automatalib.automaton.fsa.DFA; +import net.automatalib.automaton.time.mmlt.LocalTimerMealy; import net.automatalib.automaton.transducer.MealyMachine; import net.automatalib.automaton.transducer.MooreMachine; import net.automatalib.word.Word; @@ -88,4 +91,11 @@ interface MealyEquivalenceOracle extends EquivalenceOracle extends EquivalenceOracle, I, Word> {} + /** + * A specialization of the {@link EquivalenceOracle} interface for a Local Timer Mealy learning scenario. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ + interface LocalTimerMealyEquivalenceOracle extends EquivalenceOracle, LocalTimerMealySemanticInputSymbol, Word>>{} } diff --git a/api/src/main/java/de/learnlib/statistic/container/DummyStatsContainer.java b/api/src/main/java/de/learnlib/statistic/container/DummyStatsContainer.java new file mode 100644 index 0000000000..2066a75033 --- /dev/null +++ b/api/src/main/java/de/learnlib/statistic/container/DummyStatsContainer.java @@ -0,0 +1,66 @@ +package de.learnlib.statistic.container; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.time.Duration; +import java.util.Optional; + +/** + * A dummy implementation of {@link StatsContainer} that does nothing. + */ +public class DummyStatsContainer implements StatsContainer { + @Override + public void addTextInfo(String id, @Nullable String description, String text) { + + } + + @Override + public Optional getTextValue(String id) { + return Optional.empty(); + } + + @Override + public void setFlag(String id, @Nullable String description, boolean value) { + + } + + @Override + public Optional getFlagValue(String id) { + return Optional.empty(); + } + + @Override + public void startOrResumeClock(String id, @Nullable String description) { + + } + + @Override + public void pauseClock(String id) { + + } + + @Override + public Optional getClockValue(String id) { + return Optional.empty(); + } + + @Override + public void increaseCounter(String id, @Nullable String description, long increment) { + + } + + @Override + public void setCounter(String id, @Nullable String description, long count) { + + } + + @Override + public Optional getCount(String id) { + return Optional.empty(); + } + + @Override + public void printStats() { + System.out.println("Dummy container"); + } +} diff --git a/api/src/main/java/de/learnlib/statistic/container/LearnerStatsProvider.java b/api/src/main/java/de/learnlib/statistic/container/LearnerStatsProvider.java new file mode 100644 index 0000000000..79ee95c395 --- /dev/null +++ b/api/src/main/java/de/learnlib/statistic/container/LearnerStatsProvider.java @@ -0,0 +1,13 @@ +package de.learnlib.statistic.container; + +/** + * Interface for a component that is interested in storing statistics in a container. + */ +public interface LearnerStatsProvider { + /** + * Provides a container for storing statistics. + * + * @param container Stats container. + */ + void setStatsContainer(StatsContainer container); +} diff --git a/api/src/main/java/de/learnlib/statistic/container/StatsContainer.java b/api/src/main/java/de/learnlib/statistic/container/StatsContainer.java new file mode 100644 index 0000000000..90b5d3cbb0 --- /dev/null +++ b/api/src/main/java/de/learnlib/statistic/container/StatsContainer.java @@ -0,0 +1,129 @@ +package de.learnlib.statistic.container; + + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.time.Duration; +import java.util.Optional; + +/** + * Interface for a container that stores various statistics during learning. + */ +public interface StatsContainer { + + // Generic text + + /** + * Stores the provided text for the given id + * and assigns the provided description. + * + * @param id Text id + * @param description Description of the data, e.g., "configuration" + * @param text The text to be stored + */ + void addTextInfo(String id, @Nullable String description, String text); + + /** + * Retrieves the text with the provided id. + * + * @param id Id of the text value + * @return The stored text, or empty if there is no text with this id. + */ + Optional getTextValue(String id); + + + // Boolean flags + + /** + * Stores the provided boolean for the given id + * and optionally assigns the provided description. + * + * @param id Flag id + * @param description Description of the boolean, e.g., "accurate" + * @param value The boolean value to be stored + */ + void setFlag(String id, @Nullable String description, boolean value); + + /** + * Retrieves the flag with the provided id. + * + * @param id Id of the boolean value + * @return The stored boolean, or empty, if no boolean with this id exists. + */ + Optional getFlagValue(String id); + + + // Time + + /** + * Starts the clock with the given id and optionally assigns the provided description. + * If there is already a clock with this id, it is resumed. + * + * @param id Clock id + * @param description Description of the clock, e.g., "learning time" + */ + void startOrResumeClock(String id, @Nullable String description); + + /** + * Pauses the clock with the given id. If there is no clock with this id, nothing happens. + * + * @param id Clock id + */ + void pauseClock(String id); + + /** + * Returns the current value of the clock with the given id. + * + * @param id Clock id + * @return The current value of the clock, or empty, if no clock with this id exists. + */ + Optional getClockValue(String id); + + + // Counter + + /** + * Increases the counter with the given id and optionally assigns the provided description. + * If no counter with this id exists, it is created. + * + * @param id Counter id + * @param description Description of the counter, e.g., "number of rounds" + */ + default void increaseCounter(String id, @Nullable String description) { + increaseCounter(id, description, 1); + } + + /** + * Increases the counter with the given id by the provided increment + * and optionally assigns the provided description. + * If no counter with this id exists, it is created and initialized with the provided increment. + * + * @param id Counter id + * @param description Description of the counter, e.g., "number of rounds" + * @param increment Amount to increase the counter by + */ + void increaseCounter(String id, @Nullable String description, long increment); + + /** + * Sets the counter with the given id to the provided value and optionally assigns the provided description. + * If no counter with this id exists, it is created. + * + * @param id Counter id + * @param description Description of the counter, e.g., "number of rounds" + * @param count New value for the counter. Must be greater than zero. + */ + void setCounter(String id, @Nullable String description, long count); + + /** + * Gets the value of the counter with the given id. Returns empty, if no counter with this id exists. + * + * @param id Counter id + * @return The value of the counter, or empty, if no counter with this id exists. + */ + Optional getCount(String id); + + /** + * Prints all stored statistics. + */ + void printStats(); +} diff --git a/api/src/main/java/de/learnlib/sul/LocalTimerMealySUL.java b/api/src/main/java/de/learnlib/sul/LocalTimerMealySUL.java new file mode 100644 index 0000000000..8ab564a7dc --- /dev/null +++ b/api/src/main/java/de/learnlib/sul/LocalTimerMealySUL.java @@ -0,0 +1,118 @@ +package de.learnlib.sul; + +import net.automatalib.alphabet.time.mmlt.*; +import net.automatalib.word.Word; +import net.automatalib.word.WordBuilder; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Interface for a SUL with MMLT semantics. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public interface LocalTimerMealySUL { + + /** + * Follows the provided input word, starting at the current system state. + * The input word must not contain timeout symbols. Otherwise, an error occurs. + * + * @param input Input suffix. + */ + default void follow(Word> input) { + this.follow(input, -1); + } + + /** + * Follows the provided input word, starting at the current configuration. + * + * @param input Input suffix. + * @param maxTimeout Max. waiting time to use for timeoutSymbols. + */ + default void follow(Word> input, long maxTimeout) { + for (var s : input) { + if (s instanceof NonDelayingInput ndi) { + this.step(ndi); + } else if (s instanceof TimeStepSequence) { + this.collectTimeouts((TimeStepSequence) s); + } else if (s instanceof TimeoutSymbol) { + if (maxTimeout <= 0) { + throw new IllegalArgumentException("Must supply timeout when using timeout symbols."); + } + this.timeoutStep(maxTimeout); + } else { + throw new IllegalArgumentException("Unknown suffix type."); + } + } + } + + /** + * Provides a non-delaying input to the SUL and returns the observed output. + * + * @param input Input + * @return SUL output. + */ + LocalTimerMealyOutputSymbol step(NonDelayingInput input); + + /** + * Waits until a timeout occurs or the provided time is reached. + *

+ * We may observe no timeout if either the waiting time is too small or if the active location + * has no timers. + * + * @param maxTime Maximum waiting time. + * @return Observed timer output with waiting time, or null, if no timeout was observed. + */ + @Nullable + LocalTimerMealyOutputSymbol timeoutStep(long maxTime); + + /** + * Waits for one time unit and returns the observed output. + * + * @return Null if no output occurred, timer output if at least one timer expired. + * The delay of this output is set to zero. + */ + @Nullable + default LocalTimerMealyOutputSymbol timeStep() { + var res = this.timeoutStep(1); + if (res != null) { + return new LocalTimerMealyOutputSymbol<>(res.getSymbol()); + } + return null; + } + + /** + * Waits for the specified time and returns all observed timeouts. + * + * @param input Waiting time. + * @return Observed timeouts. Empty, if none. + */ + default Word> collectTimeouts(TimeStepSequence input) { + WordBuilder> wbOutput = new WordBuilder<>(); + + long remainingTime = input.getTimeSteps(); + while (remainingTime > 0) { + LocalTimerMealyOutputSymbol nextTimeout = this.timeoutStep(remainingTime); + if (nextTimeout == null) { + // No timer will expire during remaining waiting time: + break; + } else { + wbOutput.append(nextTimeout); + remainingTime -= nextTimeout.getDelay(); + } + } + + return wbOutput.toWord(); + } + + + /** + * Prepares the SUL for a new query. + */ + void pre(); + + /** + * Deinitializes the SUL. + */ + void post(); +} diff --git a/api/src/main/java/de/learnlib/symbol_filter/SymbolFilter.java b/api/src/main/java/de/learnlib/symbol_filter/SymbolFilter.java new file mode 100644 index 0000000000..9af1f079d1 --- /dev/null +++ b/api/src/main/java/de/learnlib/symbol_filter/SymbolFilter.java @@ -0,0 +1,40 @@ +package de.learnlib.symbol_filter; + +import net.automatalib.word.Word; + +/** + * Interface for a symbol filter. + * A symbol filter predicts whether a given transition is ignorable in a given state. + * This information can be used to avoid redundant queries. + * A symbol filter may answer incorrectly. + * + * @param Type for symbols in the prefix of the considered states + * @param Type of the queried symbols + */ +public interface SymbolFilter { + + /** + * Predicts whether the provided symbol is ignorable in the state + * that is addressed by the given prefix. + *

+ * ignorable typically means that the symbol triggers a silent self-loop in the considered state. + * However, the semantics may vary depending on the concrete implementation. + *

+ * Predictions may not be correct, i.e., an accepted symbol may be actually ignorable and an ignored symbol + * may be actually not ignorable. + * + * @param prefix State prefix. + * @param symbol Input of the queried transition. + * @return IGNORE if the symbol is considered ignorable, ACCEPT if it is not. + */ + SymbolFilterResponse query(Word prefix, V symbol); + + /** + * Sets the response of the filter for the given transition to the provided response. + * + * @param prefix State prefix. + * @param symbol Input of the transition that should be updated. + * @param response New response + */ + void update(Word prefix, V symbol, SymbolFilterResponse response); +} diff --git a/api/src/main/java/de/learnlib/symbol_filter/SymbolFilterResponse.java b/api/src/main/java/de/learnlib/symbol_filter/SymbolFilterResponse.java new file mode 100644 index 0000000000..e0a5dd6936 --- /dev/null +++ b/api/src/main/java/de/learnlib/symbol_filter/SymbolFilterResponse.java @@ -0,0 +1,6 @@ +package de.learnlib.symbol_filter; + +public enum SymbolFilterResponse { + ACCEPT, + IGNORE +} diff --git a/api/src/main/java/module-info.java b/api/src/main/java/module-info.java index e62d48409f..fd3814f990 100644 --- a/api/src/main/java/module-info.java +++ b/api/src/main/java/module-info.java @@ -43,4 +43,6 @@ exports de.learnlib.query; exports de.learnlib.statistic; exports de.learnlib.sul; + exports de.learnlib.statistic.container; + exports de.learnlib.symbol_filter; } diff --git a/commons/datastructures/src/main/java/de/learnlib/datastructure/observationtable/RowImpl.java b/commons/datastructures/src/main/java/de/learnlib/datastructure/observationtable/RowImpl.java index cdee25aa0c..b75ec76e75 100644 --- a/commons/datastructures/src/main/java/de/learnlib/datastructure/observationtable/RowImpl.java +++ b/commons/datastructures/src/main/java/de/learnlib/datastructure/observationtable/RowImpl.java @@ -18,7 +18,7 @@ import net.automatalib.common.util.array.ArrayStorage; import net.automatalib.word.Word; -final class RowImpl implements Row { +public final class RowImpl implements Row { private final Word label; private final int rowId; @@ -37,7 +37,7 @@ final class RowImpl implements Row { * @param alphabetSize * the size of the alphabet, used for initializing the successor array */ - RowImpl(Word label, int rowId, int alphabetSize) { + public RowImpl(Word label, int rowId, int alphabetSize) { this(label, rowId); makeShort(alphabetSize); @@ -51,7 +51,7 @@ final class RowImpl implements Row { * @param rowId * the unique row identifier */ - RowImpl(Word label, int rowId) { + public RowImpl(Word label, int rowId) { this.label = label; this.rowId = rowId; } @@ -63,7 +63,7 @@ final class RowImpl implements Row { * @param initialAlphabetSize * the size of the input alphabet. */ - void makeShort(int initialAlphabetSize) { + public void makeShort(int initialAlphabetSize) { if (lpIndex == -1) { return; } @@ -85,7 +85,7 @@ public RowImpl getSuccessor(int inputIdx) { * @param succ * the successor row */ - void setSuccessor(int inputIdx, RowImpl succ) { + public void setSuccessor(int inputIdx, RowImpl succ) { successors.set(inputIdx, succ); } @@ -110,7 +110,7 @@ public int getRowContentId() { * @param id * the contents id */ - void setRowContentId(int id) { + public void setRowContentId(int id) { this.rowContentId = id; } @@ -127,11 +127,11 @@ int getLpIndex() { return lpIndex; } - void setLpIndex(int lpIndex) { + public void setLpIndex(int lpIndex) { this.lpIndex = lpIndex; } - void ensureInputCapacity(int capacity) { + public void ensureInputCapacity(int capacity) { this.successors.ensureCapacity(capacity); } } diff --git a/commons/datastructures/src/main/java/module-info.java b/commons/datastructures/src/main/java/module-info.java index e9ccbfa9b9..5b1d9590d5 100644 --- a/commons/datastructures/src/main/java/module-info.java +++ b/commons/datastructures/src/main/java/module-info.java @@ -36,6 +36,7 @@ // annotations are 'provided'-scoped and do not need to be loaded at runtime requires static org.checkerframework.checker.qual; + requires org.slf4j; exports de.learnlib.datastructure.discriminationtree; exports de.learnlib.datastructure.discriminationtree.iterators; diff --git a/commons/util/src/main/java/de/learnlib/util/statistic/container/CounterStatistic.java b/commons/util/src/main/java/de/learnlib/util/statistic/container/CounterStatistic.java new file mode 100644 index 0000000000..c14a33bc36 --- /dev/null +++ b/commons/util/src/main/java/de/learnlib/util/statistic/container/CounterStatistic.java @@ -0,0 +1,32 @@ +package de.learnlib.util.statistic.container; + +/** + * A counter that can be increased and set to a particular positive number. + */ +class CounterStatistic extends LearnerStatistic { + private long count; + + public CounterStatistic(String id, String description) { + this(id, description, 0); + } + + public CounterStatistic(String id, String description, long count) { + super(id, description); + this.count = count; + } + + public void setCount(long count) { + if (count < 0) { + throw new IllegalArgumentException(); + } + this.count = count; + } + + public void increase(long increment) { + this.count += increment; + } + + public long getCount() { + return count; + } +} diff --git a/commons/util/src/main/java/de/learnlib/util/statistic/container/FlagStatistic.java b/commons/util/src/main/java/de/learnlib/util/statistic/container/FlagStatistic.java new file mode 100644 index 0000000000..5bb04bcec5 --- /dev/null +++ b/commons/util/src/main/java/de/learnlib/util/statistic/container/FlagStatistic.java @@ -0,0 +1,23 @@ +package de.learnlib.util.statistic.container; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A boolean flag that is unset by default and can be set. + */ +class FlagStatistic extends LearnerStatistic { + private boolean flagged; + + public FlagStatistic(String id, @Nullable String description, boolean value) { + super(id, description); + this.flagged = value; + } + + public void setFlag(boolean value) { + this.flagged = value; + } + + public boolean isFlagged() { + return flagged; + } +} diff --git a/commons/util/src/main/java/de/learnlib/util/statistic/container/LearnerStatistic.java b/commons/util/src/main/java/de/learnlib/util/statistic/container/LearnerStatistic.java new file mode 100644 index 0000000000..efba05c487 --- /dev/null +++ b/commons/util/src/main/java/de/learnlib/util/statistic/container/LearnerStatistic.java @@ -0,0 +1,36 @@ +package de.learnlib.util.statistic.container; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Various types of statistical data to be stored in a StatsContainer. + */ +abstract class LearnerStatistic { + private final String id; + private final String description; + + /** + * Creates a new LearnerStatistic. + * + * @param id Unique id of the statistic. Must be unique within the StatsContainer. + * @param description Optional description of the statistic. If no description is provided, the id is used. + */ + public LearnerStatistic(String id, @Nullable String description) { + this.id = id; + + if (description == null) { + this.description = id; + } else { + this.description = description; + } + } + + public String getId() { + return id; + } + + public String getDescription() { + return description; + } + +} diff --git a/commons/util/src/main/java/de/learnlib/util/statistic/container/MapStatsContainer.java b/commons/util/src/main/java/de/learnlib/util/statistic/container/MapStatsContainer.java new file mode 100644 index 0000000000..9fce4f4e80 --- /dev/null +++ b/commons/util/src/main/java/de/learnlib/util/statistic/container/MapStatsContainer.java @@ -0,0 +1,159 @@ +package de.learnlib.util.statistic.container; + +import de.learnlib.statistic.container.StatsContainer; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.time.Duration; +import java.util.*; + +/** + * A {@link StatsContainer} that stores all statistics in a {@link Map}. + */ +public class MapStatsContainer implements StatsContainer { + private final Map statistics = new HashMap<>(); // id -> stat + + @Override + public void addTextInfo(String id, @Nullable String description, String text) { + statistics.put(id, new TextStatistic(id, description, text)); + } + + @Override + public Optional getTextValue(String id) { + var value = statistics.get(id); + if (value instanceof TextStatistic textStatistic) { + return Optional.of(textStatistic.getText()); + } + return Optional.empty(); + } + + @Override + public void setFlag(String id, @Nullable String description, boolean value) { + statistics.put(id, new FlagStatistic(id, description, value)); + } + + @Override + public Optional getFlagValue(String id) { + var value = statistics.get(id); + if (value instanceof FlagStatistic flagStatistic) { + return Optional.of(flagStatistic.isFlagged()); + } + return Optional.empty(); + } + + @Override + public void startOrResumeClock(String id, @Nullable String description) { + var value = statistics.get(id); + if (value instanceof StopClockStatistic clockStatistic) { + clockStatistic.resume(); + } else { + // Create and start a new clock: + var newClock = new StopClockStatistic(id, description); + statistics.put(id, newClock); + newClock.resume(); + } + } + + @Override + public void pauseClock(String id) { + var value = statistics.get(id); + if (value instanceof StopClockStatistic clockStatistic) { + clockStatistic.pause(); + } + } + + @Override + public Optional getClockValue(String id) { + var value = statistics.get(id); + if (value instanceof StopClockStatistic clockStatistic) { + return Optional.of(clockStatistic.getElapsed()); + } + return Optional.empty(); + } + + @Override + public void increaseCounter(String id, @Nullable String description, long increment) { + var value = statistics.get(id); + if (value instanceof CounterStatistic counterStatistic) { + counterStatistic.increase(increment); + } else { + // Create a new counter: + setCounter(id, description, increment); + } + } + + @Override + public void setCounter(String id, @Nullable String description, long count) { + statistics.put(id, new CounterStatistic(id, description, count)); + } + + @Override + public Optional getCount(String id) { + var value = statistics.get(id); + if (value instanceof CounterStatistic counterStatistic) { + return Optional.of(counterStatistic.getCount()); + } + return Optional.empty(); + } + + // ================== + + public String toJson() { + List sortedStats = statistics.values().stream().sorted(Comparator.comparing(LearnerStatistic::getDescription)).toList(); + + List lines = new ArrayList<>(); + for (var stat : sortedStats) { + if (stat instanceof StopClockStatistic sc) { + lines.add(String.format("\"%s [ms]\": %d", stat.getDescription(), sc.getElapsed().toMillis())); + } else if (stat instanceof CounterStatistic c) { + lines.add(String.format("\"%s\": %d", stat.getDescription(), c.getCount())); + } else if (stat instanceof FlagStatistic f) { + lines.add(String.format("\"%s\": %s", stat.getDescription(), f.isFlagged())); + } else if (stat instanceof TextStatistic t) { + lines.add(String.format("\"%s\": \"%s\"", stat.getDescription(), t.getText())); + } + } + + if (lines.isEmpty()) { + return "{}"; + } + + return "{" + String.join(",\n", lines) + "}"; + } + + + public String toYaml() { + List sortedStats = statistics.values().stream().sorted(Comparator.comparing(LearnerStatistic::getDescription)).toList(); + + List lines = new ArrayList<>(); + for (var stat : sortedStats) { + if (stat instanceof StopClockStatistic sc) { + lines.add(String.format(" \"%s [ms]\": %d", stat.getDescription(), sc.getElapsed().toMillis())); + } else if (stat instanceof CounterStatistic c) { + lines.add(String.format(" \"%s\": %d", stat.getDescription(), c.getCount())); + } else if (stat instanceof FlagStatistic f) { + lines.add(String.format(" \"%s\": %s", stat.getDescription(), f.isFlagged())); + } else if (stat instanceof TextStatistic t) { + lines.add(String.format(" \"%s\": \"%s\"", stat.getDescription(), t.getText())); + } + } + + if (lines.isEmpty()) { + return null; + } + + // Add dash to first line: + String newFirstLine = " - " + lines.get(0).stripLeading(); + lines.set(0, newFirstLine); + + return String.join("\n", lines); + } + + + public void printStats() { + // Print results: + System.out.println("============================================"); + System.out.println("Statistics:"); + System.out.println(this.toYaml()); + System.out.println("============================================"); + } +} diff --git a/commons/util/src/main/java/de/learnlib/util/statistic/container/StopClockStatistic.java b/commons/util/src/main/java/de/learnlib/util/statistic/container/StopClockStatistic.java new file mode 100644 index 0000000000..9adf3c797e --- /dev/null +++ b/commons/util/src/main/java/de/learnlib/util/statistic/container/StopClockStatistic.java @@ -0,0 +1,36 @@ +package de.learnlib.util.statistic.container; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.time.Duration; +import java.time.Instant; + +/** + * A stop clock that can be paused and resumed. + */ +class StopClockStatistic extends LearnerStatistic { + private Instant started; + private Duration elapsed; + + public StopClockStatistic(String id, @Nullable String description) { + super(id, description); + this.elapsed = Duration.ZERO; + this.started = null; + } + + public void resume() { + this.started = Instant.now(); + } + + public void pause() { + if (started == null) { + return; + } + this.elapsed = this.elapsed.plus(Duration.between(started, Instant.now())); + this.started = null; + } + + public Duration getElapsed() { + return this.elapsed; + } +} diff --git a/commons/util/src/main/java/de/learnlib/util/statistic/container/TextStatistic.java b/commons/util/src/main/java/de/learnlib/util/statistic/container/TextStatistic.java new file mode 100644 index 0000000000..5d9e81aafb --- /dev/null +++ b/commons/util/src/main/java/de/learnlib/util/statistic/container/TextStatistic.java @@ -0,0 +1,16 @@ +package de.learnlib.util.statistic.container; + +import org.checkerframework.checker.nullness.qual.Nullable; + +class TextStatistic extends LearnerStatistic { + private final String text; + + public TextStatistic(String id, @Nullable String description, String text) { + super(id, description); + this.text = text; + } + + public String getText() { + return text; + } +} diff --git a/commons/util/src/main/java/module-info.java b/commons/util/src/main/java/module-info.java index e4b6a888c4..0c15ae9d7f 100644 --- a/commons/util/src/main/java/module-info.java +++ b/commons/util/src/main/java/module-info.java @@ -43,4 +43,5 @@ exports de.learnlib.util.moore; exports de.learnlib.util.nfa; exports de.learnlib.util.statistic; + exports de.learnlib.util.statistic.container; } diff --git a/drivers/simulator/src/main/java/de/learnlib/driver/simulator/LocalTimerMealySimulatorSUL.java b/drivers/simulator/src/main/java/de/learnlib/driver/simulator/LocalTimerMealySimulatorSUL.java new file mode 100644 index 0000000000..8b2543947b --- /dev/null +++ b/drivers/simulator/src/main/java/de/learnlib/driver/simulator/LocalTimerMealySimulatorSUL.java @@ -0,0 +1,71 @@ +package de.learnlib.driver.simulator; + +import de.learnlib.sul.LocalTimerMealySUL; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealyOutputSymbol; +import net.automatalib.alphabet.time.mmlt.NonDelayingInput; +import net.automatalib.alphabet.time.mmlt.TimeoutSymbol; +import net.automatalib.automaton.time.mmlt.LocalTimerMealy; +import net.automatalib.automaton.time.mmlt.semantics.LocalTimerMealyConfiguration; +import org.checkerframework.checker.nullness.qual.Nullable; + + +/** + * Simulates the extended semantics of an MMLT. + * + * @param Location type. + * @param Non-delaying input type. + * @param Output symbol type. + */ +public class LocalTimerMealySimulatorSUL implements LocalTimerMealySUL { + + private final LocalTimerMealy automaton; + + private LocalTimerMealyConfiguration currentConfiguration; + + + public LocalTimerMealySimulatorSUL(LocalTimerMealy automaton) { + this.automaton = automaton; + this.currentConfiguration = null; + } + + + @Override + public LocalTimerMealyOutputSymbol step(NonDelayingInput input) { + if (this.currentConfiguration == null) { + throw new IllegalStateException("Not initialized!"); + } + + var trans = this.automaton.getSemantics().getTransition(this.currentConfiguration, input); + this.currentConfiguration = trans.target(); + return trans.output(); + } + + @Override + public @Nullable LocalTimerMealyOutputSymbol timeoutStep(long maxTime) { + if (this.currentConfiguration == null) { + throw new IllegalStateException("Not initialized!"); + } + + var trans = this.automaton.getSemantics().getTransition(this.currentConfiguration, new TimeoutSymbol<>(), maxTime); + this.currentConfiguration = trans.target(); + + if (trans.output().equals(automaton.getSemantics().getSilentOutput())) { + // No timeout observed: + return null; + } else { + return trans.output(); + } + } + + @Override + public void pre() { + this.currentConfiguration = automaton.getSemantics().getInitialConfiguration().copy(); + } + + @Override + public void post() { + this.currentConfiguration = null; + } + + +} diff --git a/examples/pom.xml b/examples/pom.xml index b69ee71f87..cfe2ac6224 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -178,6 +178,10 @@ limitations under the License. org.mockito mockito-core + + de.learnlib + learnlib-symbol-filters + diff --git a/examples/src/main/java/de/learnlib/example/mmlt/Example1.java b/examples/src/main/java/de/learnlib/example/mmlt/Example1.java new file mode 100644 index 0000000000..3613d6860f --- /dev/null +++ b/examples/src/main/java/de/learnlib/example/mmlt/Example1.java @@ -0,0 +1,142 @@ +package de.learnlib.example.mmlt; + +import de.learnlib.algorithm.lstar.mmlt.LStarLocalTimerMealy; +import de.learnlib.datastructure.observationtable.writer.ObservationTableASCIIWriter; +import de.learnlib.driver.simulator.LocalTimerMealySimulatorSUL; +import de.learnlib.filter.cache.mmlt.LocalTimerMealyTreeSULCache; +import de.learnlib.filter.cache.mmlt.TimeoutReducerSUL; +import de.learnlib.filter.statistic.sul.LocalTimerMealyStatsSUL; +import de.learnlib.oracle.EquivalenceOracle; +import de.learnlib.oracle.equivalence.mmlt.LocalTimerMealyEQOracleChain; +import de.learnlib.oracle.equivalence.mmlt.LocalTimerMealyRandomWpOracle; +import de.learnlib.oracle.equivalence.mmlt.LocalTimerMealySimulatorOracle; +import de.learnlib.oracle.equivalence.mmlt.ResetSearchOracle; +import de.learnlib.oracle.membership.TimedQueryOracle; +import de.learnlib.oracle.symbol_filters.CachedSymbolFilter; +import de.learnlib.oracle.symbol_filters.mmlt.LocalTimerMealyRandomSymbolFilter; +import de.learnlib.oracle.symbol_filters.mmlt.LocalTimerMealyStatisticsSymbolFilter; +import de.learnlib.query.DefaultQuery; +import de.learnlib.statistic.container.StatsContainer; +import de.learnlib.symbol_filter.SymbolFilter; +import de.learnlib.testsupport.example.mmlt.LocalTimerMealyExamples; +import de.learnlib.util.statistic.container.MapStatsContainer; +import net.automatalib.alphabet.impl.GrowingMapAlphabet; +import net.automatalib.alphabet.time.mmlt.*; +import net.automatalib.serialization.dot.GraphDOT; +import net.automatalib.word.Word; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * This example shows how to learn a Mealy machine with local timers, + * an automaton model for real-time systems. + */ +public class Example1 { + + public static void main(String[] args) { + var model = LocalTimerMealyExamples.SensorCollector(); + + // We first create a statistics container. + // This container will store various statistical data during learning: + var stats = new MapStatsContainer(); + stats.addTextInfo("LocalTimerMealyModel", null, model.name()); + stats.setCounter("original_locs", "Locations in original", model.automaton().getStates().size()); + stats.setCounter("original_inputs", "Untimed alphabet size in original", model.automaton().getUntimedAlphabet().size()); + + // ====================== + // Set up the pipeline: + GrowingMapAlphabet> alphabet = new GrowingMapAlphabet<>(); + alphabet.addAll(model.automaton().getUntimedAlphabet()); + + // We use a simulator SUL to simulate our automaton: + var sul = new LocalTimerMealySimulatorSUL<>(model.automaton()); + + // We count all operations that are performed on the SUL with a stats-SUL: + var statsAfterCache = new LocalTimerMealyStatsSUL<>(sul, stats); + + // We use a cache to avoid redundant operations: + var cacheSUL = new LocalTimerMealyTreeSULCache<>(statsAfterCache, model.params()); + cacheSUL.setStatsContainer(stats); + var toReducerSul = new TimeoutReducerSUL<>(cacheSUL, model.params().maxTimeoutWaitingTime(), stats); + + // We use a query oracle to answer queries from the learner: + var timeOracle = new TimedQueryOracle<>(toReducerSul, model.params()); + + // We use a chain of different equivalence oracles: + LocalTimerMealyEQOracleChain chainOracle = new LocalTimerMealyEQOracleChain<>(); + chainOracle.addOracle(cacheSUL.createCacheConsistencyTest()); + chainOracle.addOracle(new ResetSearchOracle<>(timeOracle, 100, 1.0, 1.0)); + chainOracle.addOracle(new LocalTimerMealyRandomWpOracle<>(timeOracle, 100, 16, 0, 100)); + chainOracle.addOracle(new LocalTimerMealySimulatorOracle<>(model.automaton())); // ensure that we eventually find an accurate model + chainOracle.setStatsContainer(stats); + + // Set up our L* learner: + List>> suffixes = new ArrayList<>(); + alphabet.forEach(s -> suffixes.add(Word.fromLetter(s))); + suffixes.add(Word.fromLetter(new TimeoutSymbol<>())); + + // A symbol filter allows us to reduce queries by exploiting prior knowledge. + // For this example, we use a RandomSymbolFilter. This filter correctly predicts + // whether a transition silently self-loops with an accuracy of 90%: + SymbolFilter, NonDelayingInput> filter = + new LocalTimerMealyRandomSymbolFilter<>(model.automaton(), 0.1, new Random(100)); + + filter = new LocalTimerMealyStatisticsSymbolFilter<>(model.automaton(), filter, stats); + filter = new CachedSymbolFilter<>(filter); // need to wrap to enable updates to responses + + var learner = new LStarLocalTimerMealy<>(alphabet, model.params(), suffixes, timeOracle, filter); + learner.setStatsContainer(stats); + + // Start learning: + runExperiment(learner, chainOracle, stats, 100); + + // Troubleshooting + // If you attempt to learn a model of some application and the learner + // throws assertion errors or illegal state exceptions, + // your SUL likely has no MMLT semantics. + // In this case, you can try to learn a partial model by excluding TimeStepSymbol + // from the input alphabet for the counterexample search: + // Replace tester.findCounterExample(hyp, hyp.getSemantics().getInputAlphabet()); + // with: tester.findCounterExample(hyp, hyp.getSemantics().getInputAlphabet().stream().filter(s -> !(s instanceof TimeStepSymbol)).toList()); + } + + private static void runExperiment(LStarLocalTimerMealy learner, + EquivalenceOracle.LocalTimerMealyEquivalenceOracle tester, + StatsContainer stats, int maxRounds) { + stats.startOrResumeClock("learningRt", "Processing time"); + learner.startLearning(); + + var hyp = learner.getHypothesisModel(); + DefaultQuery, Word>> cex = tester.findCounterExample(hyp, hyp.getSemantics().getInputAlphabet()); + stats.increaseCounter("roundCount", "CEX queries"); + + int roundCount = 1; + while (cex != null && roundCount < maxRounds) { + learner.refineHypothesis(cex); + hyp = learner.getHypothesisModel(); + cex = tester.findCounterExample(hyp, hyp.getSemantics().getInputAlphabet()); + stats.increaseCounter("roundCount", null); + roundCount += 1; + } + stats.pauseClock("learningRt"); + + final var finalHypothesis = learner.getHypothesisModel(); + + // Add some more stats: + stats.setCounter("result_locs", "Locations in result", finalHypothesis.getStates().size()); + + // Print final result + statistics: + stats.printStats(); + + System.out.println("Final hypothesis:"); + try { + GraphDOT.write(finalHypothesis.transitionGraphView(true, true), System.out); + } catch (IOException ignored) { + } + new ObservationTableASCIIWriter<>().write(learner.getObservationTable(), System.out); + + } +} diff --git a/examples/src/main/java/module-info.java b/examples/src/main/java/module-info.java index 30cc747eb9..5291347843 100644 --- a/examples/src/main/java/module-info.java +++ b/examples/src/main/java/module-info.java @@ -39,6 +39,7 @@ requires de.learnlib.oracle.emptiness; requires de.learnlib.oracle.equivalence; requires de.learnlib.oracle.membership; + requires de.learnlib.oracle.symbol_filters; requires de.learnlib.oracle.parallelism; requires de.learnlib.oracle.property; requires de.learnlib.testsupport.example; diff --git a/filters/cache/src/main/java/de/learnlib/filter/cache/LearningCache.java b/filters/cache/src/main/java/de/learnlib/filter/cache/LearningCache.java index cf6a862a95..2c2a39ae14 100644 --- a/filters/cache/src/main/java/de/learnlib/filter/cache/LearningCache.java +++ b/filters/cache/src/main/java/de/learnlib/filter/cache/LearningCache.java @@ -16,11 +16,16 @@ package de.learnlib.filter.cache; import de.learnlib.oracle.EquivalenceOracle; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealyOutputSymbol; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; import net.automatalib.automaton.fsa.DFA; +import net.automatalib.automaton.time.mmlt.LocalTimerMealy; import net.automatalib.automaton.transducer.MealyMachine; import net.automatalib.automaton.transducer.MooreMachine; import net.automatalib.word.Word; +import java.util.List; + /** * Interface for a cache used in automata learning. *

@@ -80,4 +85,20 @@ interface MealyLearningCache extends LearningCache extends LearningCache, I, Word> {} + + /** + * Specialization of the {@link LearningCache} interface for MMLT learning. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ + interface LocalTimerMealyLearningCache extends LearningCache, LocalTimerMealySemanticInputSymbol, Word>>{ + /** + * Lists all words that are currently in the cache. + * If a cached word is a prefix of another cached word, only the longer of them is returned. + * + * @return List of all stored words. + */ + List>> listAllWords(); + } } diff --git a/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/CacheTreeNode.java b/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/CacheTreeNode.java new file mode 100644 index 0000000000..111cba4e8e --- /dev/null +++ b/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/CacheTreeNode.java @@ -0,0 +1,170 @@ +package de.learnlib.filter.cache.mmlt; + +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealyOutputSymbol; +import net.automatalib.alphabet.time.mmlt.NonDelayingInput; +import net.automatalib.alphabet.time.mmlt.TimeStepSequence; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * A node in the tree cache used by the MMLT learner. + *

+ * A node has a parent and children for an arbitrary number of transitions + * with a non-delaying input. + * There is at most one timed transition. + * This transition has a sequence of time steps as input. + * The output is the output at the last time step in the sequence. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +class CacheTreeNode { + private record CacheTreeTransition(LocalTimerMealyOutputSymbol output, CacheTreeNode target) { + } + + + @Nullable + private CacheTreeNode parent; + private LocalTimerMealySemanticInputSymbol parentInput; + + private long timeout; + @Nullable + private CacheTreeTransition timeTransition; + + private Map, CacheTreeTransition> untimedChildren; + + public CacheTreeNode(CacheTreeNode parent, LocalTimerMealySemanticInputSymbol parentInput) { + this.parent = parent; + this.parentInput = parentInput; + + this.timeTransition = null; + this.timeout = -1; + + this.untimedChildren = new HashMap<>(); + } + + public CacheTreeNode addTimeChild(long timeout, LocalTimerMealyOutputSymbol output) { + if (this.hasTimeChild()) { + throw new IllegalStateException("State already has time child."); + } + + CacheTreeNode newChild = new CacheTreeNode<>(this, new TimeStepSequence<>(timeout)); + this.timeout = timeout; + this.timeTransition = new CacheTreeTransition<>(output, newChild); + return newChild; + } + + + // ------------------------------------------------------- + + /** + * Infers the number of predecessor nodes, i.e., the level + * of this node. + * + * @return Number of predecessors. Zero if this is the cache root. + */ + public int getNumPredecessors() { + int parentCount = 0; + var current = this; + + while (current.getParent() != null) { + parentCount++; + current = current.getParent(); + } + return parentCount; + } + + public boolean hasTimeChild() { + return this.timeTransition != null; + } + + public long getTimeout() { + if (!this.hasTimeChild()) { + throw new IllegalStateException(); + } + return timeout; + } + + public LocalTimerMealyOutputSymbol getTimeoutOutput() { + if (!this.hasTimeChild()) { + throw new IllegalStateException(); + } + return this.timeTransition.output(); + } + + public CacheTreeNode getTimeoutChild() { + if (!this.hasTimeChild()) { + throw new IllegalStateException(); + } + return this.timeTransition.target(); + } + + /** + * Breaks the time sequence: introduces a new child cx after the given number of time steps + * and adds the former child as child to cx. + * + * @param newTimeout Time at which the timeout sequence is split + * @param output Output at the end of the new time sequence + * @return New child node + */ + public CacheTreeNode splitTimeout(long newTimeout, LocalTimerMealyOutputSymbol output) { + if (this.timeTransition == null || newTimeout >= this.getTimeout()) { + throw new IllegalArgumentException("Must split at lower timeout."); + } + + CacheTreeNode newChild = new CacheTreeNode<>(this, new TimeStepSequence<>(newTimeout)); + newChild.timeout = this.timeout - newTimeout; + newChild.timeTransition = this.timeTransition; // keep output + target + this.timeTransition.target().setParent(newChild, new TimeStepSequence<>(this.timeout - newTimeout)); + + this.timeout = newTimeout; + this.timeTransition = new CacheTreeTransition<>(output, newChild); + + return newChild; + } + + // ------------------------------------------------------- + public CacheTreeNode getParent() { + return parent; + } + + public LocalTimerMealySemanticInputSymbol getParentInput() { + return parentInput; + } + + public void setParent(CacheTreeNode parent, LocalTimerMealySemanticInputSymbol parentInput) { + this.parent = parent; + this.parentInput = parentInput; + } + + // ------------------------------------------------------- + public boolean hasChild(NonDelayingInput input) { + return this.untimedChildren.containsKey(input); + } + + public LocalTimerMealyOutputSymbol getOutput(NonDelayingInput input) { + return this.untimedChildren.get(input).output(); + } + + public CacheTreeNode getChild(NonDelayingInput input) { + return this.untimedChildren.get(input).target(); + } + + public CacheTreeNode addUntimedChild(NonDelayingInput input, LocalTimerMealyOutputSymbol output) { + if (untimedChildren.containsKey(input)) { + throw new IllegalArgumentException("State already has an child for this input."); + } + + CacheTreeNode child = new CacheTreeNode<>(this, input); + this.untimedChildren.put(input, new CacheTreeTransition<>(output, child)); + return child; + } + + public Map, CacheTreeTransition> getUntimedChildren() { + return Collections.unmodifiableMap(this.untimedChildren); + } +} \ No newline at end of file diff --git a/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyCacheConsistencyTest.java b/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyCacheConsistencyTest.java new file mode 100644 index 0000000000..64a8cab48c --- /dev/null +++ b/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyCacheConsistencyTest.java @@ -0,0 +1,184 @@ +package de.learnlib.filter.cache.mmlt; + +import de.learnlib.algorithm.LocalTimerMealyModelParams; +import de.learnlib.oracle.EquivalenceOracle; +import de.learnlib.query.DefaultQuery; +import net.automatalib.alphabet.time.mmlt.*; +import net.automatalib.automaton.time.mmlt.LocalTimerMealy; +import net.automatalib.word.Word; +import net.automatalib.word.WordBuilder; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +/** + * Searches for counterexamples by comparing the behavior of the hypothesis and the query cache. + * If there are multiple counterexamples, the shortest one is returned. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class LocalTimerMealyCacheConsistencyTest implements EquivalenceOracle.LocalTimerMealyEquivalenceOracle { + private final static Logger logger = LoggerFactory.getLogger(LocalTimerMealyCacheConsistencyTest.class); + + private final LocalTimerMealyTreeSULCache sulCache; + private final LocalTimerMealyModelParams modelParams; + + LocalTimerMealyCacheConsistencyTest(LocalTimerMealyTreeSULCache sulCache, LocalTimerMealyModelParams modelParams) { + this.sulCache = sulCache; + this.modelParams = modelParams; + } + + private DefaultQuery, Word>> queryCache(Word> word) { + WordBuilder> wbInput = new WordBuilder<>(); + WordBuilder> wbOutput = new WordBuilder<>(); + + this.sulCache.pre(); + for (var sym : word) { + if (sym instanceof NonDelayingInput ndi) { + LocalTimerMealyOutputSymbol res = this.sulCache.step(ndi); + wbInput.append(ndi); + wbOutput.append(res); + } else if (sym instanceof TimeStepSequence ws) { + LocalTimerMealyOutputSymbol res = this.sulCache.timeoutStep(ws.getTimeSteps()); + wbInput.append(ws); + + if (res == null) { + wbOutput.append(new LocalTimerMealyOutputSymbol<>(this.modelParams.silentOutput())); + } else { + wbOutput.append(res); + } + } else { + throw new AssertionError("Symbol type must not be used in cache."); + } + } + this.sulCache.post(); + + return new DefaultQuery<>(wbInput.toWord(), wbOutput.toWord()); + } + + /** + * The cache does not use timeout symbols. Using these instead of time-step-sequences has several performance benefits. + * This function converts a query with a time-step-sequence to one that uses timeout symbols where possible. + * + * @param originalQuery Original query + * @return Converted query + */ + private DefaultQuery, Word>> convertTimeSequences(DefaultQuery, Word>> originalQuery) { + WordBuilder> wbInput = new WordBuilder<>(); + WordBuilder> wbOutput = new WordBuilder<>(); + + int symIdx = 0; + var queryInput = originalQuery.getInput(); + var queryOutput = originalQuery.getOutput(); + + while (symIdx < queryInput.length()) { + var inputSym = queryInput.getSymbol(symIdx); + var outputSym = queryOutput.getSymbol(symIdx); + symIdx++; + + if (inputSym instanceof NonDelayingInput ds) { + wbInput.append(ds); + wbOutput.append(outputSym); + } else if (inputSym instanceof TimeStepSequence ws) { + if (!outputSym.getSymbol().equals(this.modelParams.silentOutput()) || ws.getTimeSteps() == this.modelParams.maxTimeoutWaitingTime()) { + // Found a timeout OR no timeout after max_delay: + wbInput.append(new TimeoutSymbol<>()); + wbOutput.append(outputSym); + continue; + } + if (ws.getTimeSteps() >= this.modelParams.maxTimeoutWaitingTime()) { + throw new AssertionError("Wait time that exceeds max_delay in cache."); + } + + // Special case: silent output before max delay + // Cannot replace with "timeout", as this implies wait until max_delay. + // Hence: skip subsequent waits until reaching wait with output OR max_delay OR end of word: + long combinedWaitTime = ws.getTimeSteps(); + LocalTimerMealyOutputSymbol combinedOutput = outputSym; + + while (combinedOutput.getSymbol().equals(this.modelParams.silentOutput()) && combinedWaitTime < this.modelParams.maxTimeoutWaitingTime() + && symIdx < queryInput.length() && + queryInput.getSymbol(symIdx) instanceof TimeStepSequence nextWs) { + combinedWaitTime += nextWs.getTimeSteps(); + combinedOutput = queryOutput.getSymbol(symIdx); + symIdx++; + } + + if (combinedWaitTime >= this.modelParams.maxTimeoutWaitingTime() || !combinedOutput.getSymbol().equals(this.modelParams.silentOutput())) { + wbInput.append(new TimeoutSymbol<>()); + + if (combinedOutput.getSymbol().equals(this.modelParams.silentOutput())) { + // Reached max delay -> waiting for any time will now produce no more timeouts: + wbOutput.append(new LocalTimerMealyOutputSymbol<>(this.modelParams.silentOutput())); + } else { + // Found non-silent output: + wbOutput.append(new LocalTimerMealyOutputSymbol<>(combinedWaitTime, combinedOutput.getSymbol())); + } + } else { + // Reached end of word before max_delay OR non-wait symbol -> ignore rest of this word: + if (symIdx < queryInput.length() - 1) { + logger.debug("Ignoring at least one symbol during cache comparison."); + } + break; + } + } + } + return new DefaultQuery<>(wbInput.toWord(), wbOutput.toWord()); + } + + private DefaultQuery, Word>> reduceToAllowedInputs(Set> allowedInputs, DefaultQuery, Word>> query) { + // Find the longest prefix with allowed inputs: + int prefixLength = 0; + while (prefixLength < query.getInput().length() && allowedInputs.contains(query.getInput().getSymbol(prefixLength))) { + prefixLength++; + } + + if (prefixLength == query.getInput().length()) { + return query; // maximum length -> no need to reduce + } else { + return new DefaultQuery<>(query.getInput().subWord(0, prefixLength), query.getOutput().subWord(0, prefixLength)); + } + } + + + @Override + public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, Collection> inputs) { + Set> allowedInputs = new HashSet<>(inputs); + boolean allInputsConsidered = allowedInputs.containsAll(hypothesis.getSemantics().getInputAlphabet()); + + // Query all cached words: + List>> cachedWords = this.sulCache.listAllWords(); + + List, Word>>> counterexamples = new ArrayList<>(); + for (var word : cachedWords) { + // First, query word as-is (may include wait-symbols in input): + DefaultQuery, Word>> rawCacheQuery = this.queryCache(word); + + // Next, convert query that includes wait-symbols to query with timeout-symbols: + var convertedQuery = this.convertTimeSequences(rawCacheQuery); + + // The counterexample may only use a subset of the allowed inputs. + // If so, cut the query to the prefix of the word that is allowed: + var reducedQuery = (allInputsConsidered) ? convertedQuery : this.reduceToAllowedInputs(allowedInputs, convertedQuery); + + // Finally, query hypothesis using the converted query: + Word> hypOutput = hypothesis.getSemantics().computeSuffixOutput(Word.epsilon(), reducedQuery.getInput()); + + if (!hypOutput.equals(reducedQuery.getOutput())) { + // Hyp gives different output than cache (= SUL): + counterexamples.add(reducedQuery); + } + } + + if (counterexamples.isEmpty()) { + return null; + } + + // Take the shortest word: + return counterexamples.stream().min(Comparator.comparingInt(w -> w.getInput().length())).get(); + } + +} diff --git a/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyTreeSULCache.java b/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyTreeSULCache.java new file mode 100644 index 0000000000..0e259485b8 --- /dev/null +++ b/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyTreeSULCache.java @@ -0,0 +1,272 @@ +package de.learnlib.filter.cache.mmlt; + + +import de.learnlib.algorithm.LocalTimerMealyModelParams; +import de.learnlib.filter.cache.LearningCache; +import de.learnlib.oracle.EquivalenceOracle; +import de.learnlib.statistic.container.DummyStatsContainer; +import de.learnlib.statistic.container.LearnerStatsProvider; +import de.learnlib.statistic.container.StatsContainer; +import de.learnlib.sul.LocalTimerMealySUL; +import net.automatalib.alphabet.impl.GrowingMapAlphabet; + +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealyOutputSymbol; +import net.automatalib.alphabet.time.mmlt.NonDelayingInput; +import net.automatalib.alphabet.time.mmlt.TimeStepSequence; +import net.automatalib.automaton.transducer.impl.CompactMealy; +import net.automatalib.graph.Graph; +import net.automatalib.graph.concept.GraphViewable; +import net.automatalib.word.Word; +import net.automatalib.word.WordBuilder; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.*; + +/** + * Caches queries sent to a LocalTimerMealySUL. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class LocalTimerMealyTreeSULCache implements LocalTimerMealySUL, LearningCache.LocalTimerMealyLearningCache, GraphViewable, LearnerStatsProvider { + private final LocalTimerMealySUL delegate; + + private final CacheTreeNode cacheRoot; + private CacheTreeNode currentState; + + private final LocalTimerMealyModelParams modelParams; + private final LocalTimerMealyOutputSymbol silentOutput; + private boolean cacheMiss; + + private StatsContainer stats = new DummyStatsContainer(); + + @Override + public void setStatsContainer(StatsContainer container) { + this.stats = container; + } + + public LocalTimerMealyTreeSULCache(LocalTimerMealySUL delegate, LocalTimerMealyModelParams modelParams) { + this.delegate = delegate; + this.modelParams = modelParams; + this.silentOutput = new LocalTimerMealyOutputSymbol<>(modelParams.silentOutput()); + + // Init cache: + this.cacheRoot = new CacheTreeNode<>(null, null); + this.currentState = null; + } + + + private void followCurrentPrefix() { + this.delegate.pre(); + + WordBuilder> wbPrefix = new WordBuilder<>(); + + var current = this.currentState; + while (current.getParent() != null) { + wbPrefix.append(current.getParentInput()); + current = current.getParent(); + } + + Word> prefix = wbPrefix.reverse().toWord(); + this.delegate.follow(prefix); + } + + @Override + public LocalTimerMealyOutputSymbol step(NonDelayingInput input) { + if (this.currentState == null) { + throw new IllegalStateException(); + } + + if (!cacheMiss) { + if (this.currentState.hasChild(input)) { + LocalTimerMealyOutputSymbol output = this.currentState.getOutput(input); + this.currentState = this.currentState.getChild(input); + return output; + } + this.followCurrentPrefix(); + this.cacheMiss = true; + } + + // Cache miss -> query + insert: + LocalTimerMealyOutputSymbol output = this.delegate.step(input); + this.currentState = this.currentState.addUntimedChild(input, output); + return output; + } + + + @Override + public @Nullable LocalTimerMealyOutputSymbol timeoutStep(long maxTime) { + if (currentState == null) { + throw new IllegalStateException(); + } + + long remaining = maxTime; + if (!this.cacheMiss) { + // Move to closest state in cache: + while (remaining > 0) { + if (!currentState.hasTimeChild()) { + break; // cache miss + } + + if (currentState.getTimeout() > remaining) { + // Split current timeout: + this.currentState = this.currentState.splitTimeout(remaining, this.silentOutput); + return null; // no timer in this state + } + + LocalTimerMealyOutputSymbol currentOutput = currentState.getTimeoutOutput(); + remaining -= currentState.getTimeout(); + this.currentState = this.currentState.getTimeoutChild(); + + if (!currentOutput.equals(this.silentOutput)) { + // Found valid timeout: + return new LocalTimerMealyOutputSymbol<>(maxTime - remaining, currentOutput.getSymbol()); + } + } + + if (remaining == 0) { + return null; // no timer in this state + } + + this.followCurrentPrefix(); + this.cacheMiss = true; + } + + + LocalTimerMealyOutputSymbol timeoutStepResult = this.delegate.timeoutStep(remaining); + if (timeoutStepResult == null) { // no timers here + this.currentState = this.currentState.addTimeChild(remaining, this.silentOutput); + return null; + } else { + this.currentState = this.currentState.addTimeChild(timeoutStepResult.getDelay(), new LocalTimerMealyOutputSymbol<>(timeoutStepResult.getSymbol())); + return new LocalTimerMealyOutputSymbol<>(maxTime - remaining + timeoutStepResult.getDelay(), timeoutStepResult.getSymbol()); + } + + } + + + @Override + public void pre() { + this.currentState = this.cacheRoot; + this.cacheMiss = false; + } + + @Override + public void post() { + this.currentState = null; + + if (this.cacheMiss) { + this.delegate.post(); + stats.increaseCounter("Cache_Missed_Count", "Cache misses"); + } else { + stats.increaseCounter("Cache_Hit_Count", "Cache hits"); + } + } + + // ------------------------------------------------------- + + /** + * Returns the leaves of the cache tree. + * + * @return List of leaf nodes. + */ + private List> getLeaves() { + List> leaves = new ArrayList<>(); + + Deque> unvisited = new ArrayDeque<>(); + unvisited.add(this.cacheRoot); + + while (!unvisited.isEmpty()) { + CacheTreeNode currentNode = unvisited.remove(); + + int successors = 0; + if (currentNode.hasTimeChild()) { + unvisited.add(currentNode.getTimeoutChild()); + successors++; + } + + for (var sym : currentNode.getUntimedChildren().keySet()) { + unvisited.add(currentNode.getChild(sym)); + successors++; + } + if (successors == 0) { // leaf + leaves.add(currentNode); + } + } + + return leaves; + } + + @Override + public List>> listAllWords() { + List> leaves = this.getLeaves(); + + List>> finalWords = new ArrayList<>(leaves.size()); + + for (var leaf : leaves) { + // Word builder capacity = number of predecessors: + int symCount = leaf.getNumPredecessors(); + WordBuilder> wbInput = new WordBuilder<>(symCount); + + // Move towards the root: + var current = leaf; + while (current.getParent() != null) { + wbInput.append(current.getParentInput()); + current = current.getParent(); + } + + // Start at root -> flip buffer: + wbInput.reverse(); + finalWords.add(wbInput.toWord()); + } + + return finalWords; + } + + + @Override + public Graph graphView() { + // Convert tree to a mealy automaton: + CompactMealy, LocalTimerMealyOutputSymbol> mealy = new CompactMealy<>(new GrowingMapAlphabet<>()); + + Map, Integer> stateMap = new HashMap<>(); + stateMap.put(this.cacheRoot, mealy.addInitialState()); + + Deque> pending = new ArrayDeque<>(); + pending.add(this.cacheRoot); + + + while (!pending.isEmpty()) { + CacheTreeNode current = pending.remove(); + + if (current.hasTimeChild()) { + var child = current.getTimeoutChild(); + if (!stateMap.containsKey(child)) { + stateMap.put(child, mealy.addState()); + pending.add(child); + } + mealy.addAlphabetSymbol(new TimeStepSequence<>(current.getTimeout())); + mealy.addTransition(stateMap.get(current), new TimeStepSequence<>(current.getTimeout()), stateMap.get(child), current.getTimeoutOutput()); + } + + for (var sym : current.getUntimedChildren().keySet()) { + var child = current.getChild(sym); + if (!stateMap.containsKey(child)) { + stateMap.put(child, mealy.addState()); + pending.add(child); + } + mealy.addAlphabetSymbol(sym); + mealy.addTransition(stateMap.get(current), sym, stateMap.get(child), current.getOutput(sym)); + } + } + + return mealy.graphView(); + } + + @Override + public EquivalenceOracle.LocalTimerMealyEquivalenceOracle createCacheConsistencyTest() { + return new LocalTimerMealyCacheConsistencyTest<>(this, this.modelParams); + } + +} diff --git a/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/TimeoutReducerSUL.java b/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/TimeoutReducerSUL.java new file mode 100644 index 0000000000..461d8d1992 --- /dev/null +++ b/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/TimeoutReducerSUL.java @@ -0,0 +1,80 @@ +package de.learnlib.filter.cache.mmlt; + +import de.learnlib.statistic.container.LearnerStatsProvider; +import de.learnlib.statistic.container.StatsContainer; +import de.learnlib.sul.LocalTimerMealySUL; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealyOutputSymbol; +import net.automatalib.alphabet.time.mmlt.NonDelayingInput; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Avoids redundant queries for timeouts. + *

+ * Assume we waited maxDelay for a timeout and observed no expiration. + * Then, any consecutive timeout-input must also show no timer (assuming sufficient maxDelay). + * Hence, we do not need to query the SUL for these. + *

+ * We may observe a timeout again after any non-delaying input, as this may + * trigger a location-change. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class TimeoutReducerSUL implements LocalTimerMealySUL, LearnerStatsProvider { + + private final LocalTimerMealySUL delegate; + private final long maxDelay; + + /** + * Delay since the last non-delaying input OR + * timer expiration. + */ + private long noTimeoutWaitingTime; + + private StatsContainer stats; + + public TimeoutReducerSUL(LocalTimerMealySUL delegate, long maxDelay, StatsContainer stats) { + this.delegate = delegate; + this.maxDelay = maxDelay; + this.stats = stats; + } + + @Override + public LocalTimerMealyOutputSymbol step(NonDelayingInput input) { + this.noTimeoutWaitingTime = 0; // might observe expirations again + return delegate.step(input); + } + + @Override + public @Nullable LocalTimerMealyOutputSymbol timeoutStep(long maxTime) { + if (this.noTimeoutWaitingTime >= this.maxDelay) { + return null; // cannot observe expiration until non-delaying input + } + + var result = delegate.timeoutStep(maxTime); + + if (result == null) { + this.noTimeoutWaitingTime += maxTime; + } else { + this.noTimeoutWaitingTime = 0; + } + + return result; + } + + @Override + public void pre() { + delegate.pre(); + this.noTimeoutWaitingTime = 0; + } + + @Override + public void post() { + delegate.post(); + } + + @Override + public void setStatsContainer(StatsContainer container) { + this.stats = container; + } +} diff --git a/filters/cache/src/main/java/module-info.java b/filters/cache/src/main/java/module-info.java index d75b963446..edd994d448 100644 --- a/filters/cache/src/main/java/module-info.java +++ b/filters/cache/src/main/java/module-info.java @@ -47,4 +47,5 @@ exports de.learnlib.filter.cache.mealy; exports de.learnlib.filter.cache.moore; exports de.learnlib.filter.cache.sul; + exports de.learnlib.filter.cache.mmlt; } diff --git a/filters/cache/src/test/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyCacheTest.java b/filters/cache/src/test/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyCacheTest.java new file mode 100644 index 0000000000..b0eb0810de --- /dev/null +++ b/filters/cache/src/test/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyCacheTest.java @@ -0,0 +1,138 @@ +package de.learnlib.filter.cache.mmlt; + +import de.learnlib.algorithm.LocalTimerMealyModelParams; +import de.learnlib.driver.simulator.LocalTimerMealySimulatorSUL; +import de.learnlib.oracle.membership.TimedQueryOracle; +import net.automatalib.alphabet.impl.GrowingMapAlphabet; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.NonDelayingInput; +import net.automatalib.alphabet.time.mmlt.TimeStepSymbol; +import net.automatalib.alphabet.time.mmlt.TimeoutSymbol; +import net.automatalib.automaton.time.impl.mmlt.CompactLocalTimerMealy; +import net.automatalib.automaton.time.impl.mmlt.StringSymbolCombiner; +import net.automatalib.common.util.random.RandomUtil; +import net.automatalib.word.Word; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Random; + +@Test +public class LocalTimerMealyCacheTest { + private CompactLocalTimerMealy buildBaseModel() { + var symbols = List.of("p1", "p2", "abort", "collect"); + GrowingMapAlphabet> alphabet = new GrowingMapAlphabet<>(); + symbols.forEach(s -> alphabet.add(new NonDelayingInput<>(s))); + + var model = new CompactLocalTimerMealy<>(alphabet, "void", StringSymbolCombiner.getInstance()); + + var s0 = model.addState(); + var s1 = model.addState(); + var s2 = model.addState(); + var s3 = model.addState(); + + model.setInitialState(s0); + + model.addTransition(s0, new NonDelayingInput<>("p1"), "go", s1); + model.addTransition(s1, new NonDelayingInput<>("abort"), "ok", s1); + model.addLocalReset(s1, new NonDelayingInput<>("abort")); + + model.addPeriodicTimer(s1, "a", 3, "part"); + model.addPeriodicTimer(s1, "b", 6, "noise"); + model.addOneShotTimer(s1, "c", 40, "done", s3); + + model.addTransition(s0, new NonDelayingInput<>("p2"), "go", s2); + model.addTransition(s2, new NonDelayingInput<>("abort"), "void", s3); + model.addOneShotTimer(s2, "d", 4, "done", s3); + + model.addTransition(s3, new NonDelayingInput<>("collect"), "void", s0); + + return model; + } + + /** + * Tests if the information in the cache is consistent with the output of the SUL. + */ + public void testCacheAndSULConsistency() { + Random random = new Random(100); + + var automaton = buildBaseModel(); + var params = new LocalTimerMealyModelParams<>("void", 4, 80, StringSymbolCombiner.getInstance()); + + var sul = new LocalTimerMealySimulatorSUL<>(automaton); + var cacheSUL = new LocalTimerMealyTreeSULCache<>(sul, params); + var timeOracleWithCache = new TimedQueryOracle<>(cacheSUL, params); + var timeOracleWithoutCache = new TimedQueryOracle<>(sul, params); + + + var listAlphabet = new ArrayList<>(automaton.getSemantics().getInputAlphabet()); + + // Generate some random words and compare outputs of the cache, SUL, and automaton: + List>> words = new ArrayList<>(); + for (int i = 0; i < 500; i++) { + int maxLength = random.nextInt(1, 500); + var symbols = RandomUtil.sample(random, listAlphabet, maxLength); + var word = Word.fromList(symbols); + words.add(word); + + var cacheOutput = timeOracleWithCache.querySuffixOutput(Word.epsilon(), word); + var sulOutput = timeOracleWithoutCache.querySuffixOutput(Word.epsilon(), word); + var automatonOutput = automaton.getSemantics().computeSuffixOutput(Word.epsilon(), word); + + Assert.assertEquals(sulOutput, automatonOutput, "Automaton output does not match SUL output for word " + word); + Assert.assertEquals(cacheOutput, sulOutput, "Cache output does not match SUL output for word " + word); + } + + // Now that the cache contents have changed, ensure that the results are still correct: + for (var word : words) { + var cacheOutput = timeOracleWithCache.querySuffixOutput(Word.epsilon(), word); + var sulOutput = timeOracleWithoutCache.querySuffixOutput(Word.epsilon(), word); + + Assert.assertEquals(sulOutput, cacheOutput, "Cache output does not match SUL output for word " + word); + } + } + + @Test + public void testCacheConsistencyTest() { + // Test if the cache consistency test works correctly: + var refAutomaton = buildBaseModel(); + var params = new LocalTimerMealyModelParams<>("void", 4, 80, StringSymbolCombiner.getInstance()); + + var sul = new LocalTimerMealySimulatorSUL<>(refAutomaton); + var cacheSUL = new LocalTimerMealyTreeSULCache<>(sul, params); + var timeOracleWithCache = new TimedQueryOracle<>(cacheSUL, params); + + // Add word to cache: + Word> testWord = Word.fromSymbols( + new NonDelayingInput<>("p2"), new TimeoutSymbol<>(), new TimeStepSymbol<>(), new TimeoutSymbol<>() + ); + timeOracleWithCache.querySuffixOutput(Word.epsilon(), testWord); + + // Create a bad hypothesis: + var badAutomaton = buildBaseModel(); + badAutomaton.removeTimer(2, "d"); + badAutomaton.addPeriodicTimer(2, "d", 4, "done"); + + // Query the cache for a counterexample: + Word> expectedCex = Word.fromSymbols( + new NonDelayingInput<>("p2"), new TimeoutSymbol<>(), new TimeoutSymbol<>() + ); + + var cacheConsistencyTest = cacheSUL.createCacheConsistencyTest(); + var cex = cacheConsistencyTest.findCounterExample(badAutomaton, refAutomaton.getSemantics().getInputAlphabet()); + Assert.assertNotNull(cex); + Assert.assertEquals(cex.getInput(), expectedCex); + + // Now test with a reduced alphabet: + var symbols = List.of("p1", "abort", "collect"); // not p1 + GrowingMapAlphabet> reducedAlphabet = new GrowingMapAlphabet<>(); + symbols.forEach(s -> reducedAlphabet.add(new NonDelayingInput<>(s))); + reducedAlphabet.add(new TimeoutSymbol<>()); + + // The only counterexample in the cache has the prefix p2, which is now omitted: + Assert.assertNull(cacheConsistencyTest.findCounterExample(badAutomaton, reducedAlphabet)); + } +} diff --git a/filters/statistics/pom.xml b/filters/statistics/pom.xml index 83ab5b76ec..2e7ec208c8 100644 --- a/filters/statistics/pom.xml +++ b/filters/statistics/pom.xml @@ -76,6 +76,11 @@ limitations under the License. mockito-core + + org.checkerframework + checker-qual + + org.testng testng diff --git a/filters/statistics/src/main/java/de/learnlib/filter/statistic/sul/LocalTimerMealyStatsSUL.java b/filters/statistics/src/main/java/de/learnlib/filter/statistic/sul/LocalTimerMealyStatsSUL.java new file mode 100644 index 0000000000..eacbb22998 --- /dev/null +++ b/filters/statistics/src/main/java/de/learnlib/filter/statistic/sul/LocalTimerMealyStatsSUL.java @@ -0,0 +1,91 @@ +package de.learnlib.filter.statistic.sul; + +import de.learnlib.statistic.container.LearnerStatsProvider; +import de.learnlib.statistic.container.StatsContainer; +import de.learnlib.sul.LocalTimerMealySUL; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealyOutputSymbol; +import net.automatalib.alphabet.time.mmlt.NonDelayingInput; +import net.automatalib.alphabet.time.mmlt.TimeStepSequence; +import net.automatalib.word.Word; +import org.checkerframework.checker.nullness.qual.Nullable; + + +/** + * Wrapper for an MMLT SUL that gathers various statistics on queries sent to this SUL. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class LocalTimerMealyStatsSUL implements LocalTimerMealySUL, LearnerStatsProvider { + private final LocalTimerMealySUL delegate; + private StatsContainer stats; + + @Nullable + private final String name; + + public LocalTimerMealyStatsSUL(LocalTimerMealySUL delegate, StatsContainer stats) { + this(delegate, stats, null); + } + + public LocalTimerMealyStatsSUL(LocalTimerMealySUL delegate, StatsContainer stats, String name) { + this.delegate = delegate; + this.stats = stats; + this.name = name; + } + + @Override + public void setStatsContainer(StatsContainer container) { + this.stats = container; + } + + private String withPrefix(String label) { + if (this.name == null) { + return label; + } + return this.name + ":" + label; + } + + @Override + public LocalTimerMealyOutputSymbol step(NonDelayingInput input) { + stats.increaseCounter(withPrefix("sul_untimed_syms_counter"), + withPrefix("Total untimed symbols")); + return this.delegate.step(input); + } + + @Override + public @Nullable LocalTimerMealyOutputSymbol timeoutStep(long maxTime) { + LocalTimerMealyOutputSymbol res = this.delegate.timeoutStep(maxTime); + if (res == null) { + // Waited until maxTime, no timeout occurred: + stats.increaseCounter(withPrefix("sul_total_time"), + withPrefix("Total query time"), maxTime); + } else { + stats.increaseCounter(withPrefix("sul_total_time"), + withPrefix("Total query time"), res.getDelay()); + } + + return res; + } + + @Override + public Word> collectTimeouts(TimeStepSequence input) { + stats.increaseCounter(withPrefix("sul_total_time"), + withPrefix("Total query time"), + input.getTimeSteps()); + return this.delegate.collectTimeouts(input); + } + + @Override + public void pre() { + this.delegate.pre(); + stats.increaseCounter(withPrefix("sul_resets_counter"), + withPrefix("SUL resets")); + } + + @Override + public void post() { + this.delegate.post(); + } + + +} diff --git a/filters/statistics/src/main/java/module-info.java b/filters/statistics/src/main/java/module-info.java index f262e24f56..90ce2aef02 100644 --- a/filters/statistics/src/main/java/module-info.java +++ b/filters/statistics/src/main/java/module-info.java @@ -34,6 +34,7 @@ // annotations are 'provided'-scoped and do not need to be loaded at runtime requires static de.learnlib.tooling.annotation; + requires static org.checkerframework.checker.qual; exports de.learnlib.filter.statistic; exports de.learnlib.filter.statistic.learner; diff --git a/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/RandomWMethodEQOracle.java b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/RandomWMethodEQOracle.java index a8e2017979..2110266218 100644 --- a/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/RandomWMethodEQOracle.java +++ b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/RandomWMethodEQOracle.java @@ -44,7 +44,7 @@ /** * Implements an equivalence test based on a randomized version of the W-method as described in Complementing Model Learning with Mutation-Based Fuzzing by Rick + * href="https://arxiv.org/abs/1611.02429">Complementing LocalTimerMealyModel Learning with Mutation-Based Fuzzing by Rick * Smetsers, Joshua Moerman, Mark Janssen, Sicco Verwer. Instead of enumerating the test suite in order, this is a * sampling implementation: *

    diff --git a/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/RandomWpMethodEQOracle.java b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/RandomWpMethodEQOracle.java index f6310b6427..d23b0fa0d0 100644 --- a/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/RandomWpMethodEQOracle.java +++ b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/RandomWpMethodEQOracle.java @@ -45,7 +45,7 @@ /** * Implements an equivalence test based on a randomized version of the W(p)-method as described in Complementing Model Learning with Mutation-Based Fuzzing by Rick + * href="https://arxiv.org/abs/1611.02429">Complementing LocalTimerMealyModel Learning with Mutation-Based Fuzzing by Rick * Smetsers, Joshua Moerman, Mark Janssen, Sicco Verwer. Instead of enumerating the test suite in order, this is a * sampling implementation: *
      diff --git a/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/mmlt/LocalTimerMealyEQOracleChain.java b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/mmlt/LocalTimerMealyEQOracleChain.java new file mode 100644 index 0000000000..33fbeb6a29 --- /dev/null +++ b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/mmlt/LocalTimerMealyEQOracleChain.java @@ -0,0 +1,92 @@ +package de.learnlib.oracle.equivalence.mmlt; + +import de.learnlib.oracle.EquivalenceOracle; +import de.learnlib.query.DefaultQuery; +import de.learnlib.statistic.container.DummyStatsContainer; +import de.learnlib.statistic.container.LearnerStatsProvider; +import de.learnlib.statistic.container.StatsContainer; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealyOutputSymbol; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.automaton.time.mmlt.LocalTimerMealy; +import net.automatalib.word.Word; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +/** + * A chain of MMLT equivalence oracles. The oracles are queried in the given order until either a counterexample is found + * or nor example is found. + *

      + * This operates similarly to {@link de.learnlib.oracle.equivalence.EQOracleChain}, + * but also stores statistics about the queries in a {@link StatsContainer}. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class LocalTimerMealyEQOracleChain implements EquivalenceOracle.LocalTimerMealyEquivalenceOracle, LearnerStatsProvider { + + private static final Logger logger = LoggerFactory.getLogger(LocalTimerMealyEQOracleChain.class); + + private final List> oracles = new ArrayList<>(); + private StatsContainer stats = new DummyStatsContainer(); + + /** + * Stores names for the equivalence oracles that are used in statistics. + */ + private List oracleNames; + + public void addOracle(LocalTimerMealyEquivalenceOracle oracle) { + this.oracles.add(oracle); + + // Update names: + // Names follow the convention typeName + # + index of this oracle among all oracles of same type. + this.oracleNames = new ArrayList<>(oracles.size()); + + Map typeCounter = new HashMap<>(); + for (var eqOracle : this.oracles) { + String typeName = eqOracle.getClass().getSimpleName(); + + int currentCount = typeCounter.getOrDefault(typeName, -1); + int newCount = currentCount + 1; + typeCounter.put(typeName, newCount); + + this.oracleNames.add(typeName + "#" + newCount); + } + } + + + @Override + public void setStatsContainer(StatsContainer container) { + this.stats = container; + + // Propagate to all oracles: + for (var oracle : this.oracles) { + if (oracle instanceof LearnerStatsProvider provider) { + provider.setStatsContainer(stats); + } + } + } + + @Override + public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, Collection> inputs) { + if (this.oracles.isEmpty()) throw new IllegalStateException("Must specify at least one cex oracle in chain."); + + int oracleIdx = 0; + for (var oracle : this.oracles) { + var cex = oracle.findCounterExample(hypothesis, inputs); + if (cex != null) { + String oracleName = this.oracleNames.get(oracleIdx); + stats.increaseCounter("cnt_cex_" + oracleName, "CEX from " + oracleName); + + logger.debug("{} found counterexample: {}", "cnt_cex_" + oracleName, cex); + + return cex; + } + oracleIdx++; + } + + return null; + } +} diff --git a/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/mmlt/LocalTimerMealyRandomWpOracle.java b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/mmlt/LocalTimerMealyRandomWpOracle.java new file mode 100644 index 0000000000..51be1240c0 --- /dev/null +++ b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/mmlt/LocalTimerMealyRandomWpOracle.java @@ -0,0 +1,147 @@ +package de.learnlib.oracle.equivalence.mmlt; + +import de.learnlib.oracle.EquivalenceOracle; +import de.learnlib.oracle.AbstractTimedQueryOracle; +import de.learnlib.query.DefaultQuery; +import de.learnlib.statistic.container.DummyStatsContainer; +import de.learnlib.statistic.container.LearnerStatsProvider; +import de.learnlib.statistic.container.StatsContainer; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealyOutputSymbol; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.automaton.time.impl.mmlt.ReducedLocalTimerMealySemantics; +import net.automatalib.automaton.time.mmlt.LocalTimerMealy; +import net.automatalib.common.util.string.AbstractPrintable; +import net.automatalib.util.automaton.Automata; +import net.automatalib.util.automaton.cover.LocalTimerMealyCover; +import net.automatalib.word.Word; +import net.automatalib.word.WordBuilder; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +/** + * RandomWP counterexample search for MMLT learning. + * Key modification: samples prefix from entry prefixes instead of all state prefixes. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class LocalTimerMealyRandomWpOracle implements EquivalenceOracle.LocalTimerMealyEquivalenceOracle, LearnerStatsProvider { + private static final Logger logger = LoggerFactory.getLogger(LocalTimerMealyRandomWpOracle.class); + private final AbstractTimedQueryOracle timeOracle; + + private StatsContainer stats = new DummyStatsContainer(); + + private final Random random; + private final int minSize; + private final int rndLen; + private final int bound; + + public LocalTimerMealyRandomWpOracle(AbstractTimedQueryOracle timeOracle, + long randomSeed, + int minSize, int rndAddLength, int bound) { + + this.timeOracle = timeOracle; + + this.random = new Random(randomSeed); + + this.minSize = minSize; + this.rndLen = rndAddLength; + this.bound = bound; + } + + @Override + public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, Collection> inputs) { + return findCounterExampleInternal(hypothesis, inputs); + } + + private DefaultQuery, Word>> findCounterExampleInternal(LocalTimerMealy hypothesis, Collection> inputs) { + // Make expanded form of hypothesis: + var hypSemModel = ReducedLocalTimerMealySemantics.forLocalTimerMealy(hypothesis); + + // Create a list of symbols (for faster access): + List> listAlphabet = new ArrayList<>(inputs); + + // Identify global suffixes: + var globalSuffixes = Automata.characterizingSet(hypSemModel, inputs); + + // Get list of prefixes in deterministic order (so we can reproduce experiments easily): + var locationCover = LocalTimerMealyCover.getLocalTimerMealyLocationCover(hypothesis, listAlphabet); + var prefixList = locationCover + .values() + .stream() + .sorted(Comparator.comparing(AbstractPrintable::toString)) + .toList(); + + // Generate test words: + for (int i = 0; i < this.bound; i++) { + stats.increaseCounter("WP_TESTED_WORD", "RandomWpOracle: tested words"); + + var sulAnswer = this.generateTestword(prefixList, globalSuffixes, hypothesis, hypSemModel, listAlphabet); + Word> hypAnswer = hypothesis.getSemantics().computeSuffixOutput(sulAnswer.getPrefix(), sulAnswer.getSuffix()); + + // Found inconsistency if outputs do no match: + if (!sulAnswer.getOutput().equals(hypAnswer)) { + return sulAnswer; + } + } + + return null; + } + + private DefaultQuery, Word>> generateTestword(List>> prefixes, + List>> globalSuffixes, + LocalTimerMealy hypothesis, + ReducedLocalTimerMealySemantics hypSemModel, + List> alphabet) { + + WordBuilder> wbTestWord = new WordBuilder<>(); + + // 1. Pick a random entry config prefix: + Word> prefix = prefixes.get(this.random.nextInt(prefixes.size())); + wbTestWord.append(prefix); + + // 2. Add random middle part: + int size = minSize; + while ((size > 0) || (this.random.nextDouble() > 1 / (this.rndLen + 1.0))) { + var nextSymbol = alphabet.get(this.random.nextInt(alphabet.size())); + wbTestWord.append(nextSymbol); + + if (size > 0) { + size--; + } + } + + // 3. Pick a random suffix for this state: + // 50% chance for state testing, 50% chance for transition testing + Word> suffix = Word.epsilon(); + if (this.random.nextBoolean()) { + if (!globalSuffixes.isEmpty()) { + suffix = globalSuffixes.get(random.nextInt(globalSuffixes.size())); + } + } else { + // Identify configuration reached by prefix: + var currentConfig = hypothesis.getSemantics().traceInputs(wbTestWord.toWord()); + var state = hypSemModel.getStateForConfiguration(currentConfig, true); + var localSuffixes = Automata.stateCharacterizingSet(hypSemModel, alphabet, state); + + if (!localSuffixes.isEmpty()) { + suffix = localSuffixes.get(random.nextInt(localSuffixes.size())); + } + } + wbTestWord.append(suffix); + + // Query SUL: + var testWord = wbTestWord.toWord(); + var sulAnswer = timeOracle.answerQuery(testWord); + return new DefaultQuery<>(testWord, sulAnswer); + } + + + @Override + public void setStatsContainer(StatsContainer container) { + this.stats = container; + } +} diff --git a/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/mmlt/LocalTimerMealySimulatorOracle.java b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/mmlt/LocalTimerMealySimulatorOracle.java new file mode 100644 index 0000000000..acd60c8ada --- /dev/null +++ b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/mmlt/LocalTimerMealySimulatorOracle.java @@ -0,0 +1,42 @@ +package de.learnlib.oracle.equivalence.mmlt; + +import de.learnlib.oracle.EquivalenceOracle; +import de.learnlib.query.DefaultQuery; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealyOutputSymbol; +import net.automatalib.automaton.time.mmlt.LocalTimerMealy; +import net.automatalib.util.automaton.mmlt.LocalTimerMealyUtil; +import net.automatalib.word.Word; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * A simulator oracle for MMLTs. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class LocalTimerMealySimulatorOracle implements EquivalenceOracle.LocalTimerMealyEquivalenceOracle { + + private final LocalTimerMealy refModel; + + public LocalTimerMealySimulatorOracle(LocalTimerMealy refModel) { + this.refModel = refModel; + } + + @Override + public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, Collection> inputs) { + List> listInputs = new ArrayList<>(inputs); + + var separatingWord = LocalTimerMealyUtil.findSeparatingWord(refModel, hypothesis, listInputs); + if (separatingWord != null) { + var sulOutput = refModel.getSemantics().computeSuffixOutput(Word.epsilon(), separatingWord); + return new DefaultQuery<>(Word.epsilon(), separatingWord, sulOutput); + } else { + return null; + } + } +} diff --git a/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/mmlt/ResetSearchOracle.java b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/mmlt/ResetSearchOracle.java new file mode 100644 index 0000000000..a55d78df87 --- /dev/null +++ b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/mmlt/ResetSearchOracle.java @@ -0,0 +1,148 @@ +package de.learnlib.oracle.equivalence.mmlt; + +import de.learnlib.oracle.AbstractTimedQueryOracle; +import de.learnlib.oracle.EquivalenceOracle; +import de.learnlib.query.DefaultQuery; +import net.automatalib.alphabet.time.mmlt.*; +import net.automatalib.automaton.time.mmlt.LocalTimerMealy; +import net.automatalib.common.util.random.RandomUtil; +import net.automatalib.common.util.string.AbstractPrintable; +import net.automatalib.util.automaton.cover.LocalTimerMealyCover; +import net.automatalib.word.Word; +import net.automatalib.word.WordBuilder; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +/** + * Searches for counterexamples that reveal local resets. + *

      + * - Takes any prefix from a known location + * - Appends a single time step. + * - Appends inputs of all non-delaying inputs that self-loop in that location. + * - Appends timeout. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class ResetSearchOracle implements EquivalenceOracle.LocalTimerMealyEquivalenceOracle { + + private final static Logger logger = LoggerFactory.getLogger(ResetSearchOracle.class); + + private final AbstractTimedQueryOracle timeOracle; + private final Random locPrefixRandom; + + private final double loopInsertPerc; + private final double testedLocPerc; + + private final long loopingInputSelectionSeed; + + public ResetSearchOracle(AbstractTimedQueryOracle timeOracle, long seed, double loopInsertPerc, double testedLocPerc) { + this.timeOracle = timeOracle; + this.locPrefixRandom = new Random(seed); + this.loopInsertPerc = loopInsertPerc; + this.testedLocPerc = testedLocPerc; + + this.loopingInputSelectionSeed = seed; + } + + private List> getLoopingSymbols(S sourceLoc, List> alphabet, LocalTimerMealy hypothesis) { + + List> loopingInputs = new ArrayList<>(); + for (var sym : alphabet) { + if (!(sym instanceof NonDelayingInput ndi)) { + continue; // only consider non-delaying inputs, as only these can perform local resets + } + var trans = hypothesis.getTransition(sourceLoc, ndi); + + // Collect self-loops: + if (trans == null || (trans.successor().equals(sourceLoc))) { + loopingInputs.add(sym); + } + } + + return loopingInputs; + } + + @Override + public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, Collection> inputs) { + if (loopInsertPerc == 0) { + return null; // oracle is disabled + } + List> listInputs = new ArrayList<>(inputs); + if (listInputs.stream().noneMatch(s -> s instanceof TimeStepSymbol) || + listInputs.stream().noneMatch(s -> s instanceof TimeoutSymbol)) { + logger.warn("ResetSearchOracle requires inputs to contain TimeoutSymbol and TimeStepSymbol. Will not find counterexample."); + return null; + } + return this.findCexInternal(hypothesis, listInputs); + } + + private @Nullable DefaultQuery, Word>> findCexInternal + (LocalTimerMealy hypothesis, List> inputs) { + + // Retrieve prefixes from state cover, to establish some separation between learner and teacher: + var stateCover = LocalTimerMealyCover.getLocalTimerMealyLocationCover(hypothesis, inputs); + + // Only keep locations that have at least two stable configs (only these can have local resets): + List>> prefixes = new ArrayList<>(); + for (var loc : stateCover.keySet()) { + if (!hypothesis.getSortedTimers(loc).isEmpty() && + hypothesis.getSortedTimers(loc).get(0).initial() > 1) { + prefixes.add(stateCover.get(loc)); + } + } + + // Sort alphabetically, so that experiments are easily reproducible: + prefixes.sort(Comparator.comparing(AbstractPrintable::toString)); + + // Determine number of tested locations: + int randPrefixes = (int) Math.round(testedLocPerc * prefixes.size()); + if (randPrefixes == 0) { + logger.warn("No prefixes tested. Need higher percentage?"); + return null; + } + + List>> chosenPrefixes = RandomUtil.sampleUnique(locPrefixRandom, prefixes, randPrefixes); + + + for (var prefix : chosenPrefixes) { + // Retrieve looping symbols: + var sourceLoc = hypothesis.getSemantics().traceInputs(prefix).getLocation(); + var loopingInputs = getLoopingSymbols(sourceLoc, inputs, hypothesis); + if (loopingInputs.isEmpty()) { + continue; // no loops + } + + // Determine number of looping symbols we want to append: + int randElements = (int) Math.round(loopInsertPerc * loopingInputs.size()); + randElements = Math.min(loopingInputs.size(), randElements); + + List> chosenLoopingInputs = RandomUtil.sampleUnique(new Random(loopingInputSelectionSeed), loopingInputs, randElements); + + + // Create test word: + WordBuilder> wbTestWord = new WordBuilder<>(); + wbTestWord.append(prefix); + wbTestWord.append(new TimeStepSymbol<>()); + wbTestWord.append(Word.fromList(chosenLoopingInputs)); + wbTestWord.append(new TimeoutSymbol<>()); + + // Check if counterexample: + var testWord = wbTestWord.toWord(); + + var hypOutput = hypothesis.getSemantics().computeSuffixOutput(Word.epsilon(), testWord); + var sulOutput = timeOracle.querySuffixOutput(Word.epsilon(), testWord); + if (!hypOutput.equals(sulOutput)) { + return new DefaultQuery<>(testWord, sulOutput); + } + + } + return null; + } + + +} diff --git a/oracles/equivalence-oracles/src/main/java/module-info.java b/oracles/equivalence-oracles/src/main/java/module-info.java index 9c5d3a390b..c7d83f3289 100644 --- a/oracles/equivalence-oracles/src/main/java/module-info.java +++ b/oracles/equivalence-oracles/src/main/java/module-info.java @@ -46,4 +46,5 @@ exports de.learnlib.oracle.equivalence.spa; exports de.learnlib.oracle.equivalence.spmm; exports de.learnlib.oracle.equivalence.vpa; + exports de.learnlib.oracle.equivalence.mmlt; } diff --git a/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/TimedQueryOracle.java b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/TimedQueryOracle.java new file mode 100644 index 0000000000..712bdd231d --- /dev/null +++ b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/TimedQueryOracle.java @@ -0,0 +1,272 @@ +package de.learnlib.oracle.membership; + +import de.learnlib.algorithm.LocalTimerMealyModelParams; +import de.learnlib.oracle.AbstractTimedQueryOracle; +import de.learnlib.query.DefaultQuery; +import de.learnlib.sul.LocalTimerMealySUL; +import net.automatalib.alphabet.time.mmlt.*; +import net.automatalib.automaton.time.mmlt.MealyTimerInfo; +import net.automatalib.word.Word; +import net.automatalib.word.WordBuilder; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Implements a timed query oracle for MMLT learning. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class TimedQueryOracle extends AbstractTimedQueryOracle { + + private final static Logger logger = LoggerFactory.getLogger(TimedQueryOracle.class); + + // List of all possible timer names. + // Each name is assigned at most once while this oracle exists. This ensures globally-unique timer names. + private final List timerNames; + private int timerNameIndex = 0; + + private final LocalTimerMealySUL sul; + private final LocalTimerMealyModelParams modelParams; + + public TimedQueryOracle(LocalTimerMealySUL sul, LocalTimerMealyModelParams modelParams) { + this.sul = sul; + this.modelParams = modelParams; + + this.timerNames = generateTimerNames(); + } + + private List generateTimerNames() { + List names = new ArrayList<>(); + // Add single letter names + for (char c = 'a'; c <= 'z'; c++) { + names.add(String.valueOf(c)); + } + // Add double letter names + for (char c1 = 'a'; c1 <= 'z'; c1++) { + for (char c2 = 'a'; c2 <= 'z'; c2++) { + names.add("" + c1 + c2); + } + } + return names; + } + + /** + * Observes and aggregates any timeouts that occur after providing the given input to the SUL. + * Stops when observing inconsistent behavior. + * + * @param prefix Input to give to the SUL. + * @param maxTotalWaitingTime Maximum time that is waited for timeouts. + * @return Observed timeouts. Empty, if none. + */ + @Override + public TimerQueryResult queryTimers(Word> prefix, long maxTotalWaitingTime) { + this.sul.pre(); + + // Go to location: + this.sul.follow(prefix); + + // Collect timeouts: + TimerQueryResult timers = this.collectTimeouts(maxTotalWaitingTime); + + this.sul.post(); + return timers; + } + + /** + * Identifies the time at which the next known timeout(s) are expected. + * + * @param timeouts Known timeouts + * @param currentTime Current time. + * @return Next timeout time. + */ + private long calcNextExpectedTimeout(List> timeouts, long currentTime) { + if (timeouts.isEmpty()) { + throw new AssertionError(); + } + + long minNext = Long.MAX_VALUE; + for (var to : timeouts) { + long occurrences = currentTime / to.initial(); + long nextOcc = (occurrences + 1) * to.initial(); // time of next occ + + if (nextOcc < minNext) { + minNext = nextOcc; + } + } + + if (minNext == Long.MAX_VALUE) { + throw new AssertionError(); + } + + return minNext; + } + + private String getUniqueTimerName() { + var newTimerName = timerNames.get(timerNameIndex); + this.timerNameIndex += 1; + return newTimerName; + } + + /** + * Identifies timeouts in the current location by waiting at most [maxDelay]. + *

      + * All inferred timers are initially considered periodic. + * Stops when reaching maxTotalWaitingTime OR when an expected timeout does not occur. + * In the latter case, the "aborted" flag is set. + * + * @param maxTotalWaitingTime Maximum time until timeouts are collected. + * @return List of periodic timeouts. Null, if none observed. + */ + private TimerQueryResult collectTimeouts(long maxTotalWaitingTime) { + if (maxTotalWaitingTime < this.modelParams.maxTimeoutWaitingTime()) { + throw new IllegalArgumentException("Timer query waiting time must be at least max. waiting time for a single timeout."); + } + + List> knownTimers = new ArrayList<>(); + + // Wait for the first timeout: + LocalTimerMealyOutputSymbol firstTimeout = this.sul.timeoutStep(this.modelParams.maxTimeoutWaitingTime()); + if (firstTimeout == null) { + return new TimerQueryResult<>(false, Collections.emptyList()); // no timeouts found + } + + if (this.modelParams.outputCombiner().isCombinedSymbol(firstTimeout.getSymbol())) { + logger.warn("Multiple timers expiring at first timeout, automaton may not be minimal."); + } + + knownTimers.add(new MealyTimerInfo<>(getUniqueTimerName(), firstTimeout.getDelay(), firstTimeout.getSymbol())); + + // Wait for further timeouts: + long currentTimeStep = firstTimeout.getDelay(); // already waited for first timeout + + boolean inconsistent = false; + while (currentTimeStep < maxTotalWaitingTime) { + // Identify time of next expected timeout: + long nextExpectedTime = this.calcNextExpectedTimeout(knownTimers, currentTimeStep); + + // Wait either until next timeout OR until maximum waiting time reached: + long nextWaiting = Math.min(nextExpectedTime, maxTotalWaitingTime) - currentTimeStep; + + // Wait until next timeout: + LocalTimerMealyOutputSymbol nextOutput = this.sul.timeoutStep(nextWaiting); + if (nextOutput == null) { + if (nextExpectedTime <= maxTotalWaitingTime) { + // Expected a timeout within max. waiting time but nothing happened: + inconsistent = true; + } + + break; // either max time exceeded OR missing timeout (-> inconsistent) + } + + // Compare observed timeout with expectation: + long nextActualTime = nextOutput.getDelay() + currentTimeStep; + + TimerCheckResult evalResult = evaluateNextTimer(nextActualTime, nextExpectedTime, nextOutput, knownTimers); + if (evalResult.newTimer() != null) { + knownTimers.add(evalResult.newTimer()); + } else if (evalResult.inconsistent()) { + inconsistent = true; + break; + } + + currentTimeStep = nextActualTime; + } + + knownTimers.sort(Comparator.comparingLong(MealyTimerInfo::initial)); + return new TimerQueryResult<>(inconsistent, knownTimers); + } + + private record TimerCheckResult(@Nullable MealyTimerInfo newTimer, boolean inconsistent) { + + } + + private TimerCheckResult evaluateNextTimer(long nextActualTime, long nextExpectedTime, LocalTimerMealyOutputSymbol nextOutput, List> knownTimers) { + if (nextActualTime < nextExpectedTime) { + // A timeout occurred before we expected one -> new timer: + var newTimer = new MealyTimerInfo<>(getUniqueTimerName(), nextActualTime, nextOutput.getSymbol()); + return new TimerCheckResult<>(newTimer, false); + } else if (nextActualTime == nextExpectedTime) { + // Timeout occurred at expected time -> check if matching expected output: + Map expectedOutputs = knownTimers.stream() + .filter(t -> nextExpectedTime % t.initial() == 0) + .map(t -> modelParams.outputCombiner().separateSymbols(t.output())) // separate output of timers with same initial value + .flatMap(Collection::stream) + .collect(Collectors.groupingBy(t -> t, Collectors.counting())); // count occurrences + + Map actualOutputs = this.modelParams.outputCombiner().separateSymbols(nextOutput.getSymbol()) + .stream() + .collect(Collectors.groupingBy(e -> e, Collectors.counting())); + + // Any missing outputs? + boolean missingOutputs = expectedOutputs.keySet().stream() + .anyMatch(o -> actualOutputs.getOrDefault(o, 0L) < expectedOutputs.get(o)); // less than expected + if (missingOutputs) { + // Same time but missing output -> missed location change: + return new TimerCheckResult<>(null, true); + } + + // Any new outputs? + List newOutputs = actualOutputs.keySet().stream() + .filter(o -> expectedOutputs.getOrDefault(o, 0L) < actualOutputs.get(o)) // less than actual + .toList(); + if (!newOutputs.isEmpty()) { + // Same time and more outputs -> add new timer that uses the new outputs: + var newTimer = new MealyTimerInfo<>(getUniqueTimerName(), nextActualTime, this.modelParams.outputCombiner().combineSymbols(newOutputs)); + return new TimerCheckResult<>(newTimer, false); + } + } else { + throw new IllegalStateException(); + } + + return new TimerCheckResult<>(null, false); + } + + + @Override + protected void querySuffixOutputInternal(DefaultQuery, Word>> query) { + + sul.pre(); + sul.follow(query.getPrefix(), this.modelParams.maxTimeoutWaitingTime()); + + // Query the SUL, one symbol at a time: + WordBuilder> wbOutput = new WordBuilder<>(); + for (var s : query.getSuffix()) { + if (s instanceof TimeoutSymbol) { + LocalTimerMealyOutputSymbol output = sul.timeoutStep(this.modelParams.maxTimeoutWaitingTime()); + if (output != null) { + wbOutput.append(output); + } else { + wbOutput.append(new LocalTimerMealyOutputSymbol<>(this.modelParams.silentOutput())); // no output in time -> silent + } + } else if (s instanceof NonDelayingInput ndi) { + LocalTimerMealyOutputSymbol output = sul.step(ndi); + wbOutput.append(output); + } else if (s instanceof TimeStepSequence ws) { + if (ws.getTimeSteps() > 1) { + throw new IllegalArgumentException("Only single wait step allowed in suffix."); + } + + // Wait for a single time step: + LocalTimerMealyOutputSymbol output = sul.timeStep(); + if (output != null) { + wbOutput.append(output); + } else { + wbOutput.append(new LocalTimerMealyOutputSymbol<>(this.modelParams.silentOutput())); // no output in time -> silent + } + + } else { + throw new IllegalArgumentException("Only timeout or untimed symbols allowed in suffix."); + } + } + + + sul.post(); + query.answer(wbOutput.toWord()); + } + +} diff --git a/oracles/pom.xml b/oracles/pom.xml index acc5a8e8f4..9fe0cb944e 100644 --- a/oracles/pom.xml +++ b/oracles/pom.xml @@ -37,5 +37,6 @@ limitations under the License. membership-oracles parallelism property-oracles + symbol-filters diff --git a/oracles/symbol-filters/pom.xml b/oracles/symbol-filters/pom.xml new file mode 100644 index 0000000000..ec0777f2f1 --- /dev/null +++ b/oracles/symbol-filters/pom.xml @@ -0,0 +1,88 @@ + + + 4.0.0 + + + de.learnlib + learnlib-oracles-parent + 0.19.0-SNAPSHOT + ../pom.xml + + + learnlib-symbol-filters + + LearnLib :: Oracles :: Symbol Filters + A collection of symbol filters + + + + + de.learnlib + learnlib-api + + + + + net.automatalib + automata-api + + + + org.checkerframework + checker-qual + + + + org.slf4j + slf4j-api + + + + + de.learnlib.tooling + annotations + + + + + de.learnlib + learnlib-emptiness-oracles + test + + + de.learnlib + learnlib-equivalence-oracles + test + + + de.learnlib.testsupport + learnlib-learning-examples + + + de.learnlib + learnlib-membership-oracles + test + + + + net.automatalib + automata-core + test + + + net.automatalib + automata-modelchecking-ltsmin + test + + + net.automatalib + automata-util + test + + + + org.testng + testng + + + diff --git a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/AcceptAllSymbolFilter.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/AcceptAllSymbolFilter.java new file mode 100644 index 0000000000..c972c55715 --- /dev/null +++ b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/AcceptAllSymbolFilter.java @@ -0,0 +1,24 @@ +package de.learnlib.oracle.symbol_filters; + + +import de.learnlib.symbol_filter.SymbolFilter; +import de.learnlib.symbol_filter.SymbolFilterResponse; +import net.automatalib.word.Word; + +/** + * A pass-through filter that accepts all inputs. + * + * @param Type for symbols in the prefix of the considered states + * @param Type of the queried symbols + */ +public class AcceptAllSymbolFilter implements SymbolFilter { + @Override + public SymbolFilterResponse query(Word prefix, V symbol) { + return SymbolFilterResponse.ACCEPT; + } + + @Override + public void update(Word prefix, V symbol, SymbolFilterResponse response) { + throw new IllegalStateException("Not supported."); + } +} diff --git a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/CachedSymbolFilter.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/CachedSymbolFilter.java new file mode 100644 index 0000000000..7dc0e57d9d --- /dev/null +++ b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/CachedSymbolFilter.java @@ -0,0 +1,44 @@ +package de.learnlib.oracle.symbol_filters; + + +import de.learnlib.symbol_filter.SymbolFilter; +import de.learnlib.symbol_filter.SymbolFilterResponse; +import net.automatalib.word.Word; + +import java.util.HashMap; +import java.util.Map; + +/** + * Wrapper for a symbol filter that caches previous responses + allows caller to update these. + * + * @param Type for symbols in the prefix of the considered states + * @param Type of the queried symbols + */ +public class CachedSymbolFilter implements SymbolFilter { + private final Map, Map> previousResponses; // prefix -> (input -> legal/ignore) + private final SymbolFilter delegate; + + public CachedSymbolFilter(SymbolFilter delegate) { + this.delegate = delegate; + this.previousResponses = new HashMap<>(); + } + + @Override + public SymbolFilterResponse query(Word prefix, V symbol) { + this.previousResponses.putIfAbsent(prefix, new HashMap<>()); + var oldResponse = this.previousResponses.get(prefix).get(symbol); + if (oldResponse != null) { + return (oldResponse) ? SymbolFilterResponse.ACCEPT : SymbolFilterResponse.IGNORE; + } + + var res = delegate.query(prefix, symbol); + this.update(prefix, symbol, res); + return res; + } + + @Override + public void update(Word prefix, V symbol, SymbolFilterResponse response) { + this.previousResponses.putIfAbsent(prefix, new HashMap<>()); + this.previousResponses.get(prefix).put(symbol, (response == SymbolFilterResponse.ACCEPT)); + } +} diff --git a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/IgnoreAllSymbolFilter.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/IgnoreAllSymbolFilter.java new file mode 100644 index 0000000000..aebb9a6b8c --- /dev/null +++ b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/IgnoreAllSymbolFilter.java @@ -0,0 +1,25 @@ +package de.learnlib.oracle.symbol_filters; + +import de.learnlib.symbol_filter.SymbolFilter; +import de.learnlib.symbol_filter.SymbolFilterResponse; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.NonDelayingInput; +import net.automatalib.word.Word; + +/** + * A pass-through filter that ignores all inputs. + * + * @param Type for symbols in the prefix of the considered states + * @param Type of the queried symbols + */ +public class IgnoreAllSymbolFilter implements SymbolFilter { + @Override + public SymbolFilterResponse query(Word prefix, V symbol) { + return SymbolFilterResponse.IGNORE; + } + + @Override + public void update(Word prefix, V symbol, SymbolFilterResponse response) { + throw new IllegalStateException("Not supported."); + } +} diff --git a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/PerfectSymbolFilter.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/PerfectSymbolFilter.java new file mode 100644 index 0000000000..d37da2ec7e --- /dev/null +++ b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/PerfectSymbolFilter.java @@ -0,0 +1,34 @@ +package de.learnlib.oracle.symbol_filters; + + +import de.learnlib.symbol_filter.SymbolFilter; +import de.learnlib.symbol_filter.SymbolFilterResponse; +import net.automatalib.word.Word; + +import java.util.Random; + +/** + * A symbol filter that answers all queries correctly. + * + * @param Type for symbols in the prefix of the considered states + * @param Type of the queried symbols + */ +public abstract class PerfectSymbolFilter implements SymbolFilter { + + protected abstract SymbolFilterResponse isIgnorable(Word prefix, V symbol); + + @Override + public SymbolFilterResponse query(Word prefix, V symbol) { + + if (isIgnorable(prefix, symbol) == SymbolFilterResponse.IGNORE) { + return SymbolFilterResponse.IGNORE; + } else { + return SymbolFilterResponse.ACCEPT; + } + } + + @Override + public void update(Word prefix, V symbol, SymbolFilterResponse response) { + throw new IllegalStateException("Not supported."); + } +} diff --git a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/RandomSymbolFilter.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/RandomSymbolFilter.java new file mode 100644 index 0000000000..df301b5618 --- /dev/null +++ b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/RandomSymbolFilter.java @@ -0,0 +1,52 @@ +package de.learnlib.oracle.symbol_filters; + + +import de.learnlib.symbol_filter.SymbolFilter; +import de.learnlib.symbol_filter.SymbolFilterResponse; +import net.automatalib.word.Word; + +import java.util.Random; + +/** + * A symbol filter that falsely answers a query with a specified probability. + * + * @param Type for symbols in the prefix of the considered states + * @param Type of the queried symbols + */ +public abstract class RandomSymbolFilter implements SymbolFilter { + + private final double inaccurateProb; + private final Random random; + + public RandomSymbolFilter(double inaccurateProb, Random random) { + if (inaccurateProb > 1 || inaccurateProb < 0) { + throw new IllegalArgumentException("Ratios must be between zero and 1 (inclusive)."); + } + + this.inaccurateProb = inaccurateProb; + this.random = random; + } + + protected abstract SymbolFilterResponse isIgnorable(Word prefix, V symbol); + + @Override + public SymbolFilterResponse query(Word prefix, V symbol) { + boolean ignorable = isIgnorable(prefix, symbol) == SymbolFilterResponse.IGNORE; + + // Randomly misclassify: + if (this.random.nextDouble() <= this.inaccurateProb) { + ignorable = !ignorable; + } + + if (ignorable) { + return SymbolFilterResponse.IGNORE; + } else { + return SymbolFilterResponse.ACCEPT; + } + } + + @Override + public void update(Word prefix, V symbol, SymbolFilterResponse response) { + throw new IllegalStateException("Not supported."); + } +} diff --git a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/StatisticsSymbolFilter.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/StatisticsSymbolFilter.java new file mode 100644 index 0000000000..221c989365 --- /dev/null +++ b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/StatisticsSymbolFilter.java @@ -0,0 +1,62 @@ +package de.learnlib.oracle.symbol_filters; + +import de.learnlib.statistic.container.DummyStatsContainer; +import de.learnlib.statistic.container.LearnerStatsProvider; +import de.learnlib.statistic.container.StatsContainer; +import de.learnlib.symbol_filter.SymbolFilter; +import de.learnlib.symbol_filter.SymbolFilterResponse; +import net.automatalib.word.Word; + +/** + * Collects various statistics on symbol filtering, including false accepts + false ignores. + * + * @param Type for symbols in the prefix of the considered states + * @param Type of the queried symbols + */ +public abstract class StatisticsSymbolFilter implements SymbolFilter, LearnerStatsProvider { + + private final SymbolFilter delegate; + private StatsContainer stats = new DummyStatsContainer(); + + public StatisticsSymbolFilter(SymbolFilter delegate, StatsContainer stats) { + this.delegate = delegate; + this.stats = stats; + } + + protected abstract SymbolFilterResponse isIgnorable(Word prefix, V symbol); + + @Override + public SymbolFilterResponse query(Word prefix, V symbol) { + stats.increaseCounter("cnt_isf_queries", "Filter: queries"); + + SymbolFilterResponse filterResponse = this.delegate.query(prefix, symbol); + SymbolFilterResponse expectedResponse = this.isIgnorable(prefix, symbol); + + // Count false ignores, rejects + correct predictions: + if (filterResponse.equals(SymbolFilterResponse.ACCEPT)) { + if (filterResponse.equals(expectedResponse)) { + stats.increaseCounter("cnt_isf_correct_accepts", "Filter: correct accepts"); + } else { + stats.increaseCounter("cnt_isf_false_accepts", "Filter: false accepts"); + } + } else { + if (filterResponse.equals(expectedResponse)) { + stats.increaseCounter("cnt_isf_correct_ignores", "Filter: correct ignores"); + } else { + stats.increaseCounter("cnt_isf_false_ignores", "Filter: false ignores"); + } + } + + return filterResponse; + } + + @Override + public void update(Word prefix, V symbol, SymbolFilterResponse response) { + delegate.update(prefix, symbol, response); + } + + @Override + public void setStatsContainer(StatsContainer container) { + this.stats = container; + } +} diff --git a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/LocalTimerMealyPerfectSymbolFilter.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/LocalTimerMealyPerfectSymbolFilter.java new file mode 100644 index 0000000000..3c6fae763f --- /dev/null +++ b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/LocalTimerMealyPerfectSymbolFilter.java @@ -0,0 +1,32 @@ +package de.learnlib.oracle.symbol_filters.mmlt; + + +import de.learnlib.oracle.symbol_filters.PerfectSymbolFilter; +import de.learnlib.symbol_filter.SymbolFilterResponse; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.NonDelayingInput; +import net.automatalib.automaton.time.mmlt.LocalTimerMealy; +import net.automatalib.word.Word; + +/** + * A symbol filter for MMLTs that correctly accepts and ignores all transitions + * that silently self-loop. + * + * @param Location type + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class LocalTimerMealyPerfectSymbolFilter extends PerfectSymbolFilter, NonDelayingInput> { + + private final LocalTimerMealy automaton; + + public LocalTimerMealyPerfectSymbolFilter(LocalTimerMealy automaton) { + this.automaton = automaton; + } + + @Override + protected SymbolFilterResponse isIgnorable(Word> prefix, NonDelayingInput symbol) { + return LocalTimerMealySymbolFilterUtil.isIgnorable(this.automaton, prefix, symbol); + } + +} diff --git a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/LocalTimerMealyRandomSymbolFilter.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/LocalTimerMealyRandomSymbolFilter.java new file mode 100644 index 0000000000..cd627496f9 --- /dev/null +++ b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/LocalTimerMealyRandomSymbolFilter.java @@ -0,0 +1,35 @@ +package de.learnlib.oracle.symbol_filters.mmlt; + + +import de.learnlib.oracle.symbol_filters.RandomSymbolFilter; +import de.learnlib.symbol_filter.SymbolFilterResponse; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.NonDelayingInput; +import net.automatalib.automaton.time.mmlt.LocalTimerMealy; +import net.automatalib.word.Word; + +import java.util.Random; + +/** + * A symbol filter that falsely answers a query with a specified probability. + * + * @param Location type + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class LocalTimerMealyRandomSymbolFilter extends RandomSymbolFilter, NonDelayingInput> { + + private final LocalTimerMealy automaton; + + public LocalTimerMealyRandomSymbolFilter(LocalTimerMealy automaton, + double inaccurateProb, Random random) { + super(inaccurateProb, random); + this.automaton = automaton; + } + + + @Override + protected SymbolFilterResponse isIgnorable(Word> prefix, NonDelayingInput symbol) { + return LocalTimerMealySymbolFilterUtil.isIgnorable(this.automaton, prefix, symbol); + } +} \ No newline at end of file diff --git a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/LocalTimerMealyStatisticsSymbolFilter.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/LocalTimerMealyStatisticsSymbolFilter.java new file mode 100644 index 0000000000..5323869a03 --- /dev/null +++ b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/LocalTimerMealyStatisticsSymbolFilter.java @@ -0,0 +1,26 @@ +package de.learnlib.oracle.symbol_filters.mmlt; + +import de.learnlib.oracle.symbol_filters.StatisticsSymbolFilter; +import de.learnlib.statistic.container.StatsContainer; +import de.learnlib.symbol_filter.SymbolFilter; +import de.learnlib.symbol_filter.SymbolFilterResponse; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.NonDelayingInput; +import net.automatalib.automaton.time.mmlt.LocalTimerMealy; +import net.automatalib.word.Word; + +public class LocalTimerMealyStatisticsSymbolFilter extends StatisticsSymbolFilter, NonDelayingInput> { + + private final LocalTimerMealy automaton; + + public LocalTimerMealyStatisticsSymbolFilter(LocalTimerMealy automaton, SymbolFilter, NonDelayingInput> delegate, StatsContainer stats) { + super(delegate, stats); + this.automaton = automaton; + } + + @Override + protected SymbolFilterResponse isIgnorable(Word> prefix, NonDelayingInput symbol) { + return LocalTimerMealySymbolFilterUtil.isIgnorable(this.automaton, prefix, symbol); + } + +} diff --git a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/LocalTimerMealySymbolFilterUtil.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/LocalTimerMealySymbolFilterUtil.java new file mode 100644 index 0000000000..07874ba937 --- /dev/null +++ b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/LocalTimerMealySymbolFilterUtil.java @@ -0,0 +1,31 @@ +package de.learnlib.oracle.symbol_filters.mmlt; + +import de.learnlib.symbol_filter.SymbolFilterResponse; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.NonDelayingInput; +import net.automatalib.automaton.time.mmlt.LocalTimerMealy; +import net.automatalib.word.Word; + +class LocalTimerMealySymbolFilterUtil { + + /** + * Returns IGNORE if the provided input triggers a transition that silently self-loops, + * and ACCEPT otherwise. + * + * @param automaton Automaton + * @param prefix State prefix + * @param symbol Input symbol + * @param Location type + * @param Input type for non-delaying inputs + * @param Output symbol type + * @return IGNORE for silent self-loops, ACCEPT otherwise. + */ + static SymbolFilterResponse isIgnorable(LocalTimerMealy automaton, Word> prefix, NonDelayingInput symbol) { + var targetConfig = automaton.getSemantics().traceInputs(prefix); + var trans = automaton.getSemantics().getTransition(targetConfig, symbol); + + boolean ignorable = trans.output().equals(automaton.getSemantics().getSilentOutput()) && targetConfig.equals(trans.target()); + + return ignorable ? SymbolFilterResponse.IGNORE : SymbolFilterResponse.ACCEPT; + } +} diff --git a/oracles/symbol-filters/src/main/java/module-info.java b/oracles/symbol-filters/src/main/java/module-info.java new file mode 100644 index 0000000000..bddf05940f --- /dev/null +++ b/oracles/symbol-filters/src/main/java/module-info.java @@ -0,0 +1,39 @@ +/* Copyright (C) 2013-2025 TU Dortmund University + * This file is part of LearnLib . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This module provides a collection of symbol filters. + *

      + * This module is provided by the following Maven dependency: + *

      + * <dependency>
      + *   <groupId>de.learnlib</groupId>
      + *   <artifactId>learnlib-symbol-filters</artifactId>
      + *   <version>${version}</version>
      + * </dependency>
      + * 
      + */ +open module de.learnlib.oracle.symbol_filters { + + // annotations are 'provided'-scoped and do not need to be loaded at runtime + requires static de.learnlib.tooling.annotation; + requires static org.checkerframework.checker.qual; + requires de.learnlib.api; + requires net.automatalib.api; + + exports de.learnlib.oracle.symbol_filters; + exports de.learnlib.oracle.symbol_filters.mmlt; +} diff --git a/pom.xml b/pom.xml index 6245bdfcef..ac511ad06e 100644 --- a/pom.xml +++ b/pom.xml @@ -213,9 +213,9 @@ limitations under the License. UTF-8 UTF-8 - 11 - 11 - 11 + 17 + 17 + 17 @@ -528,6 +528,11 @@ limitations under the License. learnlib-parallelism ${project.version} + + de.learnlib + learnlib-symbol-filters + ${project.version} + de.learnlib learnlib-property-oracles diff --git a/test-support/learning-examples/pom.xml b/test-support/learning-examples/pom.xml index 87a2d91f6e..a71c376419 100644 --- a/test-support/learning-examples/pom.xml +++ b/test-support/learning-examples/pom.xml @@ -75,5 +75,9 @@ limitations under the License. org.testng testng + + net.automatalib + automata-serialization-dot + diff --git a/test-support/learning-examples/src/main/java/de/learnlib/testsupport/example/mmlt/LocalTimerMealyExamples.java b/test-support/learning-examples/src/main/java/de/learnlib/testsupport/example/mmlt/LocalTimerMealyExamples.java new file mode 100644 index 0000000000..d7a2b894ac --- /dev/null +++ b/test-support/learning-examples/src/main/java/de/learnlib/testsupport/example/mmlt/LocalTimerMealyExamples.java @@ -0,0 +1,133 @@ +package de.learnlib.testsupport.example.mmlt; + +import de.learnlib.algorithm.LocalTimerMealyModelParams; +import net.automatalib.automaton.time.impl.mmlt.StringSymbolCombiner; +import net.automatalib.serialization.dot.LocalTimerMealyGraphvizParser; +import net.automatalib.util.automaton.mmlt.LocalTimerMealyUtil; + +import java.util.List; + +public class LocalTimerMealyExamples { + + public static List> getAll() { + return List.of( + HVAC(), SCTP(), SensorCollector(), WM(), Oven(), WSN()); + } + + /** + * Returns an MMLT model of an HVAC system. + *

      + * The system has been adapted from: Taylor and Taylor: Patterns in the Machine + * + * @return LocalTimerMealyModel + */ + public static LocalTimerMealyModel HVAC() { + return automatonFromFile("HVAC"); + } + + /** + * Returns an MMLT model of an endpoint in the stream control and transmission protocol. + *

      + * The model has been adapted from: Stewart et al.: Stream Control Transmission Protocol (RFC 9260, Figure 3) + * + * @return LocalTimerMealyModel + */ + public static LocalTimerMealyModel SCTP() { + return automatonFromFile("SCTP"); + } + + /** + * Returns an MMLT model of a sensor collector. + *

      + * The sensor measures particulate matter and ambient noise. + * The measurement program automatically ends after some time. The program may be restarted at any time. + * Alternatively, a self-check program can be entered. This also ends after some time and may be aborted. + * At the end of either program, the collected data may be retrieved. + * + * @return LocalTimerMealyModel + */ + public static LocalTimerMealyModel SensorCollector() { + return automatonFromFile("sensor_collector"); + } + + /** + * Returns an MMLT model of a washing machine. + *

      + * The machine is initially off. After powering it on and closing the door, + * the user can start either the short or the normal program. An open + * door prevents starting and triggers a warning. Not choosing a program within 10 seconds turns the machine off. + *

      + * In normal model, the machine fills the drum, heats the water, and starts the main wash. During this wash, + * it regularly adjusts the drum speed and maintains temperature. After 2 hours, + * the water is drained and the drum is spun at full speed for some time. Afterwards the remaining water is drained. + * The short program makes less adjustments, so that a wash ends after 1 hour. + *

      + * Both programs are interrupted when a leak is detected. Normal mode may also be interrupted by "stop". + * This drains the drum immediately. Once done, the door is unlocked, a message is shown, and the machine + * beeps repeatedly until the user presses any button or opens the door. + * + * @return LocalTimerMealyModel + */ + public static LocalTimerMealyModel WM() { + return automatonFromFile("WM"); + } + + /** + * Returns an MMLT model of an oven with a time-controlled baking program. + *

      + * After powering the oven on, the oven remains idle until the program is started. + * During the program, the oven regularly measures and adjusts the temperature. + * At the end of the program, an alarm sounds. Then, the user may extend the program. + * If not extended, the program ends either when the user opens the door, presses a button, or a timeout occurs. + * + * @return LocalTimerMealyModel + */ + public static LocalTimerMealyModel Oven() { + return automatonFromFile("Oven"); + } + + /** + * Returns an MMLT model of a wireless sensor node. + *

      + * The node regularly collects and transmits data. If the battery is low, no data is transmitted. Then, + * a user may collect the data manually. + * The node can be shut down at any time. If the battery is empty, it is shut down automatically. + * + * @return LocalTimerMealyModel + */ + public static LocalTimerMealyModel WSN() { + return automatonFromFile("WSN"); + } + + // =================================== + + /** + * Loads the automaton model with the provided resource name. + * Also infers suitable model parameters: + * - Maximum time to wait for a timeout in any configuration. + * - Maximum waiting time for timer queries. This must be at least the max. time for a timeout. + * We choose twice the maximum value of any timer in the model. When inferring a timer with one of these values, + * the learner has the chance to observe its timeout at least twice. This increases the chance of observing non-periodic behavior. + * + */ + static LocalTimerMealyModel automatonFromFile(String name) { + + net.automatalib.automaton.time.mmlt.LocalTimerMealy automaton; + try (var modelResource = LocalTimerMealyExamples.class.getResourceAsStream("/mmlt/" + name + ".dot")) { + automaton = LocalTimerMealyGraphvizParser.parseLocalTimerMealy(modelResource, "void", StringSymbolCombiner.getInstance()); + } catch (Exception ex) { + throw new RuntimeException("Failed to load automaton from resource " + name, ex); + } + + long maxTimeoutDelay = LocalTimerMealyUtil.getMaximumTimeoutDelay(automaton); + long maxTimerQueryWaitingFinal = LocalTimerMealyUtil.getMaximumInitialTimerValue(automaton) * 2; + + if (name.contains("SCTP")) { + maxTimerQueryWaitingFinal = 9000; // SCTP needs more waiting time + } + + return new LocalTimerMealyModel<>(name, automaton, new LocalTimerMealyModelParams<>("void", maxTimeoutDelay, maxTimerQueryWaitingFinal, StringSymbolCombiner.getInstance())); + } + + +} diff --git a/test-support/learning-examples/src/main/java/de/learnlib/testsupport/example/mmlt/LocalTimerMealyModel.java b/test-support/learning-examples/src/main/java/de/learnlib/testsupport/example/mmlt/LocalTimerMealyModel.java new file mode 100644 index 0000000000..07cba765ec --- /dev/null +++ b/test-support/learning-examples/src/main/java/de/learnlib/testsupport/example/mmlt/LocalTimerMealyModel.java @@ -0,0 +1,20 @@ +package de.learnlib.testsupport.example.mmlt; + +import de.learnlib.algorithm.LocalTimerMealyModelParams; +import net.automatalib.automaton.time.mmlt.LocalTimerMealy; + +/** + * Convenience class for storing a name, an automaton and model parameters. + * + * @param name Automaton name + * @param automaton MMLT + * @param params Model parameters + * @param Location type + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public record LocalTimerMealyModel(String name, + LocalTimerMealy automaton, + LocalTimerMealyModelParams params) { + +} diff --git a/test-support/learning-examples/src/main/java/module-info.java b/test-support/learning-examples/src/main/java/module-info.java index ac70dd8719..222665195e 100644 --- a/test-support/learning-examples/src/main/java/module-info.java +++ b/test-support/learning-examples/src/main/java/module-info.java @@ -37,6 +37,7 @@ requires net.automatalib.serialization.learnlibv2; requires net.automatalib.util; requires org.slf4j; + requires net.automatalib.serialization.dot; // annotations are 'provided'-scoped and do not need to be loaded at runtime requires static org.checkerframework.checker.qual; @@ -50,4 +51,5 @@ exports de.learnlib.testsupport.example.spmm; exports de.learnlib.testsupport.example.sst; exports de.learnlib.testsupport.example.vpa; + exports de.learnlib.testsupport.example.mmlt; } diff --git a/test-support/learning-examples/src/main/resources/mmlt/HVAC.dot b/test-support/learning-examples/src/main/resources/mmlt/HVAC.dot new file mode 100644 index 0000000000..5c176a909b --- /dev/null +++ b/test-support/learning-examples/src/main/resources/mmlt/HVAC.dot @@ -0,0 +1,61 @@ +// Model of an HVAC system +// Adapted from Taylor and Taylor: Patterns in the Machine +digraph g { + + s0 [shape="circle"]; + s1 [timers="a=2000" shape="circle"]; + s2 [shape="circle"]; + s3 [shape="circle"]; + s4 [timers="a=2000" shape="circle"]; + s5 [shape="circle"]; + s6 [timers="a=2000" shape="circle"]; + s7 [timers="a=2000" shape="circle"]; + s8 [timers="a=2000" shape="circle"]; + s9 [timers="a=2000" shape="circle"]; + s10 [shape="circle"]; + s0 -> s1 [resets="a" label="Supplementing.Active / FromTransition.init"]; + s0 -> s2 [label="Supplementing.Inactive / Activity.initializeActive"]; + s1 -> s10 [label="Activity.OffMode / Stage.init"]; + s1 -> s2 [label="FromTransition.Completed / void"]; + s1 -> s1 [resets="a" label="to[a] / FromTransition.check"]; + s2 -> s10 [label="Activity.OffMode / Stage.init"]; + s2 -> s3 [label="Capacity.Excess / void"]; + s2 -> s5 [label="Capacity.NeedMore / Supplementing.enter"]; + s2 -> s6 [resets="a" label="OffCycle.IsStartInOffCycle / OffCycle.startCycling,OffCycle.startOffTime,Stage.startingOff"]; + s2 -> s8 [resets="a" label="OnCycle.IsStartInOnCycle / OnCycle.startCycling,OnCycle.startOnTime,Stage.startingOn"]; + s3 -> s10 [label="Activity.OffMode / Stage.init"]; + s3 -> s4 [resets="a" label="Activity.OnRequest / void"]; + s3 -> s10 [label="Supplementing.Inactive / Stage.shutdown"]; + s4 -> s10 [label="Activity.OffMode / Stage.init"]; + s4 -> s10 [label="BackTransition.Completed / Stage.notifyLower,Stage.shutdown"]; + s4 -> s4 [resets="a" label="to[a] / BackTransition.check"]; + s5 -> s10 [label="Activity.OffMode / Stage.init"]; + s5 -> s2 [label="Capacity.NeedLess / Supplementing.ext"]; + s6 -> s10 [label="Activity.OffMode / Stage.init"]; + s6 -> s3 [label="Capacity.Excess / void"]; + s6 -> s5 [label="Capacity.NeedMore / Supplementing.enter"]; + s6 -> s8 [resets="a" label="OffCycle.OffTimeExpired / OnCycle.startOnTime,Stage.startingOn"]; + s6 -> s7 [resets="a" label="OffCycle.StartingOffTimeExpired / Stage.off"]; + s6 -> s6 [resets="a" label="to[a] / OffCycle.checkStartingOffTime"]; + s7 -> s10 [label="Activity.OffMode / Stage.init"]; + s7 -> s3 [label="Capacity.Excess / void"]; + s7 -> s5 [label="Capacity.NeedMore / Supplementing.enter"]; + s7 -> s8 [resets="a" label="OffCycle.OffTimeExpired / OnCycle.startOnTime,Stage.startingOn"]; + s7 -> s7 [resets="a" label="to[a] / OffCycle.checkOffTime"]; + s8 -> s10 [label="Activity.OffMode / Stage.init"]; + s8 -> s3 [label="Capacity.Excess / void"]; + s8 -> s5 [label="Capacity.NeedMore / Supplementing.enter"]; + s8 -> s6 [resets="a" label="OnCycle.OnTimeExpired / OffCycle.startOffTime,Stage.startingOff"]; + s8 -> s9 [resets="a" label="OnCycle.StartingOnTimeExpired / Stage.on"]; + s8 -> s8 [resets="a" label="to[a] / OnCycle.checkStartingOnTime"]; + s9 -> s10 [label="Activity.OffMode / Stage.init"]; + s9 -> s3 [label="Capacity.Excess / void"]; + s9 -> s5 [label="Capacity.NeedMore / Supplementing.enter"]; + s9 -> s6 [resets="a" label="OnCycle.OnTimeExpired / OffCycle.startOffTime,Stage.startingOff"]; + s9 -> s9 [resets="a" label="to[a] / OnCycle.checkOnTime"]; + s10 -> s0 [label="Activity.OnRequest / void"]; + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s10; + +} diff --git a/test-support/learning-examples/src/main/resources/mmlt/Oven.dot b/test-support/learning-examples/src/main/resources/mmlt/Oven.dot new file mode 100644 index 0000000000..4de25c32d9 --- /dev/null +++ b/test-support/learning-examples/src/main/resources/mmlt/Oven.dot @@ -0,0 +1,26 @@ +// Model of an oven with a time-controlled baking program. +// After powering the oven on, the oven remains idle until the program is started. +// During the program, the oven regularly measures and adjusts the temperature. +// At the end of the program, an alarm sounds. Then, the user may extend the program. +// If not extended, the program ends either when the user opens the door, presses a button, or a timeout occurs. +digraph g { + + s0 [shape="circle"]; + s1 [timers="a=3500,b=300000" shape="circle"]; + s2 [timers="a=5000" shape="circle"]; + s3 [shape="circle"]; + s0 -> s3 [label="User.Power / void"]; + s0 -> s1 [resets="a,b" label="User.Start / Temp.on"]; + s1 -> s0 [label="User.Stop / Temp.off"]; + s1 -> s1 [resets="a" label="to[a] / Temp.adjust"]; + s1 -> s2 [resets="a" label="to[b] / Alarm.start,Temp.off"]; + s2 -> s1 [resets="a,b" label="User.Extend / Alarm.stop,Temp.on"]; + s2 -> s0 [label="User.Open / Alarm.stop"]; + s2 -> s0 [label="User.Stop / Alarm.stop"]; + s2 -> s0 [label="to[a] / Alarm.stop"]; + s3 -> s0 [label="User.Power / void"]; + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s3; + +} diff --git a/test-support/learning-examples/src/main/resources/mmlt/SCTP.dot b/test-support/learning-examples/src/main/resources/mmlt/SCTP.dot new file mode 100644 index 0000000000..9f2d239ad9 --- /dev/null +++ b/test-support/learning-examples/src/main/resources/mmlt/SCTP.dot @@ -0,0 +1,225 @@ +// Model of the endpoint association in the SCTP protocol. +// Adapted from Stewart et al.: Stream Control Transmission Protocol (RFC 9260, Figure 3) +digraph g { + + s0 [shape="circle" ]; + s1 [shape="circle" ]; + s2 [timers="a=1000" shape="circle" ]; + s3 [timers="a=1000" shape="circle" ]; + s4 [timers="a=1000" shape="circle" ]; + s5 [timers="a=1000" shape="circle" ]; + s6 [timers="a=1000" shape="circle" ]; + s7 [timers="a=1000" shape="circle" ]; + s8 [timers="a=1000" shape="circle" ]; + s9 [timers="a=1000" shape="circle" ]; + s10 [timers="a=1000" shape="circle" ]; + s11 [shape="circle" ]; + s12 [timers="a=1000" shape="circle" ]; + s13 [timers="a=1000" shape="circle" ]; + s14 [timers="a=1000" shape="circle" ]; + s15 [timers="a=1000" shape="circle" ]; + s16 [timers="a=1000" shape="circle" ]; + s17 [timers="a=1000" shape="circle" ]; + s18 [timers="a=1000" shape="circle" ]; + s19 [timers="a=1000" shape="circle" ]; + s20 [timers="a=1000" shape="circle" ]; + s21 [timers="a=1000" shape="circle" ]; + s22 [timers="a=1000" shape="circle" ]; + s23 [timers="a=1000" shape="circle" ]; + s24 [timers="a=1000" shape="circle" ]; + s25 [timers="a=1000" shape="circle" ]; + s26 [timers="a=1000" shape="circle" ]; + s27 [timers="a=1000" shape="circle" ]; + s28 [timers="a=1000" shape="circle" ]; + s29 [timers="a=1000" shape="circle" ]; + s30 [timers="a=1000" shape="circle" ]; + s31 [shape="circle" ]; + s32 [timers="a=1000" shape="circle" ]; + s33 [timers="a=1000" shape="circle" ]; + s34 [timers="a=1000" shape="circle" ]; + s35 [timers="a=1000" shape="circle" ]; + s36 [timers="a=1000" shape="circle" ]; + s37 [timers="a=1000" shape="circle" ]; + s38 [timers="a=1000" shape="circle" ]; + s39 [timers="a=1000" shape="circle" ]; + s40 [shape="circle" label="Closed"]; + s0 -> s40 [label="Receive.Abort / void"]; + s0 -> s1 [label="Receive.Shutdown / void"]; + s0 -> s40 [label="User.Abort / Send.abort"]; + s0 -> s11 [label="User.Shutdown / void"]; + s1 -> s2 [resets="a" label="no_outstanding / Send.shutdown_ack"]; + s1 -> s40 [label="Receive.Abort / void"]; + s1 -> s40 [label="User.Abort / Send.abort"]; + s2 -> s40 [label="Receive.Abort / void"]; + s2 -> s40 [label="Receive.Shutdown_ack / Send.shutdown_complete"]; + s2 -> s40 [label="Receive.Shutdown_complete / void"]; + s2 -> s40 [label="User.Abort / Send.abort"]; + s2 -> s3 [resets="a" label="to[a] / Send.shutdown_ack"]; + s3 -> s40 [label="Receive.Abort / void"]; + s3 -> s40 [label="Receive.Shutdown_ack / Send.shutdown_complete"]; + s3 -> s40 [label="Receive.Shutdown_complete / void"]; + s3 -> s40 [label="User.Abort / Send.abort"]; + s3 -> s4 [resets="a" label="to[a] / Send.shutdown_ack"]; + s4 -> s40 [label="Receive.Abort / void"]; + s4 -> s40 [label="Receive.Shutdown_ack / Send.shutdown_complete"]; + s4 -> s40 [label="Receive.Shutdown_complete / void"]; + s4 -> s40 [label="User.Abort / Send.abort"]; + s4 -> s5 [resets="a" label="to[a] / Send.shutdown_ack"]; + s5 -> s40 [label="Receive.Abort / void"]; + s5 -> s40 [label="Receive.Shutdown_ack / Send.shutdown_complete"]; + s5 -> s40 [label="Receive.Shutdown_complete / void"]; + s5 -> s40 [label="User.Abort / Send.abort"]; + s5 -> s6 [resets="a" label="to[a] / Send.shutdown_ack"]; + s6 -> s40 [label="Receive.Abort / void"]; + s6 -> s40 [label="Receive.Shutdown_ack / Send.shutdown_complete"]; + s6 -> s40 [label="Receive.Shutdown_complete / void"]; + s6 -> s40 [label="User.Abort / Send.abort"]; + s6 -> s7 [resets="a" label="to[a] / Send.shutdown_ack"]; + s7 -> s40 [label="Receive.Abort / void"]; + s7 -> s40 [label="Receive.Shutdown_ack / Send.shutdown_complete"]; + s7 -> s40 [label="Receive.Shutdown_complete / void"]; + s7 -> s40 [label="User.Abort / Send.abort"]; + s7 -> s8 [resets="a" label="to[a] / Send.shutdown_ack"]; + s8 -> s40 [label="Receive.Abort / void"]; + s8 -> s40 [label="Receive.Shutdown_ack / Send.shutdown_complete"]; + s8 -> s40 [label="Receive.Shutdown_complete / void"]; + s8 -> s40 [label="User.Abort / Send.abort"]; + s8 -> s9 [resets="a" label="to[a] / Send.shutdown_ack"]; + s9 -> s40 [label="Receive.Abort / void"]; + s9 -> s40 [label="Receive.Shutdown_ack / Send.shutdown_complete"]; + s9 -> s40 [label="Receive.Shutdown_complete / void"]; + s9 -> s40 [label="User.Abort / Send.abort"]; + s9 -> s10 [resets="a" label="to[a] / Send.shutdown_ack"]; + s10 -> s40 [label="Receive.Abort / void"]; + s10 -> s40 [label="Receive.Shutdown_ack / Send.shutdown_complete"]; + s10 -> s40 [label="Receive.Shutdown_complete / void"]; + s10 -> s40 [label="User.Abort / Send.abort"]; + s10 -> s31 [label="to[a] / User.error"]; + s11 -> s12 [resets="a" label="no_outstanding / Send.shutdown"]; + s11 -> s40 [label="Receive.Abort / void"]; + s11 -> s40 [label="User.Abort / Send.abort"]; + s12 -> s40 [label="Receive.Abort / void"]; + s12 -> s2 [resets="a" label="Receive.Shutdown / Send.shutdown_ack"]; + s12 -> s40 [label="Receive.Shutdown_ack / Send.shutdown_complete"]; + s12 -> s40 [label="User.Abort / Send.abort"]; + s12 -> s13 [resets="a" label="to[a] / Send.shutdown"]; + s13 -> s40 [label="Receive.Abort / void"]; + s13 -> s2 [resets="a" label="Receive.Shutdown / Send.shutdown_ack"]; + s13 -> s40 [label="Receive.Shutdown_ack / Send.shutdown_complete"]; + s13 -> s40 [label="User.Abort / Send.abort"]; + s13 -> s14 [resets="a" label="to[a] / Send.shutdown"]; + s14 -> s40 [label="Receive.Abort / void"]; + s14 -> s2 [resets="a" label="Receive.Shutdown / Send.shutdown_ack"]; + s14 -> s40 [label="Receive.Shutdown_ack / Send.shutdown_complete"]; + s14 -> s40 [label="User.Abort / Send.abort"]; + s14 -> s15 [resets="a" label="to[a] / Send.shutdown"]; + s15 -> s40 [label="Receive.Abort / void"]; + s15 -> s2 [resets="a" label="Receive.Shutdown / Send.shutdown_ack"]; + s15 -> s40 [label="Receive.Shutdown_ack / Send.shutdown_complete"]; + s15 -> s40 [label="User.Abort / Send.abort"]; + s15 -> s16 [resets="a" label="to[a] / Send.shutdown"]; + s16 -> s40 [label="Receive.Abort / void"]; + s16 -> s2 [resets="a" label="Receive.Shutdown / Send.shutdown_ack"]; + s16 -> s40 [label="Receive.Shutdown_ack / Send.shutdown_complete"]; + s16 -> s40 [label="User.Abort / Send.abort"]; + s16 -> s17 [resets="a" label="to[a] / Send.shutdown"]; + s17 -> s40 [label="Receive.Abort / void"]; + s17 -> s2 [resets="a" label="Receive.Shutdown / Send.shutdown_ack"]; + s17 -> s40 [label="Receive.Shutdown_ack / Send.shutdown_complete"]; + s17 -> s40 [label="User.Abort / Send.abort"]; + s17 -> s18 [resets="a" label="to[a] / Send.shutdown"]; + s18 -> s40 [label="Receive.Abort / void"]; + s18 -> s2 [resets="a" label="Receive.Shutdown / Send.shutdown_ack"]; + s18 -> s40 [label="Receive.Shutdown_ack / Send.shutdown_complete"]; + s18 -> s40 [label="User.Abort / Send.abort"]; + s18 -> s19 [resets="a" label="to[a] / Send.shutdown"]; + s19 -> s40 [label="Receive.Abort / void"]; + s19 -> s2 [resets="a" label="Receive.Shutdown / Send.shutdown_ack"]; + s19 -> s40 [label="Receive.Shutdown_ack / Send.shutdown_complete"]; + s19 -> s40 [label="User.Abort / Send.abort"]; + s19 -> s20 [resets="a" label="to[a] / Send.shutdown"]; + s20 -> s40 [label="Receive.Abort / void"]; + s20 -> s2 [resets="a" label="Receive.Shutdown / Send.shutdown_ack"]; + s20 -> s40 [label="Receive.Shutdown_ack / Send.shutdown_complete"]; + s20 -> s40 [label="User.Abort / Send.abort"]; + s20 -> s31 [label="to[a] / User.error"]; + s21 -> s40 [label="Receive.Abort / void"]; + s21 -> s22 [resets="a" label="Receive.Init_ack / Send.cookie_echo"]; + s21 -> s40 [label="User.Abort / Send.abort"]; + s21 -> s32 [resets="a" label="to[a] / Send.init"]; + s22 -> s40 [label="Receive.Abort / void"]; + s22 -> s0 [label="Receive.Cookie_ack / void"]; + s22 -> s40 [label="User.Abort / Send.abort"]; + s22 -> s23 [resets="a" label="to[a] / Send.cookie_echo"]; + s23 -> s40 [label="Receive.Abort / void"]; + s23 -> s0 [label="Receive.Cookie_ack / void"]; + s23 -> s40 [label="User.Abort / Send.abort"]; + s23 -> s24 [resets="a" label="to[a] / Send.cookie_echo"]; + s24 -> s40 [label="Receive.Abort / void"]; + s24 -> s0 [label="Receive.Cookie_ack / void"]; + s24 -> s40 [label="User.Abort / Send.abort"]; + s24 -> s25 [resets="a" label="to[a] / Send.cookie_echo"]; + s25 -> s40 [label="Receive.Abort / void"]; + s25 -> s0 [label="Receive.Cookie_ack / void"]; + s25 -> s40 [label="User.Abort / Send.abort"]; + s25 -> s26 [resets="a" label="to[a] / Send.cookie_echo"]; + s26 -> s40 [label="Receive.Abort / void"]; + s26 -> s0 [label="Receive.Cookie_ack / void"]; + s26 -> s40 [label="User.Abort / Send.abort"]; + s26 -> s27 [resets="a" label="to[a] / Send.cookie_echo"]; + s27 -> s40 [label="Receive.Abort / void"]; + s27 -> s0 [label="Receive.Cookie_ack / void"]; + s27 -> s40 [label="User.Abort / Send.abort"]; + s27 -> s28 [resets="a" label="to[a] / Send.cookie_echo"]; + s28 -> s40 [label="Receive.Abort / void"]; + s28 -> s0 [label="Receive.Cookie_ack / void"]; + s28 -> s40 [label="User.Abort / Send.abort"]; + s28 -> s29 [resets="a" label="to[a] / Send.cookie_echo"]; + s29 -> s40 [label="Receive.Abort / void"]; + s29 -> s0 [label="Receive.Cookie_ack / void"]; + s29 -> s40 [label="User.Abort / Send.abort"]; + s29 -> s30 [resets="a" label="to[a] / Send.cookie_echo"]; + s30 -> s40 [label="Receive.Abort / void"]; + s30 -> s0 [label="Receive.Cookie_ack / void"]; + s30 -> s40 [label="User.Abort / Send.abort"]; + s30 -> s31 [label="to[a] / User.error"]; + s32 -> s40 [label="Receive.Abort / void"]; + s32 -> s22 [resets="a" label="Receive.Init_ack / Send.cookie_echo"]; + s32 -> s40 [label="User.Abort / Send.abort"]; + s32 -> s33 [resets="a" label="to[a] / Send.init"]; + s33 -> s40 [label="Receive.Abort / void"]; + s33 -> s22 [resets="a" label="Receive.Init_ack / Send.cookie_echo"]; + s33 -> s40 [label="User.Abort / Send.abort"]; + s33 -> s34 [resets="a" label="to[a] / Send.init"]; + s34 -> s40 [label="Receive.Abort / void"]; + s34 -> s22 [resets="a" label="Receive.Init_ack / Send.cookie_echo"]; + s34 -> s40 [label="User.Abort / Send.abort"]; + s34 -> s35 [resets="a" label="to[a] / Send.init"]; + s35 -> s40 [label="Receive.Abort / void"]; + s35 -> s22 [resets="a" label="Receive.Init_ack / Send.cookie_echo"]; + s35 -> s40 [label="User.Abort / Send.abort"]; + s35 -> s36 [resets="a" label="to[a] / Send.init"]; + s36 -> s40 [label="Receive.Abort / void"]; + s36 -> s22 [resets="a" label="Receive.Init_ack / Send.cookie_echo"]; + s36 -> s40 [label="User.Abort / Send.abort"]; + s36 -> s37 [resets="a" label="to[a] / Send.init"]; + s37 -> s40 [label="Receive.Abort / void"]; + s37 -> s22 [resets="a" label="Receive.Init_ack / Send.cookie_echo"]; + s37 -> s40 [label="User.Abort / Send.abort"]; + s37 -> s38 [resets="a" label="to[a] / Send.init"]; + s38 -> s40 [label="Receive.Abort / void"]; + s38 -> s22 [resets="a" label="Receive.Init_ack / Send.cookie_echo"]; + s38 -> s40 [label="User.Abort / Send.abort"]; + s38 -> s39 [resets="a" label="to[a] / Send.init"]; + s39 -> s40 [label="Receive.Abort / void"]; + s39 -> s22 [resets="a" label="Receive.Init_ack / Send.cookie_echo"]; + s39 -> s40 [label="User.Abort / Send.abort"]; + s39 -> s31 [label="to[a] / User.error"]; + s40 -> s40 [label="Receive.Init / Send.init_ack"]; + s40 -> s0 [label="Receive.Valid / Send.cookie_ack"]; + s40 -> s21 [resets="a" label="User.Associate / Send.init"]; + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s40; + +} diff --git a/test-support/learning-examples/src/main/resources/mmlt/WM.dot b/test-support/learning-examples/src/main/resources/mmlt/WM.dot new file mode 100644 index 0000000000..89f9ade3e3 --- /dev/null +++ b/test-support/learning-examples/src/main/resources/mmlt/WM.dot @@ -0,0 +1,196 @@ +// Model of a washing machine +// The machine is initially off. After powering it on and closing the door, +// the user can start either the short or the normal program. An open +// door prevents starting and triggers a warning. Not choosing a program within 10 seconds turns the machine off. +// In normal model, the machine fills the drum, heats the water, and starts the main wash. During this wash, +// it regularly adjusts the drum speed and maintains temperature. After 2 hours, +// the water is drained and the drum is spun at full speed for some time. Afterwards the remaining water is drained. +// The short program makes less adjustments, so that a wash ends after 1 hour. +// Both programs are interrupted when a leak is detected. Normal mode may also be interrupted by "stop". +// This drains the drum immediately. Once done, the door is unlocked, a message is shown, and the machine +// beeps repeatedly until the user presses any button or opens the door. +digraph g { + + s0 [timers="a=10000" shape="circle"]; + s1 [timers="a=10000" shape="circle"]; + s2 [shape="circle"]; + s3 [shape="circle"]; + s4 [timers="a=2000" shape="circle"]; + s5 [shape="circle"]; + s6 [timers="a=360000" shape="circle"]; + s7 [timers="a=360000" shape="circle"]; + s8 [timers="a=360000" shape="circle"]; + s9 [timers="a=360000" shape="circle"]; + s10 [timers="a=360000" shape="circle"]; + s11 [timers="a=360000" shape="circle"]; + s12 [timers="a=360000" shape="circle"]; + s13 [timers="a=360000" shape="circle"]; + s14 [timers="a=360000" shape="circle"]; + s15 [timers="a=360000" shape="circle"]; + s16 [timers="a=360000" shape="circle"]; + s17 [timers="a=360000" shape="circle"]; + s18 [timers="a=360000" shape="circle"]; + s19 [timers="a=360000" shape="circle"]; + s20 [timers="a=360000" shape="circle"]; + s21 [timers="a=360000" shape="circle"]; + s22 [timers="a=360000" shape="circle"]; + s23 [timers="a=360000" shape="circle"]; + s24 [timers="a=360000" shape="circle"]; + s25 [timers="a=360000" shape="circle"]; + s26 [shape="circle"]; + s27 [timers="a=180000" shape="circle"]; + s28 [shape="circle"]; + s29 [shape="circle"]; + s30 [timers="a=10000" shape="circle"]; + s31 [shape="circle"]; + s32 [timers="a=3600000" shape="circle"]; + s33 [shape="circle"]; + s34 [shape="circle"]; + s0 -> s30 [resets="a" label="Buttons.Start_short / void"]; + s0 -> s1 [resets="a" label="Buttons.Start / void"]; + s0 -> s34 [label="to[a] / Display.off"]; + s1 -> s2 [label="Door.Closed / Display.rem_normal,Door.lock,PumpIn.start"]; + s1 -> s0 [resets="a" label="Door.Open / Display.door_warning"]; + s1 -> s34 [label="to[a] / Display.off"]; + s2 -> s3 [label="Buttons.Stop / PumpIn.stop,PumpOut.start"]; + s2 -> s29 [label="Water.Leak / Display.alarm,PumpIn.stop"]; + s2 -> s5 [label="Water.Full / Heater.start,PumpIn.stop"]; + s3 -> s4 [resets="a" label="Water.Empty / Display.done,Door.unlock"]; + s4 -> s0 [resets="a" label="Buttons.Start_short / void"]; + s4 -> s0 [resets="a" label="Buttons.On / void"]; + s4 -> s0 [resets="a" label="Buttons.Start / void"]; + s4 -> s0 [resets="a" label="Buttons.Stop / void"]; + s4 -> s0 [resets="a" label="Door.Open / void"]; + s4 -> s4 [resets="a" label="to[a] / Beeper.beep"]; + s5 -> s3 [label="Buttons.Stop / Heater.stop,PumpOut.start"]; + s5 -> s29 [label="Water.Leak / Display.alarm,Heater.stop"]; + s5 -> s6 [resets="a" label="Water.Temp_ok / Detergent.add,Drum.normal_speed,Heater.stop"]; + s6 -> s3 [label="Buttons.Stop / Drum.stop,Heater.stop,PumpOut.start"]; + s6 -> s29 [label="Water.Leak / Display.alarm,Drum.stop,Heater.stop"]; + s6 -> s6 [label="Water.Temp_low / Heater.start"]; + s6 -> s6 [label="Water.Temp_ok / Heater.stop"]; + s6 -> s7 [resets="a" label="to[a] / Drum.change_speed"]; + s7 -> s3 [label="Buttons.Stop / Drum.stop,Heater.stop,PumpOut.start"]; + s7 -> s29 [label="Water.Leak / Display.alarm,Drum.stop,Heater.stop"]; + s7 -> s7 [label="Water.Temp_low / Heater.start"]; + s7 -> s7 [label="Water.Temp_ok / Heater.stop"]; + s7 -> s8 [resets="a" label="to[a] / Drum.change_speed"]; + s8 -> s3 [label="Buttons.Stop / Drum.stop,Heater.stop,PumpOut.start"]; + s8 -> s29 [label="Water.Leak / Display.alarm,Drum.stop,Heater.stop"]; + s8 -> s8 [label="Water.Temp_low / Heater.start"]; + s8 -> s8 [label="Water.Temp_ok / Heater.stop"]; + s8 -> s9 [resets="a" label="to[a] / Drum.change_speed"]; + s9 -> s3 [label="Buttons.Stop / Drum.stop,Heater.stop,PumpOut.start"]; + s9 -> s29 [label="Water.Leak / Display.alarm,Drum.stop,Heater.stop"]; + s9 -> s9 [label="Water.Temp_low / Heater.start"]; + s9 -> s9 [label="Water.Temp_ok / Heater.stop"]; + s9 -> s10 [resets="a" label="to[a] / Drum.change_speed"]; + s10 -> s3 [label="Buttons.Stop / Drum.stop,Heater.stop,PumpOut.start"]; + s10 -> s29 [label="Water.Leak / Display.alarm,Drum.stop,Heater.stop"]; + s10 -> s10 [label="Water.Temp_low / Heater.start"]; + s10 -> s10 [label="Water.Temp_ok / Heater.stop"]; + s10 -> s11 [resets="a" label="to[a] / Drum.change_speed"]; + s11 -> s3 [label="Buttons.Stop / Drum.stop,Heater.stop,PumpOut.start"]; + s11 -> s29 [label="Water.Leak / Display.alarm,Drum.stop,Heater.stop"]; + s11 -> s11 [label="Water.Temp_low / Heater.start"]; + s11 -> s11 [label="Water.Temp_ok / Heater.stop"]; + s11 -> s12 [resets="a" label="to[a] / Drum.change_speed"]; + s12 -> s3 [label="Buttons.Stop / Drum.stop,Heater.stop,PumpOut.start"]; + s12 -> s29 [label="Water.Leak / Display.alarm,Drum.stop,Heater.stop"]; + s12 -> s12 [label="Water.Temp_low / Heater.start"]; + s12 -> s12 [label="Water.Temp_ok / Heater.stop"]; + s12 -> s13 [resets="a" label="to[a] / Drum.change_speed"]; + s13 -> s3 [label="Buttons.Stop / Drum.stop,Heater.stop,PumpOut.start"]; + s13 -> s29 [label="Water.Leak / Display.alarm,Drum.stop,Heater.stop"]; + s13 -> s13 [label="Water.Temp_low / Heater.start"]; + s13 -> s13 [label="Water.Temp_ok / Heater.stop"]; + s13 -> s14 [resets="a" label="to[a] / Drum.change_speed"]; + s14 -> s3 [label="Buttons.Stop / Drum.stop,Heater.stop,PumpOut.start"]; + s14 -> s29 [label="Water.Leak / Display.alarm,Drum.stop,Heater.stop"]; + s14 -> s14 [label="Water.Temp_low / Heater.start"]; + s14 -> s14 [label="Water.Temp_ok / Heater.stop"]; + s14 -> s15 [resets="a" label="to[a] / Drum.change_speed"]; + s15 -> s3 [label="Buttons.Stop / Drum.stop,Heater.stop,PumpOut.start"]; + s15 -> s29 [label="Water.Leak / Display.alarm,Drum.stop,Heater.stop"]; + s15 -> s15 [label="Water.Temp_low / Heater.start"]; + s15 -> s15 [label="Water.Temp_ok / Heater.stop"]; + s15 -> s16 [resets="a" label="to[a] / Drum.change_speed"]; + s16 -> s3 [label="Buttons.Stop / Drum.stop,Heater.stop,PumpOut.start"]; + s16 -> s29 [label="Water.Leak / Display.alarm,Drum.stop,Heater.stop"]; + s16 -> s16 [label="Water.Temp_low / Heater.start"]; + s16 -> s16 [label="Water.Temp_ok / Heater.stop"]; + s16 -> s17 [resets="a" label="to[a] / Drum.change_speed"]; + s17 -> s3 [label="Buttons.Stop / Drum.stop,Heater.stop,PumpOut.start"]; + s17 -> s29 [label="Water.Leak / Display.alarm,Drum.stop,Heater.stop"]; + s17 -> s17 [label="Water.Temp_low / Heater.start"]; + s17 -> s17 [label="Water.Temp_ok / Heater.stop"]; + s17 -> s18 [resets="a" label="to[a] / Drum.change_speed"]; + s18 -> s3 [label="Buttons.Stop / Drum.stop,Heater.stop,PumpOut.start"]; + s18 -> s29 [label="Water.Leak / Display.alarm,Drum.stop,Heater.stop"]; + s18 -> s18 [label="Water.Temp_low / Heater.start"]; + s18 -> s18 [label="Water.Temp_ok / Heater.stop"]; + s18 -> s19 [resets="a" label="to[a] / Drum.change_speed"]; + s19 -> s3 [label="Buttons.Stop / Drum.stop,Heater.stop,PumpOut.start"]; + s19 -> s29 [label="Water.Leak / Display.alarm,Drum.stop,Heater.stop"]; + s19 -> s19 [label="Water.Temp_low / Heater.start"]; + s19 -> s19 [label="Water.Temp_ok / Heater.stop"]; + s19 -> s20 [resets="a" label="to[a] / Drum.change_speed"]; + s20 -> s3 [label="Buttons.Stop / Drum.stop,Heater.stop,PumpOut.start"]; + s20 -> s29 [label="Water.Leak / Display.alarm,Drum.stop,Heater.stop"]; + s20 -> s20 [label="Water.Temp_low / Heater.start"]; + s20 -> s20 [label="Water.Temp_ok / Heater.stop"]; + s20 -> s21 [resets="a" label="to[a] / Drum.change_speed"]; + s21 -> s3 [label="Buttons.Stop / Drum.stop,Heater.stop,PumpOut.start"]; + s21 -> s29 [label="Water.Leak / Display.alarm,Drum.stop,Heater.stop"]; + s21 -> s21 [label="Water.Temp_low / Heater.start"]; + s21 -> s21 [label="Water.Temp_ok / Heater.stop"]; + s21 -> s22 [resets="a" label="to[a] / Drum.change_speed"]; + s22 -> s3 [label="Buttons.Stop / Drum.stop,Heater.stop,PumpOut.start"]; + s22 -> s29 [label="Water.Leak / Display.alarm,Drum.stop,Heater.stop"]; + s22 -> s22 [label="Water.Temp_low / Heater.start"]; + s22 -> s22 [label="Water.Temp_ok / Heater.stop"]; + s22 -> s23 [resets="a" label="to[a] / Drum.change_speed"]; + s23 -> s3 [label="Buttons.Stop / Drum.stop,Heater.stop,PumpOut.start"]; + s23 -> s29 [label="Water.Leak / Display.alarm,Drum.stop,Heater.stop"]; + s23 -> s23 [label="Water.Temp_low / Heater.start"]; + s23 -> s23 [label="Water.Temp_ok / Heater.stop"]; + s23 -> s24 [resets="a" label="to[a] / Drum.change_speed"]; + s24 -> s3 [label="Buttons.Stop / Drum.stop,Heater.stop,PumpOut.start"]; + s24 -> s29 [label="Water.Leak / Display.alarm,Drum.stop,Heater.stop"]; + s24 -> s24 [label="Water.Temp_low / Heater.start"]; + s24 -> s24 [label="Water.Temp_ok / Heater.stop"]; + s24 -> s25 [resets="a" label="to[a] / Drum.change_speed"]; + s25 -> s3 [label="Buttons.Stop / Drum.stop,Heater.stop,PumpOut.start"]; + s25 -> s29 [label="Water.Leak / Display.alarm,Drum.stop,Heater.stop"]; + s25 -> s25 [label="Water.Temp_low / Heater.start"]; + s25 -> s25 [label="Water.Temp_ok / Heater.stop"]; + s25 -> s26 [label="to[a] / Drum.stop,Heater.stop,PumpOut.start"]; + s26 -> s3 [label="Buttons.Stop / PumpOut.start"]; + s26 -> s29 [label="Water.Leak / Display.alarm,PumpOut.stop"]; + s26 -> s27 [resets="a" label="Water.Low / Drum.full_speed,PumpOut.stop"]; + s27 -> s3 [label="Buttons.Stop / Drum.stop,PumpOut.start"]; + s27 -> s29 [label="Water.Leak / Display.alarm,Drum.stop"]; + s27 -> s28 [label="to[a] / Drum.stop,PumpOut.start"]; + s28 -> s3 [label="Buttons.Stop / PumpOut.start"]; + s28 -> s4 [resets="a" label="Water.Empty / Display.done,Door.unlock,PumpOut.stop"]; + s28 -> s29 [label="Water.Leak / Display.alarm,PumpOut.stop"]; + s30 -> s31 [label="Door.Closed / Display.rem_short,Door.lock,Heater.start,PumpIn.start"]; + s30 -> s0 [resets="a" label="Door.Open / Display.door_warning"]; + s30 -> s34 [label="to[a] / Display.off"]; + s31 -> s3 [label="Buttons.Stop / Heater.stop,PumpIn.stop,PumpOut.start"]; + s31 -> s29 [label="Water.Leak / Display.alarm,Heater.stop,PumpIn.stop"]; + s31 -> s32 [resets="a" label="Water.Full / Detergent.add,Drum.normal_speed,Heater.stop,PumpIn.stop"]; + s32 -> s3 [label="Buttons.Stop / Drum.stop,Heater.stop,PumpOut.start"]; + s32 -> s29 [label="Water.Leak / Display.alarm,Drum.stop,Heater.stop"]; + s32 -> s32 [label="Water.Temp_low / Heater.start"]; + s32 -> s32 [label="Water.Temp_ok / Heater.stop"]; + s32 -> s33 [label="to[a] / Drum.normal_speed,Heater.stop,PumpOut.start"]; + s33 -> s3 [label="Buttons.Stop / Drum.stop,PumpOut.start"]; + s33 -> s4 [resets="a" label="Water.Empty / Display.done,Door.unlock,Drum.stop,PumpOut.stop"]; + s33 -> s29 [label="Water.Leak / Display.alarm,Drum.stop,PumpOut.stop"]; + s34 -> s0 [resets="a" label="Buttons.On / Display.welcome"]; + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s34; + +} diff --git a/test-support/learning-examples/src/main/resources/mmlt/WSN.dot b/test-support/learning-examples/src/main/resources/mmlt/WSN.dot new file mode 100644 index 0000000000..12d305ae35 --- /dev/null +++ b/test-support/learning-examples/src/main/resources/mmlt/WSN.dot @@ -0,0 +1,24 @@ +// Model of a wireless sensor node that regularly collects and transmits data. +// If the battery is low, no data is transmitted. Then, +// a user may collect the data manually. +// The node can be shut down at any time. If the battery is empty, it is shut down automatically. +digraph g { + + s0 [timers="a=60000,b=3600000" shape="circle"]; + s1 [timers="a=300000" shape="circle"]; + s2 [shape="circle"]; + s3 [shape="circle"]; + s0 -> s1 [resets="a" label="Battery.Low / Tx.disable"]; + s0 -> s3 [label="User.Power / void"]; + s0 -> s0 [resets="a" label="to[a] / Sensor.sample"]; + s0 -> s0 [resets="b" label="to[b] / Tx.send"]; + s1 -> s2 [label="Battery.Empty / void"]; + s1 -> s1 [resets="a" label="User.Collect / Buffer.get"]; + s1 -> s3 [label="User.Power / void"]; + s1 -> s1 [resets="a" label="to[a] / Sensor.sample"]; + s3 -> s0 [resets="a,b" label="User.Power / void"]; + +__start0 [label="" shape="none" width="0" height="0"]; +__start0 -> s3; + +} diff --git a/test-support/learning-examples/src/main/resources/mmlt/sensor_collector.dot b/test-support/learning-examples/src/main/resources/mmlt/sensor_collector.dot new file mode 100644 index 0000000000..9035e0a600 --- /dev/null +++ b/test-support/learning-examples/src/main/resources/mmlt/sensor_collector.dot @@ -0,0 +1,26 @@ +// An MMLT model of a sensor that measures particulate matter and ambient noise. +// The measurement program automatically ends after some time. It may be restarted at any time. +// Alternatively, a self-check program can be entered. This also ends after some time and may be aborted. +// At the end of either program, the collected data may be retrieved. +digraph g { + s0 [label="L0" timers=""] + s1 [label="L1" timers="a=3,b=6,c=40"] + s2 [label="L2" timers="d=4"] + s3 [label="L3" timers=""] + + s0 -> s1 [label="p1/go"] + + s1 -> s1 [label="abort / ok" resets="a,b,c"] + s1 -> s1 [label="to[a] / part"] + s1 -> s1 [label="to[b] / noise"] + s1 -> s3 [label="to[c] / done"] + + s0 -> s2 [label="p2 / go"] + s2 -> s3 [label="abort / void"] + s2 -> s3 [label="to[d] / done"] + + s3 -> s0 [label="collect / void"] + + __start0 [label="" shape="none" width="0" height="0"]; + __start0 -> s0; +} \ No newline at end of file