From ac247902609b0d54f968830850576a49e65646f1 Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Wed, 8 Oct 2025 14:16:37 +0200 Subject: [PATCH 01/27] bumped to java 17 --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 6245bdfce..0b4f6f114 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 From f22d988b8d9beb69ce9669d861579880582942b1 Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Wed, 8 Oct 2025 14:31:08 +0200 Subject: [PATCH 02/27] Made RowImpl public to enable access in custom observation table implementations. --- .../de/learnlib/datastructure/observationtable/RowImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 cdee25aa0..79291c63a 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; From d0286d43e791c4caed8992c5db882541d0afbe4d Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Wed, 8 Oct 2025 15:57:26 +0200 Subject: [PATCH 03/27] Made several methods of RowImpl public, to enable external access from observation tables that are defined in the learner module. --- .../datastructure/observationtable/RowImpl.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 79291c63a..b75ec76e7 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 @@ -37,7 +37,7 @@ public 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 @@ public 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 @@ public 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); } } From 33bad63bcf52b9761bcc20710bc98d668a865987 Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Thu, 9 Oct 2025 12:18:05 +0200 Subject: [PATCH 04/27] Started with integration of MMLT. --- .../lstar/mmlt/LStarLocalTimerMealy.java | 431 +++++++++++++ .../LStarLocalTimerMealyHypDataContainer.java | 72 +++ .../LocalTimerMealyHypothesisBuilder.java | 127 ++++ .../mmlt/LocalTimerMealyObservationTable.java | 582 ++++++++++++++++++ .../lstar/mmlt/LocationTimerInfo.java | 119 ++++ .../lstar/mmlt/cex/CexPreprocessor.java | 37 ++ .../lstar/mmlt/cex/ExtendedDecomposition.java | 39 ++ ...lTimerMealyCounterexampleDecompositor.java | 139 +++++ .../LocalTimerMealyCounterexampleHandler.java | 176 ++++++ ...alTimerMealyInconsPrefixTransformAcex.java | 66 ++ .../LocalTimerMealyOutputInconsistency.java | 22 + .../mmlt/cex/results/CexAnalysisResult.java | 11 + .../mmlt/cex/results/FalseIgnoreResult.java | 29 + .../results/MissingDiscriminatorResult.java | 36 ++ .../cex/results/MissingOneShotResult.java | 28 + .../mmlt/cex/results/MissingResetResult.java | 30 + .../IInternalLocalTimerMealyHypothesis.java | 55 ++ .../mmlt/hyp/LocalTimerMealyHypothesis.java | 118 ++++ .../algorithm/LocalTimerMealyModelParams.java | 40 ++ .../de/learnlib/oracle/TimedQueryOracle.java | 82 +++ .../container/DummyStatsContainer.java | 61 ++ .../container/LearnerStatsProvider.java | 13 + .../statistic/container/StatsContainerX.java | 124 ++++ .../de/learnlib/sul/LocalTimerMealySUL.java | 119 ++++ .../learnlib/symbol_filter/SymbolFilter.java | 38 ++ .../symbol_filter/SymbolFilterResponse.java | 27 + api/src/main/java/module-info.java | 2 + .../src/main/java/module-info.java | 1 + .../statistic/container/CounterStatistic.java | 32 + .../statistic/container/FlagStatistic.java | 23 + .../statistic/container/LearnerStatistic.java | 36 ++ .../container/MapStatsContainer.java | 99 +++ .../container/StopClockStatistic.java | 36 ++ .../statistic/container/TextStatistic.java | 16 + .../LocalTimerMealySimulatorSUL.java | 71 +++ .../filter/cache/mmlt/CacheTreeNode.java | 170 +++++ .../mmlt/FastLocalTimerMealyTreeCacheSUL.java | 261 ++++++++ .../filter/cache/mmlt/TimeoutReducerSUL.java | 80 +++ filters/statistics/pom.xml | 5 + .../sul/LocalTimerMealyStatsSUL.java | 98 +++ .../statistics/src/main/java/module-info.java | 1 + .../mmlt/LocalTimerMealySimulatorOracle.java | 63 ++ .../equivalence/mmlt/ResetSearchOracle.java | 167 +++++ .../oracle/membership/TimedQueryOracle.java | 271 ++++++++ 44 files changed, 4053 insertions(+) create mode 100644 algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/LStarLocalTimerMealy.java create mode 100644 algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/LStarLocalTimerMealyHypDataContainer.java create mode 100644 algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/LocalTimerMealyHypothesisBuilder.java create mode 100644 algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/LocalTimerMealyObservationTable.java create mode 100644 algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/LocationTimerInfo.java create mode 100644 algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/CexPreprocessor.java create mode 100644 algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/ExtendedDecomposition.java create mode 100644 algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/LocalTimerMealyCounterexampleDecompositor.java create mode 100644 algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/LocalTimerMealyCounterexampleHandler.java create mode 100644 algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/LocalTimerMealyInconsPrefixTransformAcex.java create mode 100644 algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/LocalTimerMealyOutputInconsistency.java create mode 100644 algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/results/CexAnalysisResult.java create mode 100644 algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/results/FalseIgnoreResult.java create mode 100644 algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/results/MissingDiscriminatorResult.java create mode 100644 algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/results/MissingOneShotResult.java create mode 100644 algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/results/MissingResetResult.java create mode 100644 algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/hyp/IInternalLocalTimerMealyHypothesis.java create mode 100644 algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/hyp/LocalTimerMealyHypothesis.java create mode 100644 api/src/main/java/de/learnlib/algorithm/LocalTimerMealyModelParams.java create mode 100644 api/src/main/java/de/learnlib/oracle/TimedQueryOracle.java create mode 100644 api/src/main/java/de/learnlib/statistic/container/DummyStatsContainer.java create mode 100644 api/src/main/java/de/learnlib/statistic/container/LearnerStatsProvider.java create mode 100644 api/src/main/java/de/learnlib/statistic/container/StatsContainerX.java create mode 100644 api/src/main/java/de/learnlib/sul/LocalTimerMealySUL.java create mode 100644 api/src/main/java/de/learnlib/symbol_filter/SymbolFilter.java create mode 100644 api/src/main/java/de/learnlib/symbol_filter/SymbolFilterResponse.java create mode 100644 commons/util/src/main/java/de/learnlib/util/statistic/container/CounterStatistic.java create mode 100644 commons/util/src/main/java/de/learnlib/util/statistic/container/FlagStatistic.java create mode 100644 commons/util/src/main/java/de/learnlib/util/statistic/container/LearnerStatistic.java create mode 100644 commons/util/src/main/java/de/learnlib/util/statistic/container/MapStatsContainer.java create mode 100644 commons/util/src/main/java/de/learnlib/util/statistic/container/StopClockStatistic.java create mode 100644 commons/util/src/main/java/de/learnlib/util/statistic/container/TextStatistic.java create mode 100644 drivers/simulator/src/main/java/de/learnlib/driver/simulator/LocalTimerMealySimulatorSUL.java create mode 100644 filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/CacheTreeNode.java create mode 100644 filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/FastLocalTimerMealyTreeCacheSUL.java create mode 100644 filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/TimeoutReducerSUL.java create mode 100644 filters/statistics/src/main/java/de/learnlib/filter/statistic/sul/LocalTimerMealyStatsSUL.java create mode 100644 oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/mmlt/LocalTimerMealySimulatorOracle.java create mode 100644 oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/mmlt/ResetSearchOracle.java create mode 100644 oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/TimedQueryOracle.java 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 000000000..acd23c388 --- /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.TimedQueryOracle; +import de.learnlib.query.DefaultQuery; +import de.learnlib.statistic.container.DummyStatsContainer; +import de.learnlib.statistic.container.LearnerStatsProvider; +import de.learnlib.statistic.container.StatsContainerX; +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.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 StatsContainerX stats = new DummyStatsContainer(); + + private final ClosingStrategy, ? super Word>> closingStrategy; + + private final TimedQueryOracle timeOracle; + private final SymbolFilter 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 Model 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, + TimedQueryOracle timeOracle, + SymbolFilter 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 Model 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, + TimedQueryOracle timeOracle, + @NonNull + SymbolFilter 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(StatsContainerX 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 000000000..14954492a --- /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 000000000..d23788eba --- /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 000000000..ccc4bddb7 --- /dev/null +++ b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/LocalTimerMealyObservationTable.java @@ -0,0 +1,582 @@ +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.TimedQueryOracle; +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.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 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, + SymbolFilter 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, TimedQueryOracle 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, TimedQueryOracle 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, TimedQueryOracle 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 TimedQueryOracle 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, TimedQueryOracle 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 TimedQueryOracle 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 TimedQueryOracle 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, TimedQueryOracle 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, TimedQueryOracle 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 000000000..f0ab4616d --- /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 000000000..d4ade3082 --- /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 000000000..5fac3ec1e --- /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 000000000..e82c123e9 --- /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.TimedQueryOracle; +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 TimedQueryOracle timeOracle; + private final AcexAnalyzer acexAnalyzer; + + public LocalTimerMealyCounterexampleDecompositor(TimedQueryOracle 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 000000000..9ee73e305 --- /dev/null +++ b/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/cex/LocalTimerMealyCounterexampleHandler.java @@ -0,0 +1,176 @@ +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.TimedQueryOracle; +import de.learnlib.statistic.container.DummyStatsContainer; +import de.learnlib.statistic.container.LearnerStatsProvider; +import de.learnlib.statistic.container.StatsContainerX; +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.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 symbolFilter; + private StatsContainerX stats = new DummyStatsContainer(); + + protected final TimedQueryOracle timeOracle; + private final LocalTimerMealyCounterexampleDecompositor decompositor; + + public LocalTimerMealyCounterexampleHandler(TimedQueryOracle timeOracle, AcexAnalyzer acexAnalyzer, SymbolFilter symbolFilter) { + this.timeOracle = timeOracle; + this.decompositor = new LocalTimerMealyCounterexampleDecompositor<>(timeOracle, acexAnalyzer); + this.symbolFilter = symbolFilter; + } + + @Override + public void setStatsContainer(StatsContainerX 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 000000000..cc1942d08 --- /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.TimedQueryOracle; +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 TimedQueryOracle 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, TimedQueryOracle 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 000000000..a5a584f79 --- /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 000000000..cb990699d --- /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 000000000..36c4eaa5b --- /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 000000000..9e2034f7d --- /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 000000000..3da5d5f01 --- /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 000000000..e6e538a25 --- /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 000000000..53c0228c4 --- /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 000000000..5a751c009 --- /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/api/src/main/java/de/learnlib/algorithm/LocalTimerMealyModelParams.java b/api/src/main/java/de/learnlib/algorithm/LocalTimerMealyModelParams.java new file mode 100644 index 000000000..a4e9aa8e4 --- /dev/null +++ b/api/src/main/java/de/learnlib/algorithm/LocalTimerMealyModelParams.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023-2024 Paul Kogel, TU Berlin + * + * THIS SOFTWARE IS INTENDED FOR ACADEMIC, RESEARCH AND EDUCATIONAL PURPOSES ONLY, + * MOST PROMINENTLY TO FACILITATE RESEARCH ON AUTOMATA LEARNING. FOR-PROFIT USE OF + * THIS SOFTWARE, INCLUDING, BUT NOT LIMITED TO, SELLING THE SOFTWARE ON ITS OWN OR + * AS PART OF TOOLS AND/OR SERVICES IS PROHIBITED. COMMERCIAL USE OR COMMERCIAL + * DISTRIBUTION OF THIS SOFTWARE OR OF ANY DERIVATES IS PROHIBITED. + * + * Apart from that, this software is licensed under the + * GNU Affero Public License version 3 (AGPLv3). + * + * https://www.gnu.org/licenses/agpl-3.0.txt + * + * 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. + */ + +package de.learnlib.algorithm; + +import net.automatalib.automaton.time.mmlt.AbstractSymbolCombiner; + +/** + * Model-specific parameters for the MMLT-learner. + * These are used by various filters, oracles, and the MMLT simulator. + * + * @param silentOutput Silent output symbol + * @param maxTimeoutWaitingTime Maximum waiting time for a timeout symbol + * @param maxTimerQueryWaitingTime Maximum waiting time for timer queries + * @param outputCombiner Function for combining simultaneously occurring outputs of timers + * @param + */ +public record LocalTimerMealyModelParams(O silentOutput, + long maxTimeoutWaitingTime, + long maxTimerQueryWaitingTime, + AbstractSymbolCombiner outputCombiner) { +} diff --git a/api/src/main/java/de/learnlib/oracle/TimedQueryOracle.java b/api/src/main/java/de/learnlib/oracle/TimedQueryOracle.java new file mode 100644 index 000000000..3d3a92947 --- /dev/null +++ b/api/src/main/java/de/learnlib/oracle/TimedQueryOracle.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 TimedQueryOracle 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/statistic/container/DummyStatsContainer.java b/api/src/main/java/de/learnlib/statistic/container/DummyStatsContainer.java new file mode 100644 index 000000000..1f89ec976 --- /dev/null +++ b/api/src/main/java/de/learnlib/statistic/container/DummyStatsContainer.java @@ -0,0 +1,61 @@ +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 StatsContainerX} that does nothing. + */ +public class DummyStatsContainer implements StatsContainerX { + @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(); + } +} 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 000000000..af7f64a59 --- /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(StatsContainerX container); +} diff --git a/api/src/main/java/de/learnlib/statistic/container/StatsContainerX.java b/api/src/main/java/de/learnlib/statistic/container/StatsContainerX.java new file mode 100644 index 000000000..943c5b513 --- /dev/null +++ b/api/src/main/java/de/learnlib/statistic/container/StatsContainerX.java @@ -0,0 +1,124 @@ +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 StatsContainerX { + + // 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); +} 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 000000000..a77f04cc2 --- /dev/null +++ b/api/src/main/java/de/learnlib/sul/LocalTimerMealySUL.java @@ -0,0 +1,119 @@ +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; + +/** + * A SUL with MMLT semantics. We use this type to interface with real systems and to + * simulate MMLT models. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public abstract class 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. + */ + public 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. timeout to use for timeoutSymbols. + */ + public 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 an input to the SUL and returns the observed output. + * + * @param input Input + * @return SUL output. + */ + public abstract 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 there are no timers defined + * in the current location. + * + * @param maxTime Maximum waiting time. + * @return Observed timer output with waiting time, or null, if no timeout observed. + */ + @Nullable + public abstract 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 + public 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. + */ + public 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. + */ + public abstract void pre(); + + /** + * Deinitializes the SUL. + */ + public abstract 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 000000000..d88c23043 --- /dev/null +++ b/api/src/main/java/de/learnlib/symbol_filter/SymbolFilter.java @@ -0,0 +1,38 @@ +package de.learnlib.symbol_filter; + +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.NonDelayingInput; +import net.automatalib.word.Word; + +/** + * Interface for a symbol filter that can be used to speed-up the learning of MMLTs. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public interface SymbolFilter { + + /** + * Predicts whether the provided symbol is not ignorable in the configuration + * identified by the provided prefix. + *

+ * "Ignorable" means the symbol belongs to a silent self-loop. + *

+ * 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 Configuration prefix. May contain delay-symbols (= tau inputs). + * @param symbol Queried transition + * @return IGNORE if the symbol is considered ignorable, ACCEPT if it is not. + */ + SymbolFilterResponse query(Word> prefix, NonDelayingInput symbol); + + /** + * Sets the response of the filter for the given transition to the provided response. + * + * @param prefix Configuration prefix. + * @param symbol Queried transition + * @param response New response + */ + void update(Word> prefix, NonDelayingInput 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 000000000..6c06e93ce --- /dev/null +++ b/api/src/main/java/de/learnlib/symbol_filter/SymbolFilterResponse.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023-2024 Paul Kogel, TU Berlin + * + * THIS SOFTWARE IS INTENDED FOR ACADEMIC, RESEARCH AND EDUCATIONAL PURPOSES ONLY, + * MOST PROMINENTLY TO FACILITATE RESEARCH ON AUTOMATA LEARNING. FOR-PROFIT USE OF + * THIS SOFTWARE, INCLUDING, BUT NOT LIMITED TO, SELLING THE SOFTWARE ON ITS OWN OR + * AS PART OF TOOLS AND/OR SERVICES IS PROHIBITED. COMMERCIAL USE OR COMMERCIAL + * DISTRIBUTION OF THIS SOFTWARE OR OF ANY DERIVATES IS PROHIBITED. + * + * Apart from that, this software is licensed under the + * GNU Affero Public License version 3 (AGPLv3). + * + * https://www.gnu.org/licenses/agpl-3.0.txt + * + * 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. + */ + +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 e62d48409..fd3814f99 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/module-info.java b/commons/datastructures/src/main/java/module-info.java index e9ccbfa9b..5b1d9590d 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 000000000..c14a33bc3 --- /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 000000000..5bb04bcec --- /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 000000000..efba05c48 --- /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 000000000..7710aa7a0 --- /dev/null +++ b/commons/util/src/main/java/de/learnlib/util/statistic/container/MapStatsContainer.java @@ -0,0 +1,99 @@ +package de.learnlib.util.statistic.container; + +import de.learnlib.statistic.container.StatsContainerX; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * A {@link StatsContainerX} that stores all statistics in a {@link Map}. + */ +public class MapStatsContainer implements StatsContainerX { + 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(); + } +} 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 000000000..9adf3c797 --- /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 000000000..5d9e81aaf --- /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/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 000000000..bdcd17af0 --- /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 extends 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/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 000000000..bddb81c3c --- /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 + */ +public 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/FastLocalTimerMealyTreeCacheSUL.java b/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/FastLocalTimerMealyTreeCacheSUL.java new file mode 100644 index 000000000..4ece66999 --- /dev/null +++ b/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/FastLocalTimerMealyTreeCacheSUL.java @@ -0,0 +1,261 @@ +package de.learnlib.filter.cache.mmlt; + + +import de.learnlib.statistic.container.DummyStatsContainer; +import de.learnlib.statistic.container.LearnerStatsProvider; +import de.learnlib.statistic.container.StatsContainerX; +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 an AbstractLocalTimerMealySUL. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class FastLocalTimerMealyTreeCacheSUL extends LocalTimerMealySUL implements GraphViewable, LearnerStatsProvider { + private final LocalTimerMealySUL delegate; + + private final CacheTreeNode cacheRoot; + private CacheTreeNode currentState; + + private final LocalTimerMealyOutputSymbol silentOutput; + private boolean cacheMiss; + + private StatsContainerX stats = new DummyStatsContainer(); + + @Override + public void setStatsContainer(StatsContainerX container) { + this.stats = container; + } + + public FastLocalTimerMealyTreeCacheSUL(LocalTimerMealySUL delegate, O silentOutput) { + this.delegate = delegate; + this.silentOutput = new LocalTimerMealyOutputSymbol<>(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, 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; + } + + 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(); + } + + +} 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 000000000..7f4817d68 --- /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.StatsContainerX; +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 extends LocalTimerMealySUL implements LearnerStatsProvider { + + private final LocalTimerMealySUL delegate; + private final long maxDelay; + + /** + * Delay since the last non-delaying input OR + * timer expiration. + */ + private long noTimeoutWaitingTime; + + private StatsContainerX stats; + + public TimeoutReducerSUL(LocalTimerMealySUL delegate, long maxDelay, StatsContainerX 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(StatsContainerX container) { + this.stats = container; + } +} diff --git a/filters/statistics/pom.xml b/filters/statistics/pom.xml index 83ab5b76e..2e7ec208c 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 000000000..0f15793ee --- /dev/null +++ b/filters/statistics/src/main/java/de/learnlib/filter/statistic/sul/LocalTimerMealyStatsSUL.java @@ -0,0 +1,98 @@ +package de.learnlib.filter.statistic.sul; + +import de.learnlib.statistic.container.LearnerStatsProvider; +import de.learnlib.statistic.container.StatsContainerX; +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 extends LocalTimerMealySUL implements LearnerStatsProvider { + private final LocalTimerMealySUL delegate; + private StatsContainerX stats; + + @Nullable + private final String name; + + public LocalTimerMealyStatsSUL(LocalTimerMealySUL delegate, StatsContainerX stats) { + this(delegate, stats, null); + } + + public LocalTimerMealyStatsSUL(LocalTimerMealySUL delegate, StatsContainerX stats, String name) { + this.delegate = delegate; + this.stats = stats; + this.name = name; + } + + public long getResetCount() { + if (this.stats == null) { + throw new IllegalStateException("No stats container set up."); + } + return this.stats.getCount(withPrefix("sul_resets_counter")).get(); + } + + @Override + public void setStatsContainer(StatsContainerX 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 f262e24f5..90ce2aef0 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/mmlt/LocalTimerMealySimulatorOracle.java b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/mmlt/LocalTimerMealySimulatorOracle.java new file mode 100644 index 000000000..b942a2a37 --- /dev/null +++ b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/mmlt/LocalTimerMealySimulatorOracle.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023-2024 Paul Kogel, TU Berlin + * + * THIS SOFTWARE IS INTENDED FOR ACADEMIC, RESEARCH AND EDUCATIONAL PURPOSES ONLY, + * MOST PROMINENTLY TO FACILITATE RESEARCH ON AUTOMATA LEARNING. FOR-PROFIT USE OF + * THIS SOFTWARE, INCLUDING, BUT NOT LIMITED TO, SELLING THE SOFTWARE ON ITS OWN OR + * AS PART OF TOOLS AND/OR SERVICES IS PROHIBITED. COMMERCIAL USE OR COMMERCIAL + * DISTRIBUTION OF THIS SOFTWARE OR OF ANY DERIVATES IS PROHIBITED. + * + * Apart from that, this software is licensed under the + * GNU Affero Public License version 3 (AGPLv3). + * + * https://www.gnu.org/licenses/agpl-3.0.txt + * + * 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. + */ + +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.Collection; + +/** + * A simulator oracle for MMLTs. + * + * @param Reference model location type + * @param Hypothesis model location type + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class LocalTimerMealySimulatorOracle implements EquivalenceOracle, LocalTimerMealySemanticInputSymbol, Word>> { + + private final LocalTimerMealy refModel; + + public LocalTimerMealySimulatorOracle(LocalTimerMealy refModel) { + this.refModel = refModel; + } + + + @Override + public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, Collection> ignored) { + var separatingWord = LocalTimerMealyUtil.findSeparatingWord(refModel, hypothesis); + + 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 000000000..6557f76b8 --- /dev/null +++ b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/mmlt/ResetSearchOracle.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2023-2024 Paul Kogel, TU Berlin + * + * THIS SOFTWARE IS INTENDED FOR ACADEMIC, RESEARCH AND EDUCATIONAL PURPOSES ONLY, + * MOST PROMINENTLY TO FACILITATE RESEARCH ON AUTOMATA LEARNING. FOR-PROFIT USE OF + * THIS SOFTWARE, INCLUDING, BUT NOT LIMITED TO, SELLING THE SOFTWARE ON ITS OWN OR + * AS PART OF TOOLS AND/OR SERVICES IS PROHIBITED. COMMERCIAL USE OR COMMERCIAL + * DISTRIBUTION OF THIS SOFTWARE OR OF ANY DERIVATES IS PROHIBITED. + * + * Apart from that, this software is licensed under the + * GNU Affero Public License version 3 (AGPLv3). + * + * https://www.gnu.org/licenses/agpl-3.0.txt + * + * 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. + */ + +package de.learnlib.oracle.equivalence.mmlt; + +import de.learnlib.oracle.TimedQueryOracle; +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 inputs that self-loop in that location. + * - Appends timeout. + * + * @param Location type + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class ResetSearchOracle implements EquivalenceOracle, LocalTimerMealySemanticInputSymbol, Word>> { + + private final static Logger logger = LoggerFactory.getLogger(ResetSearchOracle.class); + + private final TimedQueryOracle timeOracle; + private final Random locPrefixRandom; + + private final double loopInsertPerc; + private final double testedLocPerc; + + private final long loopingInputSelectionSeed; + + public ResetSearchOracle(TimedQueryOracle 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)) { + throw new AssertionError(); + } + 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> ignored) { + if (loopInsertPerc == 0) { + return null; // oracle is disabled + } + + return this.findCexInternal(hypothesis); + } + + private @Nullable DefaultQuery, Word>> findCexInternal + (LocalTimerMealy hypothesis) { + + // Retrieve prefixes from state cover, to establish some separation between learner and teacher: + var stateCover = LocalTimerMealyCover.getLocalTimerMealyLocationCover(hypothesis); + + // Only keep locations that have at least two stable configs: + 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); + + + List> listAlphabet = new ArrayList<>(hypothesis.getUntimedAlphabet()); + + for (var prefix : chosenPrefixes) { + // Retrieve looping symbols: + var sourceLoc = hypothesis.getSemantics().traceInputs(prefix).getLocation(); + var loopingInputs = getLoopingSymbols(sourceLoc, listAlphabet, 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/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 000000000..20a01f057 --- /dev/null +++ b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/TimedQueryOracle.java @@ -0,0 +1,271 @@ +package de.learnlib.oracle.membership; + +import de.learnlib.algorithm.LocalTimerMealyModelParams; +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 de.learnlib.oracle.TimedQueryOracle { + + 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()); + } + +} From 48702d415d2bcf88f38c9839113db44c7a12bc29 Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Thu, 9 Oct 2025 12:26:50 +0200 Subject: [PATCH 05/27] Extended EquivalenceOracle.java with LocalTimerMealyEquivalenceOracle. --- .../de/learnlib/oracle/EquivalenceOracle.java | 11 ++ .../mmlt/LocalTimerMealyRandomWpOracle.java | 169 ++++++++++++++++++ .../mmlt/LocalTimerMealySimulatorOracle.java | 4 +- .../equivalence/mmlt/ResetSearchOracle.java | 4 +- 4 files changed, 184 insertions(+), 4 deletions(-) create mode 100644 oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/mmlt/LocalTimerMealyRandomWpOracle.java diff --git a/api/src/main/java/de/learnlib/oracle/EquivalenceOracle.java b/api/src/main/java/de/learnlib/oracle/EquivalenceOracle.java index 065c2c747..29500443e 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,12 @@ interface MealyEquivalenceOracle extends EquivalenceOracle extends EquivalenceOracle, I, Word> {} + /** + * A specialization of the {@link EquivalenceOracle} interface for a Local Timer Mealy learning scenario. + * + * @param Location type + * @param Input type for non-delaying inputs + * @param Output symbol type + */ + interface LocalTimerMealyEquivalenceOracle extends EquivalenceOracle, LocalTimerMealySemanticInputSymbol, Word>>{} } 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 000000000..c28de0323 --- /dev/null +++ b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/mmlt/LocalTimerMealyRandomWpOracle.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2023-2024 Paul Kogel, TU Berlin + * + * THIS SOFTWARE IS INTENDED FOR ACADEMIC, RESEARCH AND EDUCATIONAL PURPOSES ONLY, + * MOST PROMINENTLY TO FACILITATE RESEARCH ON AUTOMATA LEARNING. FOR-PROFIT USE OF + * THIS SOFTWARE, INCLUDING, BUT NOT LIMITED TO, SELLING THE SOFTWARE ON ITS OWN OR + * AS PART OF TOOLS AND/OR SERVICES IS PROHIBITED. COMMERCIAL USE OR COMMERCIAL + * DISTRIBUTION OF THIS SOFTWARE OR OF ANY DERIVATES IS PROHIBITED. + * + * Apart from that, this software is licensed under the + * GNU Affero Public License version 3 (AGPLv3). + * + * https://www.gnu.org/licenses/agpl-3.0.txt + * + * 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. + */ + +package de.learnlib.oracle.equivalence.mmlt; + +import de.learnlib.oracle.EquivalenceOracle; +import de.learnlib.oracle.TimedQueryOracle; +import de.learnlib.query.DefaultQuery; +import de.learnlib.statistic.container.DummyStatsContainer; +import de.learnlib.statistic.container.LearnerStatsProvider; +import de.learnlib.statistic.container.StatsContainerX; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealyOutputSymbol; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.alphabet.time.mmlt.TimeStepSymbol; +import net.automatalib.alphabet.time.mmlt.TimeoutSymbol; +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 Location type + * @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 TimedQueryOracle timeOracle; + + private StatsContainerX stats = new DummyStatsContainer(); + + private final Random random; + private final int minSize; + private final int rndLen; + private final int bound; + + public LocalTimerMealyRandomWpOracle(TimedQueryOracle 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, @Nullable Collection> ignored) { + // Make expanded form of hypothesis: + var hypSemModel = ReducedLocalTimerMealySemantics.forLocalTimerMealy(hypothesis); + + // Create a list of symbols (for faster access): + List> listAlphabet = new ArrayList<>(hypothesis.getUntimedAlphabet()); + listAlphabet.add(new TimeoutSymbol<>()); + listAlphabet.add(new TimeStepSymbol<>()); + + // Identify global suffixes: + var globalSuffixes = Automata.characterizingSet(hypSemModel, hypSemModel.getInputAlphabet()); + + // Get list of prefixes in deterministic order (so we can reproduce experiment easily): + var locationCover = LocalTimerMealyCover.getLocalTimerMealyLocationCover(hypothesis); + 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; // expected SUL output + } + } + + return null; // no counterexample found + } + + 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, hypSemModel.getInputAlphabet(), 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(StatsContainerX 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 index b942a2a37..1d82bfd4b 100644 --- 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 @@ -40,7 +40,7 @@ * @param Input type for non-delaying inputs * @param Output symbol type */ -public class LocalTimerMealySimulatorOracle implements EquivalenceOracle, LocalTimerMealySemanticInputSymbol, Word>> { +public class LocalTimerMealySimulatorOracle implements EquivalenceOracle.LocalTimerMealyEquivalenceOracle { private final LocalTimerMealy refModel; @@ -50,7 +50,7 @@ public LocalTimerMealySimulatorOracle(LocalTimerMealy refModel) { @Override - public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, Collection> ignored) { + public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, @Nullable Collection> ignored) { var separatingWord = LocalTimerMealyUtil.findSeparatingWord(refModel, hypothesis); if (separatingWord != 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 index 6557f76b8..9cd044406 100644 --- 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 @@ -50,7 +50,7 @@ * @param Input type for non-delaying inputs * @param Output symbol type */ -public class ResetSearchOracle implements EquivalenceOracle, LocalTimerMealySemanticInputSymbol, Word>> { +public class ResetSearchOracle implements EquivalenceOracle.LocalTimerMealyEquivalenceOracle { private final static Logger logger = LoggerFactory.getLogger(ResetSearchOracle.class); @@ -90,7 +90,7 @@ private List> getLoopingSymbols(S sourceLo } @Override - public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, Collection> ignored) { + public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, @Nullable Collection> ignored) { if (loopInsertPerc == 0) { return null; // oracle is disabled } From c78cd7bded429e622c18b2d1d2aeaf2013ebeeac Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Thu, 9 Oct 2025 13:13:22 +0200 Subject: [PATCH 06/27] Created module for symbol filters; added tests for counterexample handling for MMLT learner. --- algorithms/active/lstar/pom.xml | 10 + .../lstar/mmlt/LStarLocalTimerMealy.java | 8 +- .../mmlt/LocalTimerMealyObservationTable.java | 20 +- ...lTimerMealyCounterexampleDecompositor.java | 6 +- .../LocalTimerMealyCounterexampleHandler.java | 6 +- ...alTimerMealyInconsPrefixTransformAcex.java | 6 +- ...tarLocalTimerMealyCounterexampleTests.java | 245 ++++++++++++++++++ .../lstar/mmlt/LocalTimerMealyTestUtil.java | 51 ++++ .../test/resources/mmlt/ambiguous_minimal.dot | 21 ++ .../src/test/resources/mmlt/greedy_fail.dot | 14 + .../test/resources/mmlt/initial_value_one.dot | 15 ++ .../resources/mmlt/isolated_loc_permanent.dot | 19 ++ .../test/resources/mmlt/isolated_loc_temp.dot | 27 ++ .../mmlt/missing_oneshot_high_waiting.dot | 17 ++ .../test/resources/mmlt/over_approx_reset.dot | 17 ++ .../test/resources/mmlt/recursive_decomp.dot | 37 +++ .../resources/mmlt/same_initial_value.dot | 13 + .../test/resources/mmlt/sensor_collector.dot | 26 ++ .../src/test/resources/mmlt/syntax_demo.dot | 20 ++ .../test/resources/mmlt/unique_minimal.dot | 18 ++ ...cle.java => AbstractTimedQueryOracle.java} | 2 +- .../mmlt/LocalTimerMealyRandomWpOracle.java | 8 +- .../equivalence/mmlt/ResetSearchOracle.java | 6 +- .../oracle/membership/TimedQueryOracle.java | 3 +- oracles/pom.xml | 1 + oracles/symbol-filters/pom.xml | 88 +++++++ .../mmlt/AcceptAllSymbolFilter.java | 25 ++ .../mmlt/CachedSymbolFilter.java | 47 ++++ .../mmlt/IgnoreAllSymbolFilter.java | 25 ++ .../mmlt/PerfectSymbolFilter.java | 65 +++++ .../mmlt/RandomSymbolFilter.java | 65 +++++ .../mmlt/StatisticsSymbolFilter.java | 64 +++++ pom.xml | 1 + 33 files changed, 964 insertions(+), 32 deletions(-) create mode 100644 algorithms/active/lstar/src/test/java/de/learnlib/algorithm/lstar/mmlt/LStarLocalTimerMealyCounterexampleTests.java create mode 100644 algorithms/active/lstar/src/test/java/de/learnlib/algorithm/lstar/mmlt/LocalTimerMealyTestUtil.java create mode 100644 algorithms/active/lstar/src/test/resources/mmlt/ambiguous_minimal.dot create mode 100644 algorithms/active/lstar/src/test/resources/mmlt/greedy_fail.dot create mode 100644 algorithms/active/lstar/src/test/resources/mmlt/initial_value_one.dot create mode 100644 algorithms/active/lstar/src/test/resources/mmlt/isolated_loc_permanent.dot create mode 100644 algorithms/active/lstar/src/test/resources/mmlt/isolated_loc_temp.dot create mode 100644 algorithms/active/lstar/src/test/resources/mmlt/missing_oneshot_high_waiting.dot create mode 100644 algorithms/active/lstar/src/test/resources/mmlt/over_approx_reset.dot create mode 100644 algorithms/active/lstar/src/test/resources/mmlt/recursive_decomp.dot create mode 100644 algorithms/active/lstar/src/test/resources/mmlt/same_initial_value.dot create mode 100644 algorithms/active/lstar/src/test/resources/mmlt/sensor_collector.dot create mode 100644 algorithms/active/lstar/src/test/resources/mmlt/syntax_demo.dot create mode 100644 algorithms/active/lstar/src/test/resources/mmlt/unique_minimal.dot rename api/src/main/java/de/learnlib/oracle/{TimedQueryOracle.java => AbstractTimedQueryOracle.java} (94%) create mode 100644 oracles/symbol-filters/pom.xml create mode 100644 oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/AcceptAllSymbolFilter.java create mode 100644 oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/CachedSymbolFilter.java create mode 100644 oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/IgnoreAllSymbolFilter.java create mode 100644 oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/PerfectSymbolFilter.java create mode 100644 oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/RandomSymbolFilter.java create mode 100644 oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/StatisticsSymbolFilter.java diff --git a/algorithms/active/lstar/pom.xml b/algorithms/active/lstar/pom.xml index c785db71e..0a9b0e37c 100644 --- a/algorithms/active/lstar/pom.xml +++ b/algorithms/active/lstar/pom.xml @@ -124,6 +124,16 @@ limitations under the License. org.testng testng + + de.learnlib + learnlib-symbol-filters + test + + + net.automatalib + automata-serialization-dot + test + 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 index acd23c388..8fcbbc8de 100644 --- 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 @@ -17,7 +17,7 @@ import de.learnlib.datastructure.observationtable.OTLearner; import de.learnlib.datastructure.observationtable.ObservationTable; import de.learnlib.datastructure.observationtable.Row; -import de.learnlib.oracle.TimedQueryOracle; +import de.learnlib.oracle.AbstractTimedQueryOracle; import de.learnlib.query.DefaultQuery; import de.learnlib.statistic.container.DummyStatsContainer; import de.learnlib.statistic.container.LearnerStatsProvider; @@ -54,7 +54,7 @@ public class LStarLocalTimerMealy implements OTLearner, ? super Word>> closingStrategy; - private final TimedQueryOracle timeOracle; + private final AbstractTimedQueryOracle timeOracle; private final SymbolFilter symbolFilter; private final LStarLocalTimerMealyHypDataContainer hypData; @@ -81,7 +81,7 @@ public LStarLocalTimerMealy(Alphabet> alph LocalTimerMealyModelParams modelParams, @NonNull List>> initialSuffixes, - TimedQueryOracle timeOracle, + AbstractTimedQueryOracle timeOracle, SymbolFilter symbolFilter) { this(alphabet, modelParams, initialSuffixes, ClosingStrategies.CLOSE_SHORTEST, timeOracle, symbolFilter, AcexAnalyzers.BINARY_SEARCH_BWD); } @@ -102,7 +102,7 @@ public LStarLocalTimerMealy(Alphabet> alph @NonNull List>> initialSuffixes, ClosingStrategy, ? super Word>> closingStrategy, - TimedQueryOracle timeOracle, + AbstractTimedQueryOracle timeOracle, @NonNull SymbolFilter symbolFilter, AcexAnalyzer analyzer) { 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 index ccc4bddb7..8a39c3981 100644 --- 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 @@ -3,7 +3,7 @@ import de.learnlib.datastructure.observationtable.MutableObservationTable; import de.learnlib.datastructure.observationtable.Row; import de.learnlib.datastructure.observationtable.RowImpl; -import de.learnlib.oracle.TimedQueryOracle; +import de.learnlib.oracle.AbstractTimedQueryOracle; import de.learnlib.oracle.MembershipOracle; import de.learnlib.symbol_filter.SymbolFilter; import de.learnlib.symbol_filter.SymbolFilterResponse; @@ -81,7 +81,7 @@ public LocalTimerMealyObservationTable(Alphabet location, TimedQueryOracle timeOracle) { + private void identifyLocalTimers(LocationTimerInfo location, AbstractTimedQueryOracle timeOracle) { var timerQueryResponse = timeOracle.queryTimers(location.getPrefix(), this.minTimerQueryWaitTime); if (timerQueryResponse.aborted()) { @@ -137,7 +137,7 @@ private RowImpl> addInitialLocation() { * @param newRow Newly-added short prefix row * @param timeOracle Time oracle */ - private void initLocation(RowImpl> newRow, TimedQueryOracle timeOracle) { + private void initLocation(RowImpl> newRow, AbstractTimedQueryOracle timeOracle) { LocationTimerInfo timerInfo = new LocationTimerInfo<>(newRow.getLabel()); this.identifyLocalTimers(timerInfo, timeOracle); @@ -161,7 +161,7 @@ private void initLocation(RowImpl> newRow, * @param timeOracle Time query oracle * @return New transitions */ - private List>> createOutgoingTransitions(RowImpl> spRow, TimedQueryOracle timeOracle) { + private List>> createOutgoingTransitions(RowImpl> spRow, AbstractTimedQueryOracle timeOracle) { List>> transitions = new ArrayList<>(); Word> sp = spRow.getLabel(); @@ -294,7 +294,7 @@ public List>>> initialize(List timedOracle)) { + if (!(oracle instanceof AbstractTimedQueryOracle timedOracle)) { throw new IllegalArgumentException("Must use timed oracle!"); } @@ -314,7 +314,7 @@ public List>>> initialize(List> row, TimedQueryOracle timedOracle) { + private void queryAllSuffixes(RowImpl> row, AbstractTimedQueryOracle timedOracle) { Word> prefix = row.getLabel(); List>> suffixOutputs = new ArrayList<>(this.suffixes.size()); @@ -350,7 +350,7 @@ public boolean isInitialConsistencyCheckRequired() { @Override public List>>> addSuffixes(Collection>> newSuffixes, MembershipOracle, Word>> oracle) { - if (!(oracle instanceof TimedQueryOracle timedOracle)) { + if (!(oracle instanceof AbstractTimedQueryOracle timedOracle)) { throw new IllegalArgumentException(); } @@ -394,7 +394,7 @@ public List>>> addShortPrefixes(L @Override public List>>> toShortPrefixes(List>> lpRows, MembershipOracle, Word>> oracle) { - if (!(oracle instanceof TimedQueryOracle timedOracle)) { + if (!(oracle instanceof AbstractTimedQueryOracle timedOracle)) { throw new IllegalArgumentException(); } @@ -516,7 +516,7 @@ public LocationTimerInfo getLocationTimerInfo(Row>>> addOutgoingTransition(Row> spRow, LocalTimerMealySemanticInputSymbol symbol, TimedQueryOracle timeOracle) { + public List>>> addOutgoingTransition(Row> spRow, LocalTimerMealySemanticInputSymbol symbol, AbstractTimedQueryOracle timeOracle) { if (!this.alphabet.containsSymbol(symbol)) { throw new IllegalArgumentException("Unknown symbol."); } @@ -540,7 +540,7 @@ public List>>> addOutgoingTransit return this.findUnclosedTransitions(); } - public List>>> addTimerTransition(Row> spRow, MealyTimerInfo timeout, TimedQueryOracle timeOracle) { + public List>>> addTimerTransition(Row> spRow, MealyTimerInfo timeout, AbstractTimedQueryOracle timeOracle) { return this.addOutgoingTransition(spRow, new TimeStepSequence<>(timeout.initial()), timeOracle); } 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 index e82c123e9..275bacfff 100644 --- 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 @@ -2,7 +2,7 @@ import de.learnlib.acex.AcexAnalyzer; import de.learnlib.algorithm.lstar.mmlt.hyp.LocalTimerMealyHypothesis; -import de.learnlib.oracle.TimedQueryOracle; +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; @@ -24,10 +24,10 @@ class LocalTimerMealyCounterexampleDecompositor { private static final Logger logger = LoggerFactory.getLogger(LocalTimerMealyCounterexampleDecompositor.class); - private final TimedQueryOracle timeOracle; + private final AbstractTimedQueryOracle timeOracle; private final AcexAnalyzer acexAnalyzer; - public LocalTimerMealyCounterexampleDecompositor(TimedQueryOracle timeOracle, AcexAnalyzer acexAnalyzer) { + public LocalTimerMealyCounterexampleDecompositor(AbstractTimedQueryOracle timeOracle, AcexAnalyzer acexAnalyzer) { this.timeOracle = timeOracle; this.acexAnalyzer = acexAnalyzer; } 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 index 9ee73e305..487b3509c 100644 --- 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 @@ -4,7 +4,7 @@ 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.TimedQueryOracle; +import de.learnlib.oracle.AbstractTimedQueryOracle; import de.learnlib.statistic.container.DummyStatsContainer; import de.learnlib.statistic.container.LearnerStatsProvider; import de.learnlib.statistic.container.StatsContainerX; @@ -34,10 +34,10 @@ public class LocalTimerMealyCounterexampleHandler implements LearnerSta private final SymbolFilter symbolFilter; private StatsContainerX stats = new DummyStatsContainer(); - protected final TimedQueryOracle timeOracle; + protected final AbstractTimedQueryOracle timeOracle; private final LocalTimerMealyCounterexampleDecompositor decompositor; - public LocalTimerMealyCounterexampleHandler(TimedQueryOracle timeOracle, AcexAnalyzer acexAnalyzer, SymbolFilter symbolFilter) { + public LocalTimerMealyCounterexampleHandler(AbstractTimedQueryOracle timeOracle, AcexAnalyzer acexAnalyzer, SymbolFilter symbolFilter) { this.timeOracle = timeOracle; this.decompositor = new LocalTimerMealyCounterexampleDecompositor<>(timeOracle, acexAnalyzer); this.symbolFilter = symbolFilter; 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 index cc1942d08..2430aa394 100644 --- 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 @@ -1,7 +1,7 @@ package de.learnlib.algorithm.lstar.mmlt.cex; import de.learnlib.acex.AbstractBaseCounterexample; -import de.learnlib.oracle.TimedQueryOracle; +import de.learnlib.oracle.AbstractTimedQueryOracle; import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; import net.automatalib.alphabet.time.mmlt.LocalTimerMealyOutputSymbol; import net.automatalib.word.Word; @@ -20,7 +20,7 @@ public class LocalTimerMealyInconsPrefixTransformAcex extends AbstractBase private final static Logger logger = LoggerFactory.getLogger(LocalTimerMealyInconsPrefixTransformAcex.class); - private final TimedQueryOracle timeOracle; + private final AbstractTimedQueryOracle timeOracle; private final Word> suffix; private final Function>, Word>> asTransform; @@ -32,7 +32,7 @@ public class LocalTimerMealyInconsPrefixTransformAcex extends AbstractBase * @param timeOracle membership oracle * @param asTransform retrieves the prefix of the system state in the hypothesis addressed by a word */ - public LocalTimerMealyInconsPrefixTransformAcex(Word> suffix, TimedQueryOracle timeOracle, Function>, Word>> asTransform) { + public LocalTimerMealyInconsPrefixTransformAcex(Word> suffix, AbstractTimedQueryOracle timeOracle, Function>, Word>> asTransform) { super(suffix.length()); this.timeOracle = timeOracle; this.suffix = suffix; 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 000000000..5bb29d384 --- /dev/null +++ b/algorithms/active/lstar/src/test/java/de/learnlib/algorithm/lstar/mmlt/LStarLocalTimerMealyCounterexampleTests.java @@ -0,0 +1,245 @@ +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.mmlt.AcceptAllSymbolFilter; +import de.learnlib.query.DefaultQuery; +import de.learnlib.datastructure.observationtable.writer.ObservationTableASCIIWriter; +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. + */ +public class LStarLocalTimerMealyCounterexampleTests { + + private static void learnModel(LocalTimerMealyTestUtil.Model 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 = LocalTimerMealyTestUtil.automatonFromFile("sensor_collector.dot"); + + // 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 = LocalTimerMealyTestUtil.automatonFromFile("sensor_collector.dot", 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 = LocalTimerMealyTestUtil.automatonFromFile("sensor_collector.dot", 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 = LocalTimerMealyTestUtil.automatonFromFile("sensor_collector.dot", 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 000000000..6c398b4c0 --- /dev/null +++ b/algorithms/active/lstar/src/test/java/de/learnlib/algorithm/lstar/mmlt/LocalTimerMealyTestUtil.java @@ -0,0 +1,51 @@ +package de.learnlib.algorithm.lstar.mmlt; + +import de.learnlib.algorithm.LocalTimerMealyModelParams; +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; + +/** + * Utility class for loading MMLTs from resources and printing them. + */ +class LocalTimerMealyTestUtil { + + record Model(LocalTimerMealy automaton, LocalTimerMealyModelParams params) { + + } + + static void printModel(LocalTimerMealy model) { + try { + GraphDOT.write(model.transitionGraphView(true, true), System.out); + } catch (IOException ignored) { + } + } + + + static Model 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 Model 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 Model<>(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 000000000..115f68259 --- /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 000000000..21d0cf0ea --- /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 000000000..3e3a79932 --- /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 000000000..1a6e3e082 --- /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 000000000..0b6e851b2 --- /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 000000000..bde3ff611 --- /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 000000000..3e08b3fbe --- /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 000000000..67120a67d --- /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 000000000..17ec7ede6 --- /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/sensor_collector.dot b/algorithms/active/lstar/src/test/resources/mmlt/sensor_collector.dot new file mode 100644 index 000000000..240314743 --- /dev/null +++ b/algorithms/active/lstar/src/test/resources/mmlt/sensor_collector.dot @@ -0,0 +1,26 @@ +// This sensor node is used in the diss as running example. +// p1 starts a normal measurement. part triggers a sensor for particulate matter, noise a sensor for ambient noise. +// p2 performs a self-check. +// Set maximum waiting time = 40 to reproduce bad hypothesis from diss. +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 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 000000000..2379c825b --- /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 000000000..20749808c --- /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/oracle/TimedQueryOracle.java b/api/src/main/java/de/learnlib/oracle/AbstractTimedQueryOracle.java similarity index 94% rename from api/src/main/java/de/learnlib/oracle/TimedQueryOracle.java rename to api/src/main/java/de/learnlib/oracle/AbstractTimedQueryOracle.java index 3d3a92947..f36a3fbcc 100644 --- a/api/src/main/java/de/learnlib/oracle/TimedQueryOracle.java +++ b/api/src/main/java/de/learnlib/oracle/AbstractTimedQueryOracle.java @@ -19,7 +19,7 @@ * @param Input type for non-delaying inputs * @param Output symbol type */ -public abstract class TimedQueryOracle implements MembershipOracle.MealyMembershipOracle, LocalTimerMealyOutputSymbol> { +public abstract class AbstractTimedQueryOracle implements MembershipOracle.MealyMembershipOracle, LocalTimerMealyOutputSymbol> { /** * Response for a timer query. 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 index c28de0323..364c8bde7 100644 --- 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 @@ -22,7 +22,7 @@ package de.learnlib.oracle.equivalence.mmlt; import de.learnlib.oracle.EquivalenceOracle; -import de.learnlib.oracle.TimedQueryOracle; +import de.learnlib.oracle.AbstractTimedQueryOracle; import de.learnlib.query.DefaultQuery; import de.learnlib.statistic.container.DummyStatsContainer; import de.learnlib.statistic.container.LearnerStatsProvider; @@ -54,7 +54,7 @@ */ public class LocalTimerMealyRandomWpOracle implements EquivalenceOracle.LocalTimerMealyEquivalenceOracle, LearnerStatsProvider { private static final Logger logger = LoggerFactory.getLogger(LocalTimerMealyRandomWpOracle.class); - private final TimedQueryOracle timeOracle; + private final AbstractTimedQueryOracle timeOracle; private StatsContainerX stats = new DummyStatsContainer(); @@ -63,7 +63,7 @@ public class LocalTimerMealyRandomWpOracle implements EquivalenceOracle private final int rndLen; private final int bound; - public LocalTimerMealyRandomWpOracle(TimedQueryOracle timeOracle, + public LocalTimerMealyRandomWpOracle(AbstractTimedQueryOracle timeOracle, long randomSeed, int minSize, int rndAddLength, int bound) { @@ -89,7 +89,7 @@ public LocalTimerMealyRandomWpOracle(TimedQueryOracle timeOracle, // Identify global suffixes: var globalSuffixes = Automata.characterizingSet(hypSemModel, hypSemModel.getInputAlphabet()); - // Get list of prefixes in deterministic order (so we can reproduce experiment easily): + // Get list of prefixes in deterministic order (so we can reproduce experiments easily): var locationCover = LocalTimerMealyCover.getLocalTimerMealyLocationCover(hypothesis); var prefixList = locationCover .values() 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 index 9cd044406..b3fdf9e94 100644 --- 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 @@ -21,7 +21,7 @@ package de.learnlib.oracle.equivalence.mmlt; -import de.learnlib.oracle.TimedQueryOracle; +import de.learnlib.oracle.AbstractTimedQueryOracle; import de.learnlib.oracle.EquivalenceOracle; import de.learnlib.query.DefaultQuery; import net.automatalib.alphabet.time.mmlt.*; @@ -54,7 +54,7 @@ public class ResetSearchOracle implements EquivalenceOracle.LocalTimerM private final static Logger logger = LoggerFactory.getLogger(ResetSearchOracle.class); - private final TimedQueryOracle timeOracle; + private final AbstractTimedQueryOracle timeOracle; private final Random locPrefixRandom; private final double loopInsertPerc; @@ -62,7 +62,7 @@ public class ResetSearchOracle implements EquivalenceOracle.LocalTimerM private final long loopingInputSelectionSeed; - public ResetSearchOracle(TimedQueryOracle timeOracle, long seed, double loopInsertPerc, double testedLocPerc) { + public ResetSearchOracle(AbstractTimedQueryOracle timeOracle, long seed, double loopInsertPerc, double testedLocPerc) { this.timeOracle = timeOracle; this.locPrefixRandom = new Random(seed); this.loopInsertPerc = loopInsertPerc; 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 index 20a01f057..712bdd231 100644 --- 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 @@ -1,6 +1,7 @@ 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.*; @@ -20,7 +21,7 @@ * @param Input type for non-delaying inputs * @param Output symbol type */ -public class TimedQueryOracle extends de.learnlib.oracle.TimedQueryOracle { +public class TimedQueryOracle extends AbstractTimedQueryOracle { private final static Logger logger = LoggerFactory.getLogger(TimedQueryOracle.class); diff --git a/oracles/pom.xml b/oracles/pom.xml index acc5a8e8f..9fe0cb944 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 000000000..ec0777f2f --- /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/mmlt/AcceptAllSymbolFilter.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/AcceptAllSymbolFilter.java new file mode 100644 index 000000000..1353f7022 --- /dev/null +++ b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/AcceptAllSymbolFilter.java @@ -0,0 +1,25 @@ +package de.learnlib.oracle.symbol_filters.mmlt; + + +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 accepts all inputs. + * + * @param Input type for non-delaying inputs + */ +public class AcceptAllSymbolFilter implements SymbolFilter { + @Override + public SymbolFilterResponse query(Word> prefix, NonDelayingInput symbol) { + return SymbolFilterResponse.ACCEPT; + } + + @Override + public void update(Word> prefix, NonDelayingInput symbol, SymbolFilterResponse response) { + throw new IllegalStateException("Not supported."); + } +} diff --git a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/CachedSymbolFilter.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/CachedSymbolFilter.java new file mode 100644 index 000000000..394ba1cdf --- /dev/null +++ b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/CachedSymbolFilter.java @@ -0,0 +1,47 @@ +package de.learnlib.oracle.symbol_filters.mmlt; + + +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; + +import java.util.HashMap; +import java.util.Map; + +/** + * Wrapper for a symbol filter that caches previous responses + allows caller to update these. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class CachedSymbolFilter implements SymbolFilter { + private final Map>, Boolean> previousResponses; // transition -> legal/ignore + private final SymbolFilter delegate; + + public CachedSymbolFilter(SymbolFilter delegate) { + this.delegate = delegate; + this.previousResponses = new HashMap<>(); + } + + @Override + public SymbolFilterResponse query(Word> prefix, NonDelayingInput symbol) { + var oldResponse = this.previousResponses.get(prefix.append(symbol)); + if (oldResponse != null) { + return (oldResponse) ? SymbolFilterResponse.ACCEPT : SymbolFilterResponse.IGNORE; + } + + var res = delegate.query(prefix, symbol); + this.previousResponses.put(prefix.append(symbol), res == SymbolFilterResponse.ACCEPT); + return res; + } + + @Override + public void update(Word> prefix, NonDelayingInput symbol, SymbolFilterResponse response) { + if (!this.previousResponses.containsKey(prefix.append(symbol))) { + throw new IllegalArgumentException("Can only update response if already queried."); + } + this.previousResponses.put(prefix.append(symbol), (response == SymbolFilterResponse.ACCEPT)); + } +} diff --git a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/IgnoreAllSymbolFilter.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/IgnoreAllSymbolFilter.java new file mode 100644 index 000000000..2d3530be3 --- /dev/null +++ b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/IgnoreAllSymbolFilter.java @@ -0,0 +1,25 @@ +package de.learnlib.oracle.symbol_filters.mmlt; + +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 symbol filter that ignores all symbols. + * + * @param Input type for non-delaying inputs + */ +public class IgnoreAllSymbolFilter implements SymbolFilter { + @Override + public SymbolFilterResponse query(Word> prefix, NonDelayingInput symbol) { + return SymbolFilterResponse.IGNORE; + } + + @Override + public void update(Word> prefix, NonDelayingInput symbol, SymbolFilterResponse response) { + throw new IllegalStateException("Not supported."); + } + +} diff --git a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/PerfectSymbolFilter.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/PerfectSymbolFilter.java new file mode 100644 index 000000000..413731b54 --- /dev/null +++ b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/PerfectSymbolFilter.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2023-2024 Paul Kogel, TU Berlin + * + * THIS SOFTWARE IS INTENDED FOR ACADEMIC, RESEARCH AND EDUCATIONAL PURPOSES ONLY, + * MOST PROMINENTLY TO FACILITATE RESEARCH ON AUTOMATA LEARNING. FOR-PROFIT USE OF + * THIS SOFTWARE, INCLUDING, BUT NOT LIMITED TO, SELLING THE SOFTWARE ON ITS OWN OR + * AS PART OF TOOLS AND/OR SERVICES IS PROHIBITED. COMMERCIAL USE OR COMMERCIAL + * DISTRIBUTION OF THIS SOFTWARE OR OF ANY DERIVATES IS PROHIBITED. + * + * Apart from that, this software is licensed under the + * GNU Affero Public License version 3 (AGPLv3). + * + * https://www.gnu.org/licenses/agpl-3.0.txt + * + * 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. + */ + +package de.learnlib.oracle.symbol_filters.mmlt; + + +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; + +/** + * A symbol filter that correctly accepts and ignores all transitions. + * + * @param Location type + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public class PerfectSymbolFilter implements SymbolFilter { + + private final LocalTimerMealy sulModel; + + public PerfectSymbolFilter(LocalTimerMealy sulModel) { + this.sulModel = sulModel; + } + + @Override + public SymbolFilterResponse query(Word> prefix, NonDelayingInput symbol) { + // Check if silent self-loop: + + var targetConfig = this.sulModel.getSemantics().traceInputs(prefix); + var trans = this.sulModel.getSemantics().getTransition(targetConfig, symbol); + + if (trans.output().equals(sulModel.getSemantics().getSilentOutput()) && targetConfig.equals(trans.target())) { + return SymbolFilterResponse.IGNORE; + } else { + return SymbolFilterResponse.ACCEPT; + } + } + + @Override + public void update(Word> prefix, NonDelayingInput symbol, SymbolFilterResponse response) { + throw new IllegalStateException("Not supported."); + } +} diff --git a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/RandomSymbolFilter.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/RandomSymbolFilter.java new file mode 100644 index 000000000..f5216d52f --- /dev/null +++ b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/RandomSymbolFilter.java @@ -0,0 +1,65 @@ +package de.learnlib.oracle.symbol_filters.mmlt; + + +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; + +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 RandomSymbolFilter implements SymbolFilter { + + private final double inaccurateProb; + private final Random random; + private final LocalTimerMealy sulModel; + + public RandomSymbolFilter(LocalTimerMealy sulModel, + 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; + + this.sulModel = sulModel; + } + + private boolean isSilentSelfLoop(Word> prefix, NonDelayingInput symbol) { + var targetConfig = this.sulModel.getSemantics().traceInputs(prefix); + var trans = this.sulModel.getSemantics().getTransition(targetConfig, symbol); + return trans.output().equals(sulModel.getSemantics().getSilentOutput()) && targetConfig.equals(trans.target()); + } + + @Override + public SymbolFilterResponse query(Word> prefix, NonDelayingInput symbol) { + // Check if silent self-loop: + boolean ignorable = isSilentSelfLoop(prefix, symbol); + + // 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, NonDelayingInput symbol, SymbolFilterResponse response) { + throw new IllegalStateException("Not supported."); + } +} diff --git a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/StatisticsSymbolFilter.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/StatisticsSymbolFilter.java new file mode 100644 index 000000000..115eeaadd --- /dev/null +++ b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/StatisticsSymbolFilter.java @@ -0,0 +1,64 @@ +package de.learnlib.oracle.symbol_filters.mmlt; + +import de.learnlib.statistic.container.DummyStatsContainer; +import de.learnlib.statistic.container.LearnerStatsProvider; +import de.learnlib.statistic.container.StatsContainerX; +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; + +/** + * Collects various statistics on symbol filtering, including false accepts + false ignores. + * + * @param Input type for non-delaying inputs + */ +public class StatisticsSymbolFilter implements SymbolFilter, LearnerStatsProvider { + + private final SymbolFilter delegate; + private final PerfectSymbolFilter perfectFilter; + private StatsContainerX stats = new DummyStatsContainer(); + + + public StatisticsSymbolFilter(SymbolFilter delegate, LocalTimerMealy sulModel) { + this.delegate = delegate; + this.perfectFilter = new PerfectSymbolFilter<>(sulModel); + } + + @Override + public SymbolFilterResponse query(Word> prefix, NonDelayingInput symbol) { + stats.increaseCounter("cnt_isf_queries", "Filter: queries"); + + SymbolFilterResponse filterResponse = this.delegate.query(prefix, symbol); + SymbolFilterResponse expectedResponse = this.perfectFilter.query(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, NonDelayingInput symbol, SymbolFilterResponse response) { + delegate.update(prefix, symbol, response); + } + + @Override + public void setStatsContainer(StatsContainerX container) { + this.stats = container; + } +} diff --git a/pom.xml b/pom.xml index 0b4f6f114..d3f5191f8 100644 --- a/pom.xml +++ b/pom.xml @@ -170,6 +170,7 @@ limitations under the License. filters oracles test-support + oracles/symbol-filters - @{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 index 8fcbbc8de..f3331e456 100644 --- 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 @@ -21,7 +21,7 @@ import de.learnlib.query.DefaultQuery; import de.learnlib.statistic.container.DummyStatsContainer; import de.learnlib.statistic.container.LearnerStatsProvider; -import de.learnlib.statistic.container.StatsContainerX; +import de.learnlib.statistic.container.StatsContainer; import de.learnlib.symbol_filter.SymbolFilter; import de.learnlib.symbol_filter.SymbolFilterResponse; import net.automatalib.alphabet.Alphabet; @@ -50,7 +50,7 @@ public class LStarLocalTimerMealy implements OTLearner, LocalTimerMealySemanticInputSymbol, Word>>, LearnerStatsProvider { private static final Logger logger = LoggerFactory.getLogger(LStarLocalTimerMealy.class); - private StatsContainerX stats = new DummyStatsContainer(); + private StatsContainer stats = new DummyStatsContainer(); private final ClosingStrategy, ? super Word>> closingStrategy; @@ -422,7 +422,7 @@ protected void completeConsistentTable(List implements LearnerStatsProvider { private static final Logger logger = LoggerFactory.getLogger(LocalTimerMealyCounterexampleHandler.class); private final SymbolFilter symbolFilter; - private StatsContainerX stats = new DummyStatsContainer(); + private StatsContainer stats = new DummyStatsContainer(); protected final AbstractTimedQueryOracle timeOracle; private final LocalTimerMealyCounterexampleDecompositor decompositor; @@ -44,7 +44,7 @@ public LocalTimerMealyCounterexampleHandler(AbstractTimedQueryOracle timeO } @Override - public void setStatsContainer(StatsContainerX container) { + public void setStatsContainer(StatsContainer container) { this.stats = container; } 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 000000000..e0bdd42a3 --- /dev/null +++ b/algorithms/active/lstar/src/test/java/de/learnlib/algorithm/lstar/mmlt/LStarLocalTimerMealyBenchmarkTests.java @@ -0,0 +1,167 @@ +package de.learnlib.algorithm.lstar.mmlt; + + +import de.learnlib.algorithm.LocalTimerMealyModelParams; +import de.learnlib.datastructure.observationtable.writer.ObservationTableASCIIWriter; +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.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.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.TimeoutSymbol; +import net.automatalib.automaton.time.mmlt.LocalTimerMealy; +import net.automatalib.word.Word; +import org.testng.annotations.Test; + +import de.learnlib.filter.cache.mmlt.LocalTimerMealyTreeCacheSUL; + +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("Model", 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); + LocalTimerMealyTreeCacheSUL cacheSUL = new LocalTimerMealyTreeCacheSUL<>(statsAfterCache, params.silentOutput()); + 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(new LocalTimerMealyCacheOracle<>(cacheSUL, params)); TODO! + chainOracle.addOracle(new ResetSearchOracle<>(timeOracle, seed, 1.0, 1.0)); + chainOracle.addOracle(new LocalTimerMealyRandomWpOracle<>(timeOracle, seed, 6, 12, 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 filter = new AcceptAllSymbolFilter<>(); // pass-through + switch (symbolFilterMode) { + case perfect -> filter = new PerfectSymbolFilter<>(automaton); + case random -> filter = new RandomSymbolFilter<>(automaton, 0.1, new Random(seed)); + case ignore_all -> filter = new IgnoreAllSymbolFilter<>(); + } + + filter = new StatisticsSymbolFilter<>(filter, automaton); + 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(modelFile, model.automaton(), model.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); + } + } + + @Test + public void learnExamplesPerfectFilter() { + for (String modelFile : LocalTimerMealyTestUtil.listModelFiles()) { + var model = LocalTimerMealyTestUtil.automatonFromFile(modelFile); + learnModel(modelFile, model.automaton(), model.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); + } + } + +} 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 index 6c398b4c0..8ef8edd6a 100644 --- 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 @@ -9,6 +9,13 @@ 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. @@ -26,6 +33,22 @@ static void printModel(LocalTimerMealy model) { } } + 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 Model automatonFromFile(String name) { return automatonFromFile(name, -1); diff --git a/api/src/main/java/de/learnlib/statistic/container/DummyStatsContainer.java b/api/src/main/java/de/learnlib/statistic/container/DummyStatsContainer.java index ca39e9d93..2066a7503 100644 --- a/api/src/main/java/de/learnlib/statistic/container/DummyStatsContainer.java +++ b/api/src/main/java/de/learnlib/statistic/container/DummyStatsContainer.java @@ -58,4 +58,9 @@ public void setCounter(String id, @Nullable String description, long count) { public Optional getCount(String id) { return Optional.empty(); } + + @Override + public void printStats() { + System.out.println("Dummy container"); + } } diff --git a/commons/util/src/main/java/module-info.java b/commons/util/src/main/java/module-info.java index e4b6a888c..0c15ae9d7 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/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/FastLocalTimerMealyTreeCacheSUL.java b/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyTreeCacheSUL.java similarity index 97% rename from filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/FastLocalTimerMealyTreeCacheSUL.java rename to filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyTreeCacheSUL.java index 5036f6f0a..26165e9e2 100644 --- a/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/FastLocalTimerMealyTreeCacheSUL.java +++ b/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyTreeCacheSUL.java @@ -26,7 +26,7 @@ * @param Input type for non-delaying inputs * @param Output symbol type */ -public class FastLocalTimerMealyTreeCacheSUL extends LocalTimerMealySUL implements GraphViewable, LearnerStatsProvider { +public class LocalTimerMealyTreeCacheSUL extends LocalTimerMealySUL implements GraphViewable, LearnerStatsProvider { private final LocalTimerMealySUL delegate; private final CacheTreeNode cacheRoot; @@ -42,7 +42,7 @@ public void setStatsContainer(StatsContainer container) { this.stats = container; } - public FastLocalTimerMealyTreeCacheSUL(LocalTimerMealySUL delegate, O silentOutput) { + public LocalTimerMealyTreeCacheSUL(LocalTimerMealySUL delegate, O silentOutput) { this.delegate = delegate; this.silentOutput = new LocalTimerMealyOutputSymbol<>(silentOutput); 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 000000000..520c89634 --- /dev/null +++ b/oracles/equivalence-oracles/src/main/java/de/learnlib/oracle/equivalence/mmlt/LocalTimerMealyEQOracleChain.java @@ -0,0 +1,90 @@ +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. + * + * @param Location type + * @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/module-info.java b/oracles/equivalence-oracles/src/main/java/module-info.java index 9c5d3a390..c7d83f328 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; } From b4bba2986db9f70e21f1588ea1d0811a84fbf19c Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Thu, 9 Oct 2025 14:40:36 +0200 Subject: [PATCH 11/27] Added more test models for the MMLT learner. --- .../lstar/mmlt/LocalTimerMealyTestUtil.java | 4 + .../lstar/src/test/resources/mmlt/HVAC.gv | 61 +++++ .../lstar/src/test/resources/mmlt/Oven.gv | 22 ++ .../lstar/src/test/resources/mmlt/SCTP.gv | 225 ++++++++++++++++++ .../lstar/src/test/resources/mmlt/WSN.gv | 22 ++ .../src/test/resources/mmlt/WashingMachine.gv | 186 +++++++++++++++ 6 files changed, 520 insertions(+) create mode 100644 algorithms/active/lstar/src/test/resources/mmlt/HVAC.gv create mode 100644 algorithms/active/lstar/src/test/resources/mmlt/Oven.gv create mode 100644 algorithms/active/lstar/src/test/resources/mmlt/SCTP.gv create mode 100644 algorithms/active/lstar/src/test/resources/mmlt/WSN.gv create mode 100644 algorithms/active/lstar/src/test/resources/mmlt/WashingMachine.gv 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 index 8ef8edd6a..765669919 100644 --- 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 @@ -68,6 +68,10 @@ static Model automatonFromFile(String name, int maxTime long maxTimeoutDelay = LocalTimerMealyUtil.getMaximumTimeoutDelay(automaton); long maxTimerQueryWaitingFinal = (maxTimerQueryWaiting > 0) ? maxTimerQueryWaiting : LocalTimerMealyUtil.getMaximumInitialTimerValue(automaton) * 2; + if (name.contains("SCTP")) { + maxTimerQueryWaitingFinal = 9000; // SCTP needs more waiting time + } + return new Model<>(automaton, new LocalTimerMealyModelParams<>("void", maxTimeoutDelay, maxTimerQueryWaitingFinal, StringSymbolCombiner.getInstance())); } diff --git a/algorithms/active/lstar/src/test/resources/mmlt/HVAC.gv b/algorithms/active/lstar/src/test/resources/mmlt/HVAC.gv new file mode 100644 index 000000000..5c176a909 --- /dev/null +++ b/algorithms/active/lstar/src/test/resources/mmlt/HVAC.gv @@ -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/algorithms/active/lstar/src/test/resources/mmlt/Oven.gv b/algorithms/active/lstar/src/test/resources/mmlt/Oven.gv new file mode 100644 index 000000000..3c934d0c0 --- /dev/null +++ b/algorithms/active/lstar/src/test/resources/mmlt/Oven.gv @@ -0,0 +1,22 @@ +// Model of an oven +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/algorithms/active/lstar/src/test/resources/mmlt/SCTP.gv b/algorithms/active/lstar/src/test/resources/mmlt/SCTP.gv new file mode 100644 index 000000000..f6308a512 --- /dev/null +++ b/algorithms/active/lstar/src/test/resources/mmlt/SCTP.gv @@ -0,0 +1,225 @@ +// Model of the 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/algorithms/active/lstar/src/test/resources/mmlt/WSN.gv b/algorithms/active/lstar/src/test/resources/mmlt/WSN.gv new file mode 100644 index 000000000..a8288e71d --- /dev/null +++ b/algorithms/active/lstar/src/test/resources/mmlt/WSN.gv @@ -0,0 +1,22 @@ +// Model of a wireless sensor node that collects data and transmits if +// if the battery has sufficient charge. +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/algorithms/active/lstar/src/test/resources/mmlt/WashingMachine.gv b/algorithms/active/lstar/src/test/resources/mmlt/WashingMachine.gv new file mode 100644 index 000000000..1e112ec4f --- /dev/null +++ b/algorithms/active/lstar/src/test/resources/mmlt/WashingMachine.gv @@ -0,0 +1,186 @@ +// Model of a washing machine +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; + +} From 6bcc46281f92567caa99db308f1376aa4d646244 Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Thu, 9 Oct 2025 14:47:41 +0200 Subject: [PATCH 12/27] Added more test models for the MMLT learner. --- .../lstar/mmlt/LStarLocalTimerMealyBenchmarkTests.java | 2 +- .../active/lstar/src/test/resources/mmlt/{HVAC.gv => HVAC.dot} | 0 .../active/lstar/src/test/resources/mmlt/{Oven.gv => Oven.dot} | 0 .../active/lstar/src/test/resources/mmlt/{SCTP.gv => SCTP.dot} | 0 .../active/lstar/src/test/resources/mmlt/{WSN.gv => WSN.dot} | 0 5 files changed, 1 insertion(+), 1 deletion(-) rename algorithms/active/lstar/src/test/resources/mmlt/{HVAC.gv => HVAC.dot} (100%) rename algorithms/active/lstar/src/test/resources/mmlt/{Oven.gv => Oven.dot} (100%) rename algorithms/active/lstar/src/test/resources/mmlt/{SCTP.gv => SCTP.dot} (100%) rename algorithms/active/lstar/src/test/resources/mmlt/{WSN.gv => WSN.dot} (100%) 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 index e0bdd42a3..c6a90f726 100644 --- 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 @@ -73,7 +73,7 @@ private static void runExperiment(LStarLocalTimerMealy learner, Equ if (printFinalResult) { System.out.println("Final hypothesis:"); LocalTimerMealyTestUtil.printModel(finalHypothesis); - new ObservationTableASCIIWriter<>().write(learner.getObservationTable(), System.out); + //new ObservationTableASCIIWriter<>().write(learner.getObservationTable(), System.out); } } diff --git a/algorithms/active/lstar/src/test/resources/mmlt/HVAC.gv b/algorithms/active/lstar/src/test/resources/mmlt/HVAC.dot similarity index 100% rename from algorithms/active/lstar/src/test/resources/mmlt/HVAC.gv rename to algorithms/active/lstar/src/test/resources/mmlt/HVAC.dot diff --git a/algorithms/active/lstar/src/test/resources/mmlt/Oven.gv b/algorithms/active/lstar/src/test/resources/mmlt/Oven.dot similarity index 100% rename from algorithms/active/lstar/src/test/resources/mmlt/Oven.gv rename to algorithms/active/lstar/src/test/resources/mmlt/Oven.dot diff --git a/algorithms/active/lstar/src/test/resources/mmlt/SCTP.gv b/algorithms/active/lstar/src/test/resources/mmlt/SCTP.dot similarity index 100% rename from algorithms/active/lstar/src/test/resources/mmlt/SCTP.gv rename to algorithms/active/lstar/src/test/resources/mmlt/SCTP.dot diff --git a/algorithms/active/lstar/src/test/resources/mmlt/WSN.gv b/algorithms/active/lstar/src/test/resources/mmlt/WSN.dot similarity index 100% rename from algorithms/active/lstar/src/test/resources/mmlt/WSN.gv rename to algorithms/active/lstar/src/test/resources/mmlt/WSN.dot From 150c31df6a7681a2747413817a8fe0ae568bed43 Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Thu, 9 Oct 2025 15:14:24 +0200 Subject: [PATCH 13/27] Added cache consistency test for MMLT learning. --- .../LStarLocalTimerMealyBenchmarkTests.java | 3 +- .../filter/cache/LocalTimerMealyCache.java | 24 +++ .../LocalTimerMealyCacheConsistencyTest.java | 167 ++++++++++++++++++ .../mmlt/LocalTimerMealyTreeCacheSUL.java | 9 +- 4 files changed, 199 insertions(+), 4 deletions(-) create mode 100644 filters/cache/src/main/java/de/learnlib/filter/cache/LocalTimerMealyCache.java create mode 100644 filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyCacheConsistencyTest.java 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 index c6a90f726..46ffc27c9 100644 --- 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 @@ -4,6 +4,7 @@ import de.learnlib.algorithm.LocalTimerMealyModelParams; import de.learnlib.datastructure.observationtable.writer.ObservationTableASCIIWriter; import de.learnlib.driver.simulator.LocalTimerMealySimulatorSUL; +import de.learnlib.filter.cache.mmlt.LocalTimerMealyCacheConsistencyTest; import de.learnlib.filter.cache.mmlt.TimeoutReducerSUL; import de.learnlib.filter.statistic.sul.LocalTimerMealyStatsSUL; import de.learnlib.oracle.EquivalenceOracle; @@ -102,7 +103,7 @@ private static void learnModel(String name, LocalTimerMealy a // Prepare cex oracle chain: LocalTimerMealyEQOracleChain chainOracle = new LocalTimerMealyEQOracleChain<>(); - //chainOracle.addOracle(new LocalTimerMealyCacheOracle<>(cacheSUL, params)); TODO! + chainOracle.addOracle(new LocalTimerMealyCacheConsistencyTest<>(cacheSUL, params)); chainOracle.addOracle(new ResetSearchOracle<>(timeOracle, seed, 1.0, 1.0)); chainOracle.addOracle(new LocalTimerMealyRandomWpOracle<>(timeOracle, seed, 6, 12, 100)); chainOracle.addOracle(new LocalTimerMealySimulatorOracle<>(automaton)); // ensure that we eventually find an accurate model diff --git a/filters/cache/src/main/java/de/learnlib/filter/cache/LocalTimerMealyCache.java b/filters/cache/src/main/java/de/learnlib/filter/cache/LocalTimerMealyCache.java new file mode 100644 index 000000000..7e9647bef --- /dev/null +++ b/filters/cache/src/main/java/de/learnlib/filter/cache/LocalTimerMealyCache.java @@ -0,0 +1,24 @@ +package de.learnlib.filter.cache; + +import de.learnlib.sul.LocalTimerMealySUL; +import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; +import net.automatalib.word.Word; + +import java.util.List; + +/** + * Abstract class for caches for {@link LocalTimerMealySUL}. + * + * @param Input type for non-delaying inputs + * @param Output symbol type + */ +public abstract class LocalTimerMealyCache extends LocalTimerMealySUL { + + /** + * Lists all words that are currently in the cache. + * + * @return List of all stored words. + */ + public abstract List>> listAllWords(); + +} 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 000000000..3e5d9998e --- /dev/null +++ b/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyCacheConsistencyTest.java @@ -0,0 +1,167 @@ +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.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; + +/** + * Searches for counterexamples by comparing the behavior of the hypothesis and the query cache. + * + * @param Location type + * @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 LocalTimerMealyTreeCacheSUL sulCache; + private final LocalTimerMealyModelParams modelParams; + + public LocalTimerMealyCacheConsistencyTest(LocalTimerMealyTreeCacheSUL 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 tau-sequences has several performance benefits. + * This function converts a query with a tau-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 -> no timeout: + 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 exceeding max delay OR non-wait symbol -> ignore rest of this word: + if (symIdx < queryInput.length() - 1) { + logger.warn("Ignoring at least one symbol during cache comparison."); + } + break; + } + } + } + return new DefaultQuery<>(wbInput.toWord(), wbOutput.toWord()); + } + + + @Override + public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, Collection> inputs) { + // TODO only with the provided inputs! + + // 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); + + // Finally, query hypothesis using the converted query: + Word> hypOutput = hypothesis.getSemantics().computeSuffixOutput(Word.epsilon(), convertedQuery.getInput()); + + if (!hypOutput.equals(convertedQuery.getOutput())) { + // Hyp gives different output than cache (= SUL): + counterexamples.add(convertedQuery); + } + } + + 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/LocalTimerMealyTreeCacheSUL.java b/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyTreeCacheSUL.java index 26165e9e2..01ece3bc3 100644 --- a/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyTreeCacheSUL.java +++ b/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyTreeCacheSUL.java @@ -1,6 +1,8 @@ package de.learnlib.filter.cache.mmlt; +import de.learnlib.filter.cache.LocalTimerMealyCache; +import de.learnlib.oracle.EquivalenceOracle; import de.learnlib.statistic.container.DummyStatsContainer; import de.learnlib.statistic.container.LearnerStatsProvider; import de.learnlib.statistic.container.StatsContainer; @@ -21,12 +23,12 @@ import java.util.*; /** - * Caches queries sent to an AbstractLocalTimerMealySUL. + * Caches queries sent to a LocalTimerMealySUL. * * @param Input type for non-delaying inputs * @param Output symbol type */ -public class LocalTimerMealyTreeCacheSUL extends LocalTimerMealySUL implements GraphViewable, LearnerStatsProvider { +public class LocalTimerMealyTreeCacheSUL extends LocalTimerMealyCache implements GraphViewable, LearnerStatsProvider { private final LocalTimerMealySUL delegate; private final CacheTreeNode cacheRoot; @@ -193,6 +195,7 @@ private List> getLeaves() { return leaves; } + @Override public List>> listAllWords() { List> leaves = this.getLeaves(); @@ -218,6 +221,7 @@ public List>> listAllWords() { return finalWords; } + @Override public Graph graphView() { // Convert tree to a mealy automaton: @@ -257,5 +261,4 @@ public List>> listAllWords() { return mealy.graphView(); } - } From 67e8695d2d749aa02236dda45cbc7ec482948d1f Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Fri, 10 Oct 2025 08:12:33 +0200 Subject: [PATCH 14/27] Removed location type parameter from LocalTimerMealyEquivalenceOracle. --- .../LStarLocalTimerMealyBenchmarkTests.java | 4 ++-- .../de/learnlib/oracle/EquivalenceOracle.java | 3 +-- .../LocalTimerMealyCacheConsistencyTest.java | 6 ++--- .../mmlt/LocalTimerMealyEQOracleChain.java | 12 ++++++---- .../mmlt/LocalTimerMealyRandomWpOracle.java | 23 +++++++++++-------- .../mmlt/LocalTimerMealySimulatorOracle.java | 4 ++-- .../equivalence/mmlt/ResetSearchOracle.java | 9 ++++---- 7 files changed, 32 insertions(+), 29 deletions(-) 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 index 46ffc27c9..0b41c556d 100644 --- 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 @@ -44,7 +44,7 @@ private enum FilterMode { none, random, ignore_all, perfect } - private static void runExperiment(LStarLocalTimerMealy learner, EquivalenceOracle.LocalTimerMealyEquivalenceOracle tester, StatsContainer stats, int maxRounds, + private static void runExperiment(LStarLocalTimerMealy learner, EquivalenceOracle.LocalTimerMealyEquivalenceOracle tester, StatsContainer stats, int maxRounds, boolean printFinalResult) { stats.startOrResumeClock("learningRt", "Processing time"); learner.startLearning(); @@ -102,7 +102,7 @@ private static void learnModel(String name, LocalTimerMealy a // Prepare cex oracle chain: - LocalTimerMealyEQOracleChain chainOracle = new LocalTimerMealyEQOracleChain<>(); + LocalTimerMealyEQOracleChain chainOracle = new LocalTimerMealyEQOracleChain<>(); chainOracle.addOracle(new LocalTimerMealyCacheConsistencyTest<>(cacheSUL, params)); chainOracle.addOracle(new ResetSearchOracle<>(timeOracle, seed, 1.0, 1.0)); chainOracle.addOracle(new LocalTimerMealyRandomWpOracle<>(timeOracle, seed, 6, 12, 100)); diff --git a/api/src/main/java/de/learnlib/oracle/EquivalenceOracle.java b/api/src/main/java/de/learnlib/oracle/EquivalenceOracle.java index 29500443e..31acb80bd 100644 --- a/api/src/main/java/de/learnlib/oracle/EquivalenceOracle.java +++ b/api/src/main/java/de/learnlib/oracle/EquivalenceOracle.java @@ -94,9 +94,8 @@ interface MooreEquivalenceOracle extends EquivalenceOracle Location type * @param Input type for non-delaying inputs * @param Output symbol type */ - interface LocalTimerMealyEquivalenceOracle extends EquivalenceOracle, LocalTimerMealySemanticInputSymbol, Word>>{} + interface LocalTimerMealyEquivalenceOracle extends EquivalenceOracle, LocalTimerMealySemanticInputSymbol, Word>>{} } 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 index 3e5d9998e..15b23df2e 100644 --- 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 @@ -19,11 +19,10 @@ /** * Searches for counterexamples by comparing the behavior of the hypothesis and the query cache. * - * @param Location type * @param Input type for non-delaying inputs * @param Output symbol type */ -public class LocalTimerMealyCacheConsistencyTest implements EquivalenceOracle.LocalTimerMealyEquivalenceOracle { +public class LocalTimerMealyCacheConsistencyTest implements EquivalenceOracle.LocalTimerMealyEquivalenceOracle { private final static Logger logger = LoggerFactory.getLogger(LocalTimerMealyCacheConsistencyTest.class); private final LocalTimerMealyTreeCacheSUL sulCache; @@ -134,7 +133,7 @@ private DefaultQuery, Word, Word>> findCounterExample(LocalTimerMealy hypothesis, Collection> inputs) { + public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, Collection> inputs) { // TODO only with the provided inputs! // Query all cached words: @@ -164,4 +163,5 @@ private DefaultQuery, Word w.getInput().length())).get(); } + } 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 index 520c89634..33fbeb6a2 100644 --- 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 @@ -18,16 +18,18 @@ /** * 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 Location type * @param Input type for non-delaying inputs * @param Output symbol type */ -public class LocalTimerMealyEQOracleChain implements EquivalenceOracle.LocalTimerMealyEquivalenceOracle, LearnerStatsProvider { +public class LocalTimerMealyEQOracleChain implements EquivalenceOracle.LocalTimerMealyEquivalenceOracle, LearnerStatsProvider { private static final Logger logger = LoggerFactory.getLogger(LocalTimerMealyEQOracleChain.class); - private final List> oracles = new ArrayList<>(); + private final List> oracles = new ArrayList<>(); private StatsContainer stats = new DummyStatsContainer(); /** @@ -35,7 +37,7 @@ public class LocalTimerMealyEQOracleChain implements EquivalenceOracle. */ private List oracleNames; - public void addOracle(LocalTimerMealyEquivalenceOracle oracle) { + public void addOracle(LocalTimerMealyEquivalenceOracle oracle) { this.oracles.add(oracle); // Update names: @@ -68,7 +70,7 @@ public void setStatsContainer(StatsContainer container) { } @Override - public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, Collection> inputs) { + 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; 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 index 4d2f38c56..ab27d8e81 100644 --- 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 @@ -48,11 +48,10 @@ * RandomWP counterexample search for MMLT learning. * Key modification: samples prefix from entry prefixes instead of all state prefixes. * - * @param Location type * @param Input type for non-delaying inputs * @param Output symbol type */ -public class LocalTimerMealyRandomWpOracle implements EquivalenceOracle.LocalTimerMealyEquivalenceOracle, LearnerStatsProvider { +public class LocalTimerMealyRandomWpOracle implements EquivalenceOracle.LocalTimerMealyEquivalenceOracle, LearnerStatsProvider { private static final Logger logger = LoggerFactory.getLogger(LocalTimerMealyRandomWpOracle.class); private final AbstractTimedQueryOracle timeOracle; @@ -77,7 +76,11 @@ public LocalTimerMealyRandomWpOracle(AbstractTimedQueryOracle timeOracle, } @Override - public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, @Nullable Collection> ignored) { + public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, @Nullable Collection> ignored) { + return findCounterExampleInternal(hypothesis); + } + + private DefaultQuery, Word>> findCounterExampleInternal(LocalTimerMealy hypothesis) { // Make expanded form of hypothesis: var hypSemModel = ReducedLocalTimerMealySemantics.forLocalTimerMealy(hypothesis); @@ -106,18 +109,18 @@ public LocalTimerMealyRandomWpOracle(AbstractTimedQueryOracle timeOracle, // Found inconsistency if outputs do no match: if (!sulAnswer.getOutput().equals(hypAnswer)) { - return sulAnswer; // expected SUL output + return sulAnswer; } } - return null; // no counterexample found + return null; } - private DefaultQuery, Word>> generateTestword(List>> prefixes, - List>> globalSuffixes, - LocalTimerMealy hypothesis, - ReducedLocalTimerMealySemantics hypSemModel, - List> alphabet) { + private DefaultQuery, Word>> generateTestword(List>> prefixes, + List>> globalSuffixes, + LocalTimerMealy hypothesis, + ReducedLocalTimerMealySemantics hypSemModel, + List> alphabet) { WordBuilder> wbTestWord = new WordBuilder<>(); 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 index 1d82bfd4b..5fcfc56b5 100644 --- 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 @@ -40,7 +40,7 @@ * @param Input type for non-delaying inputs * @param Output symbol type */ -public class LocalTimerMealySimulatorOracle implements EquivalenceOracle.LocalTimerMealyEquivalenceOracle { +public class LocalTimerMealySimulatorOracle implements EquivalenceOracle.LocalTimerMealyEquivalenceOracle { private final LocalTimerMealy refModel; @@ -50,7 +50,7 @@ public LocalTimerMealySimulatorOracle(LocalTimerMealy refModel) { @Override - public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, @Nullable Collection> ignored) { + public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, @Nullable Collection> ignored) { var separatingWord = LocalTimerMealyUtil.findSeparatingWord(refModel, hypothesis); if (separatingWord != 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 index b3fdf9e94..0c7ee5b60 100644 --- 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 @@ -46,11 +46,10 @@ * - Appends inputs of all inputs that self-loop in that location. * - Appends timeout. * - * @param Location type * @param Input type for non-delaying inputs * @param Output symbol type */ -public class ResetSearchOracle implements EquivalenceOracle.LocalTimerMealyEquivalenceOracle { +public class ResetSearchOracle implements EquivalenceOracle.LocalTimerMealyEquivalenceOracle { private final static Logger logger = LoggerFactory.getLogger(ResetSearchOracle.class); @@ -71,7 +70,7 @@ public ResetSearchOracle(AbstractTimedQueryOracle timeOracle, long seed, d this.loopingInputSelectionSeed = seed; } - private List> getLoopingSymbols(S sourceLoc, List> alphabet, LocalTimerMealy hypothesis) { + private List> getLoopingSymbols(S sourceLoc, List> alphabet, LocalTimerMealy hypothesis) { List> loopingInputs = new ArrayList<>(); for (var sym : alphabet) { @@ -90,7 +89,7 @@ private List> getLoopingSymbols(S sourceLo } @Override - public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, @Nullable Collection> ignored) { + public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, @Nullable Collection> ignored) { if (loopInsertPerc == 0) { return null; // oracle is disabled } @@ -98,7 +97,7 @@ private List> getLoopingSymbols(S sourceLo return this.findCexInternal(hypothesis); } - private @Nullable DefaultQuery, Word>> findCexInternal + private @Nullable DefaultQuery, Word>> findCexInternal (LocalTimerMealy hypothesis) { // Retrieve prefixes from state cover, to establish some separation between learner and teacher: From a55efa2157316648b877b25449bef7cb21d33ff8 Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Fri, 10 Oct 2025 08:28:47 +0200 Subject: [PATCH 15/27] Cache for MMLTs now inherits LearningCache --- .../LStarLocalTimerMealyBenchmarkTests.java | 8 +++---- .../learnlib/filter/cache/LearningCache.java | 20 ++++++++++++++++ .../filter/cache/LocalTimerMealyCache.java | 24 ------------------- .../filter/cache/mmlt/CacheTreeNode.java | 2 +- .../LocalTimerMealyCacheConsistencyTest.java | 4 ++-- ....java => LocalTimerMealyTreeSULCache.java} | 18 ++++++++++---- 6 files changed, 39 insertions(+), 37 deletions(-) delete mode 100644 filters/cache/src/main/java/de/learnlib/filter/cache/LocalTimerMealyCache.java rename filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/{LocalTimerMealyTreeCacheSUL.java => LocalTimerMealyTreeSULCache.java} (91%) 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 index 0b41c556d..22f23aeab 100644 --- 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 @@ -2,9 +2,7 @@ import de.learnlib.algorithm.LocalTimerMealyModelParams; -import de.learnlib.datastructure.observationtable.writer.ObservationTableASCIIWriter; import de.learnlib.driver.simulator.LocalTimerMealySimulatorSUL; -import de.learnlib.filter.cache.mmlt.LocalTimerMealyCacheConsistencyTest; import de.learnlib.filter.cache.mmlt.TimeoutReducerSUL; import de.learnlib.filter.statistic.sul.LocalTimerMealyStatsSUL; import de.learnlib.oracle.EquivalenceOracle; @@ -27,7 +25,7 @@ import net.automatalib.word.Word; import org.testng.annotations.Test; -import de.learnlib.filter.cache.mmlt.LocalTimerMealyTreeCacheSUL; +import de.learnlib.filter.cache.mmlt.LocalTimerMealyTreeSULCache; import java.util.ArrayList; import java.util.List; @@ -94,7 +92,7 @@ private static void learnModel(String name, LocalTimerMealy a // Query oracle -> TimeoutReducer -> Cache -> Query stats -> SUL LocalTimerMealySimulatorSUL sul = new LocalTimerMealySimulatorSUL<>(automaton); LocalTimerMealyStatsSUL statsAfterCache = new LocalTimerMealyStatsSUL<>(sul, stats); - LocalTimerMealyTreeCacheSUL cacheSUL = new LocalTimerMealyTreeCacheSUL<>(statsAfterCache, params.silentOutput()); + LocalTimerMealyTreeSULCache cacheSUL = new LocalTimerMealyTreeSULCache<>(statsAfterCache, params); cacheSUL.setStatsContainer(stats); LocalTimerMealySUL toReducerSul = new TimeoutReducerSUL<>(cacheSUL, params.maxTimeoutWaitingTime(), stats); @@ -103,7 +101,7 @@ private static void learnModel(String name, LocalTimerMealy a // Prepare cex oracle chain: LocalTimerMealyEQOracleChain chainOracle = new LocalTimerMealyEQOracleChain<>(); - chainOracle.addOracle(new LocalTimerMealyCacheConsistencyTest<>(cacheSUL, params)); + chainOracle.addOracle(cacheSUL.createCacheConsistencyTest()); chainOracle.addOracle(new ResetSearchOracle<>(timeOracle, seed, 1.0, 1.0)); chainOracle.addOracle(new LocalTimerMealyRandomWpOracle<>(timeOracle, seed, 6, 12, 100)); chainOracle.addOracle(new LocalTimerMealySimulatorOracle<>(automaton)); // ensure that we eventually find an accurate model 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 cf6a862a9..b5d3e6dce 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,19 @@ 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. + * + * @return List of all stored words. + */ + List>> listAllWords(); + } } diff --git a/filters/cache/src/main/java/de/learnlib/filter/cache/LocalTimerMealyCache.java b/filters/cache/src/main/java/de/learnlib/filter/cache/LocalTimerMealyCache.java deleted file mode 100644 index 7e9647bef..000000000 --- a/filters/cache/src/main/java/de/learnlib/filter/cache/LocalTimerMealyCache.java +++ /dev/null @@ -1,24 +0,0 @@ -package de.learnlib.filter.cache; - -import de.learnlib.sul.LocalTimerMealySUL; -import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; -import net.automatalib.word.Word; - -import java.util.List; - -/** - * Abstract class for caches for {@link LocalTimerMealySUL}. - * - * @param Input type for non-delaying inputs - * @param Output symbol type - */ -public abstract class LocalTimerMealyCache extends LocalTimerMealySUL { - - /** - * Lists all words that are currently in the cache. - * - * @return List of all stored words. - */ - public abstract 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 index bddb81c3c..111cba4e8 100644 --- 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 @@ -22,7 +22,7 @@ * @param Input type for non-delaying inputs * @param Output symbol type */ -public class CacheTreeNode { +class CacheTreeNode { private record CacheTreeTransition(LocalTimerMealyOutputSymbol output, CacheTreeNode target) { } 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 index 15b23df2e..0d092f244 100644 --- 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 @@ -25,10 +25,10 @@ public class LocalTimerMealyCacheConsistencyTest implements EquivalenceOracle.LocalTimerMealyEquivalenceOracle { private final static Logger logger = LoggerFactory.getLogger(LocalTimerMealyCacheConsistencyTest.class); - private final LocalTimerMealyTreeCacheSUL sulCache; + private final LocalTimerMealyTreeSULCache sulCache; private final LocalTimerMealyModelParams modelParams; - public LocalTimerMealyCacheConsistencyTest(LocalTimerMealyTreeCacheSUL sulCache, LocalTimerMealyModelParams modelParams) { + LocalTimerMealyCacheConsistencyTest(LocalTimerMealyTreeSULCache sulCache, LocalTimerMealyModelParams modelParams) { this.sulCache = sulCache; this.modelParams = modelParams; } diff --git a/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyTreeCacheSUL.java b/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyTreeSULCache.java similarity index 91% rename from filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyTreeCacheSUL.java rename to filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyTreeSULCache.java index 01ece3bc3..b8200c1be 100644 --- a/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyTreeCacheSUL.java +++ b/filters/cache/src/main/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyTreeSULCache.java @@ -1,7 +1,8 @@ package de.learnlib.filter.cache.mmlt; -import de.learnlib.filter.cache.LocalTimerMealyCache; +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; @@ -28,12 +29,13 @@ * @param Input type for non-delaying inputs * @param Output symbol type */ -public class LocalTimerMealyTreeCacheSUL extends LocalTimerMealyCache implements GraphViewable, LearnerStatsProvider { +public class LocalTimerMealyTreeSULCache extends LocalTimerMealySUL implements 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; @@ -44,9 +46,10 @@ public void setStatsContainer(StatsContainer container) { this.stats = container; } - public LocalTimerMealyTreeCacheSUL(LocalTimerMealySUL delegate, O silentOutput) { + public LocalTimerMealyTreeSULCache(LocalTimerMealySUL delegate, LocalTimerMealyModelParams modelParams) { this.delegate = delegate; - this.silentOutput = new LocalTimerMealyOutputSymbol<>(silentOutput); + this.modelParams = modelParams; + this.silentOutput = new LocalTimerMealyOutputSymbol<>(modelParams.silentOutput()); // Init cache: this.cacheRoot = new CacheTreeNode<>(null, null); @@ -108,7 +111,7 @@ public LocalTimerMealyOutputSymbol step(NonDelayingInput input) { if (currentState.getTimeout() > remaining) { // Split current timeout: - this.currentState = this.currentState.splitTimeout(remaining, silentOutput); + this.currentState = this.currentState.splitTimeout(remaining, this.silentOutput); return null; // no timer in this state } @@ -261,4 +264,9 @@ public List>> listAllWords() { return mealy.graphView(); } + @Override + public EquivalenceOracle.LocalTimerMealyEquivalenceOracle createCacheConsistencyTest() { + return new LocalTimerMealyCacheConsistencyTest<>(this, this.modelParams); + } + } From 9491c1cac5509266ea986e05967440923dbc5c48 Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Fri, 10 Oct 2025 09:22:16 +0200 Subject: [PATCH 16/27] Multiple EQ tests for MMLTs can now respect the provided list of inputs for counterexamples. --- ...tarLocalTimerMealyCounterexampleTests.java | 2 +- .../mmlt/LocalTimerMealyRandomWpOracle.java | 18 +++++++---------- .../mmlt/LocalTimerMealySimulatorOracle.java | 20 +++++++++---------- .../equivalence/mmlt/ResetSearchOracle.java | 20 +++++++++---------- 4 files changed, 27 insertions(+), 33 deletions(-) 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 index 0658b03d6..961bc2a30 100644 --- 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 @@ -57,7 +57,7 @@ private static void learnModel(LocalTimerMealyTestUtil.Model // Now continue until arriving at an accurate model: System.out.println("Running to completion"); - LocalTimerMealySimulatorOracle simOracle = new LocalTimerMealySimulatorOracle<>(model.automaton()); + LocalTimerMealySimulatorOracle simOracle = new LocalTimerMealySimulatorOracle<>(model.automaton()); int round = 0; while (round < 100) { var hyp = learner.getHypothesisModel(); 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 index ab27d8e81..bad704645 100644 --- 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 @@ -29,8 +29,6 @@ import de.learnlib.statistic.container.StatsContainer; import net.automatalib.alphabet.time.mmlt.LocalTimerMealyOutputSymbol; import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; -import net.automatalib.alphabet.time.mmlt.TimeStepSymbol; -import net.automatalib.alphabet.time.mmlt.TimeoutSymbol; import net.automatalib.automaton.time.impl.mmlt.ReducedLocalTimerMealySemantics; import net.automatalib.automaton.time.mmlt.LocalTimerMealy; import net.automatalib.common.util.string.AbstractPrintable; @@ -76,24 +74,22 @@ public LocalTimerMealyRandomWpOracle(AbstractTimedQueryOracle timeOracle, } @Override - public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, @Nullable Collection> ignored) { - return findCounterExampleInternal(hypothesis); + public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, Collection> inputs) { + return findCounterExampleInternal(hypothesis, inputs); } - private DefaultQuery, Word>> findCounterExampleInternal(LocalTimerMealy hypothesis) { + 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<>(hypothesis.getUntimedAlphabet()); - listAlphabet.add(new TimeoutSymbol<>()); - listAlphabet.add(new TimeStepSymbol<>()); + List> listAlphabet = new ArrayList<>(inputs); // Identify global suffixes: - var globalSuffixes = Automata.characterizingSet(hypSemModel, hypSemModel.getInputAlphabet()); + var globalSuffixes = Automata.characterizingSet(hypSemModel, inputs); // Get list of prefixes in deterministic order (so we can reproduce experiments easily): - var locationCover = LocalTimerMealyCover.getLocalTimerMealyLocationCover(hypothesis); + var locationCover = LocalTimerMealyCover.getLocalTimerMealyLocationCover(hypothesis, listAlphabet); var prefixList = locationCover .values() .stream() @@ -150,7 +146,7 @@ private DefaultQuery, Word Reference model location type - * @param Hypothesis model location type - * @param Input type for non-delaying inputs - * @param Output symbol type + * @param Input type for non-delaying inputs + * @param Output symbol type */ -public class LocalTimerMealySimulatorOracle implements EquivalenceOracle.LocalTimerMealyEquivalenceOracle { +public class LocalTimerMealySimulatorOracle implements EquivalenceOracle.LocalTimerMealyEquivalenceOracle { - private final LocalTimerMealy refModel; + private final LocalTimerMealy refModel; - public LocalTimerMealySimulatorOracle(LocalTimerMealy refModel) { + public LocalTimerMealySimulatorOracle(LocalTimerMealy refModel) { this.refModel = refModel; } - @Override - public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, @Nullable Collection> ignored) { - var separatingWord = LocalTimerMealyUtil.findSeparatingWord(refModel, hypothesis); + 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); 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 index 0c7ee5b60..66c94e839 100644 --- 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 @@ -43,7 +43,7 @@ *

* - Takes any prefix from a known location * - Appends a single time step. - * - Appends inputs of all inputs that self-loop in that location. + * - Appends inputs of all non-delaying inputs that self-loop in that location. * - Appends timeout. * * @param Input type for non-delaying inputs @@ -75,7 +75,7 @@ private List> getLoopingSymbols(S sour List> loopingInputs = new ArrayList<>(); for (var sym : alphabet) { if (!(sym instanceof NonDelayingInput ndi)) { - throw new AssertionError(); + continue; // only consider non-delaying inputs, as only these can perform local resets } var trans = hypothesis.getTransition(sourceLoc, ndi); @@ -89,21 +89,21 @@ private List> getLoopingSymbols(S sour } @Override - public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, @Nullable Collection> ignored) { + public @Nullable DefaultQuery, Word>> findCounterExample(LocalTimerMealy hypothesis, Collection> inputs) { if (loopInsertPerc == 0) { return null; // oracle is disabled } - - return this.findCexInternal(hypothesis); + List> listInputs = new ArrayList<>(inputs); + return this.findCexInternal(hypothesis, listInputs); } private @Nullable DefaultQuery, Word>> findCexInternal - (LocalTimerMealy hypothesis) { + (LocalTimerMealy hypothesis, List> inputs) { // Retrieve prefixes from state cover, to establish some separation between learner and teacher: - var stateCover = LocalTimerMealyCover.getLocalTimerMealyLocationCover(hypothesis); + var stateCover = LocalTimerMealyCover.getLocalTimerMealyLocationCover(hypothesis, inputs); - // Only keep locations that have at least two stable configs: + // 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() && @@ -125,12 +125,10 @@ private List> getLoopingSymbols(S sour List>> chosenPrefixes = RandomUtil.sampleUnique(locPrefixRandom, prefixes, randPrefixes); - List> listAlphabet = new ArrayList<>(hypothesis.getUntimedAlphabet()); - for (var prefix : chosenPrefixes) { // Retrieve looping symbols: var sourceLoc = hypothesis.getSemantics().traceInputs(prefix).getLocation(); - var loopingInputs = getLoopingSymbols(sourceLoc, listAlphabet, hypothesis); + var loopingInputs = getLoopingSymbols(sourceLoc, inputs, hypothesis); if (loopingInputs.isEmpty()) { continue; // no loops } From e9f5c9920d7e0e756bc5e0d805e0b9dc4a305764 Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Fri, 10 Oct 2025 10:40:48 +0200 Subject: [PATCH 17/27] Added tests for the cache; cleaned-up some files. --- .../algorithm/LocalTimerMealyModelParams.java | 23 +---- .../symbol_filter/SymbolFilterResponse.java | 21 ----- .../cache/mmlt/LocalTimerMealyCacheTest.java | 94 +++++++++++++++++++ .../mmlt/LocalTimerMealyRandomWpOracle.java | 21 ----- .../mmlt/LocalTimerMealySimulatorOracle.java | 21 ----- .../equivalence/mmlt/ResetSearchOracle.java | 21 ----- .../mmlt/PerfectSymbolFilter.java | 21 ----- 7 files changed, 95 insertions(+), 127 deletions(-) create mode 100644 filters/cache/src/test/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyCacheTest.java diff --git a/api/src/main/java/de/learnlib/algorithm/LocalTimerMealyModelParams.java b/api/src/main/java/de/learnlib/algorithm/LocalTimerMealyModelParams.java index a4e9aa8e4..e33fd5978 100644 --- a/api/src/main/java/de/learnlib/algorithm/LocalTimerMealyModelParams.java +++ b/api/src/main/java/de/learnlib/algorithm/LocalTimerMealyModelParams.java @@ -1,24 +1,3 @@ -/* - * Copyright (C) 2023-2024 Paul Kogel, TU Berlin - * - * THIS SOFTWARE IS INTENDED FOR ACADEMIC, RESEARCH AND EDUCATIONAL PURPOSES ONLY, - * MOST PROMINENTLY TO FACILITATE RESEARCH ON AUTOMATA LEARNING. FOR-PROFIT USE OF - * THIS SOFTWARE, INCLUDING, BUT NOT LIMITED TO, SELLING THE SOFTWARE ON ITS OWN OR - * AS PART OF TOOLS AND/OR SERVICES IS PROHIBITED. COMMERCIAL USE OR COMMERCIAL - * DISTRIBUTION OF THIS SOFTWARE OR OF ANY DERIVATES IS PROHIBITED. - * - * Apart from that, this software is licensed under the - * GNU Affero Public License version 3 (AGPLv3). - * - * https://www.gnu.org/licenses/agpl-3.0.txt - * - * 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. - */ - package de.learnlib.algorithm; import net.automatalib.automaton.time.mmlt.AbstractSymbolCombiner; @@ -31,7 +10,7 @@ * @param maxTimeoutWaitingTime Maximum waiting time for a timeout symbol * @param maxTimerQueryWaitingTime Maximum waiting time for timer queries * @param outputCombiner Function for combining simultaneously occurring outputs of timers - * @param + * @param Output symbol type */ public record LocalTimerMealyModelParams(O silentOutput, long maxTimeoutWaitingTime, diff --git a/api/src/main/java/de/learnlib/symbol_filter/SymbolFilterResponse.java b/api/src/main/java/de/learnlib/symbol_filter/SymbolFilterResponse.java index 6c06e93ce..e0a5dd693 100644 --- a/api/src/main/java/de/learnlib/symbol_filter/SymbolFilterResponse.java +++ b/api/src/main/java/de/learnlib/symbol_filter/SymbolFilterResponse.java @@ -1,24 +1,3 @@ -/* - * Copyright (C) 2023-2024 Paul Kogel, TU Berlin - * - * THIS SOFTWARE IS INTENDED FOR ACADEMIC, RESEARCH AND EDUCATIONAL PURPOSES ONLY, - * MOST PROMINENTLY TO FACILITATE RESEARCH ON AUTOMATA LEARNING. FOR-PROFIT USE OF - * THIS SOFTWARE, INCLUDING, BUT NOT LIMITED TO, SELLING THE SOFTWARE ON ITS OWN OR - * AS PART OF TOOLS AND/OR SERVICES IS PROHIBITED. COMMERCIAL USE OR COMMERCIAL - * DISTRIBUTION OF THIS SOFTWARE OR OF ANY DERIVATES IS PROHIBITED. - * - * Apart from that, this software is licensed under the - * GNU Affero Public License version 3 (AGPLv3). - * - * https://www.gnu.org/licenses/agpl-3.0.txt - * - * 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. - */ - package de.learnlib.symbol_filter; public enum SymbolFilterResponse { 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 000000000..dc6cfdb3e --- /dev/null +++ b/filters/cache/src/test/java/de/learnlib/filter/cache/mmlt/LocalTimerMealyCacheTest.java @@ -0,0 +1,94 @@ +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.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.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 testCacheConsistency() { + 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); + } + } +} 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 index bad704645..51be1240c 100644 --- 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 @@ -1,24 +1,3 @@ -/* - * Copyright (C) 2023-2024 Paul Kogel, TU Berlin - * - * THIS SOFTWARE IS INTENDED FOR ACADEMIC, RESEARCH AND EDUCATIONAL PURPOSES ONLY, - * MOST PROMINENTLY TO FACILITATE RESEARCH ON AUTOMATA LEARNING. FOR-PROFIT USE OF - * THIS SOFTWARE, INCLUDING, BUT NOT LIMITED TO, SELLING THE SOFTWARE ON ITS OWN OR - * AS PART OF TOOLS AND/OR SERVICES IS PROHIBITED. COMMERCIAL USE OR COMMERCIAL - * DISTRIBUTION OF THIS SOFTWARE OR OF ANY DERIVATES IS PROHIBITED. - * - * Apart from that, this software is licensed under the - * GNU Affero Public License version 3 (AGPLv3). - * - * https://www.gnu.org/licenses/agpl-3.0.txt - * - * 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. - */ - package de.learnlib.oracle.equivalence.mmlt; import de.learnlib.oracle.EquivalenceOracle; 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 index f428b6d7c..acd60c8ad 100644 --- 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 @@ -1,24 +1,3 @@ -/* - * Copyright (C) 2023-2024 Paul Kogel, TU Berlin - * - * THIS SOFTWARE IS INTENDED FOR ACADEMIC, RESEARCH AND EDUCATIONAL PURPOSES ONLY, - * MOST PROMINENTLY TO FACILITATE RESEARCH ON AUTOMATA LEARNING. FOR-PROFIT USE OF - * THIS SOFTWARE, INCLUDING, BUT NOT LIMITED TO, SELLING THE SOFTWARE ON ITS OWN OR - * AS PART OF TOOLS AND/OR SERVICES IS PROHIBITED. COMMERCIAL USE OR COMMERCIAL - * DISTRIBUTION OF THIS SOFTWARE OR OF ANY DERIVATES IS PROHIBITED. - * - * Apart from that, this software is licensed under the - * GNU Affero Public License version 3 (AGPLv3). - * - * https://www.gnu.org/licenses/agpl-3.0.txt - * - * 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. - */ - package de.learnlib.oracle.equivalence.mmlt; import de.learnlib.oracle.EquivalenceOracle; 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 index 66c94e839..4f37a5eca 100644 --- 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 @@ -1,24 +1,3 @@ -/* - * Copyright (C) 2023-2024 Paul Kogel, TU Berlin - * - * THIS SOFTWARE IS INTENDED FOR ACADEMIC, RESEARCH AND EDUCATIONAL PURPOSES ONLY, - * MOST PROMINENTLY TO FACILITATE RESEARCH ON AUTOMATA LEARNING. FOR-PROFIT USE OF - * THIS SOFTWARE, INCLUDING, BUT NOT LIMITED TO, SELLING THE SOFTWARE ON ITS OWN OR - * AS PART OF TOOLS AND/OR SERVICES IS PROHIBITED. COMMERCIAL USE OR COMMERCIAL - * DISTRIBUTION OF THIS SOFTWARE OR OF ANY DERIVATES IS PROHIBITED. - * - * Apart from that, this software is licensed under the - * GNU Affero Public License version 3 (AGPLv3). - * - * https://www.gnu.org/licenses/agpl-3.0.txt - * - * 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. - */ - package de.learnlib.oracle.equivalence.mmlt; import de.learnlib.oracle.AbstractTimedQueryOracle; diff --git a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/PerfectSymbolFilter.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/PerfectSymbolFilter.java index 413731b54..eb0fadf5d 100644 --- a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/PerfectSymbolFilter.java +++ b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/PerfectSymbolFilter.java @@ -1,24 +1,3 @@ -/* - * Copyright (C) 2023-2024 Paul Kogel, TU Berlin - * - * THIS SOFTWARE IS INTENDED FOR ACADEMIC, RESEARCH AND EDUCATIONAL PURPOSES ONLY, - * MOST PROMINENTLY TO FACILITATE RESEARCH ON AUTOMATA LEARNING. FOR-PROFIT USE OF - * THIS SOFTWARE, INCLUDING, BUT NOT LIMITED TO, SELLING THE SOFTWARE ON ITS OWN OR - * AS PART OF TOOLS AND/OR SERVICES IS PROHIBITED. COMMERCIAL USE OR COMMERCIAL - * DISTRIBUTION OF THIS SOFTWARE OR OF ANY DERIVATES IS PROHIBITED. - * - * Apart from that, this software is licensed under the - * GNU Affero Public License version 3 (AGPLv3). - * - * https://www.gnu.org/licenses/agpl-3.0.txt - * - * 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. - */ - package de.learnlib.oracle.symbol_filters.mmlt; From c9e7de22881c5847c03740cb752f1255951919ce Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Fri, 10 Oct 2025 11:28:59 +0200 Subject: [PATCH 18/27] Added tests for the MMLT cache consistency test. --- .../learnlib/filter/cache/LearningCache.java | 1 + .../LocalTimerMealyCacheConsistencyTest.java | 39 +++++++++++----- .../cache/mmlt/LocalTimerMealyCacheTest.java | 46 ++++++++++++++++++- 3 files changed, 74 insertions(+), 12 deletions(-) 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 b5d3e6dce..2c2a39ae1 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 @@ -95,6 +95,7 @@ interface MooreLearningCache extends LearningCache 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. */ 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 index 0d092f244..045a537f5 100644 --- 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 @@ -11,13 +11,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.List; +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 @@ -109,18 +107,18 @@ private DefaultQuery, Word= this.modelParams.maxTimeoutWaitingTime() || !combinedOutput.getSymbol().equals(this.modelParams.silentOutput())) { wbInput.append(new TimeoutSymbol<>()); if (combinedOutput.getSymbol().equals(this.modelParams.silentOutput())) { - // Reached max delay -> no timeout: + // 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 exceeding max delay OR non-wait symbol -> ignore rest of this word: + // Reached end of word before max_delay OR non-wait symbol -> ignore rest of this word: if (symIdx < queryInput.length() - 1) { logger.warn("Ignoring at least one symbol during cache comparison."); } @@ -131,10 +129,25 @@ private DefaultQuery, Word(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) { - // TODO only with the provided inputs! + Set> allowedInputs = new HashSet<>(inputs); + boolean allInputsConsidered = allowedInputs.containsAll(hypothesis.getSemantics().getInputAlphabet()); // Query all cached words: List>> cachedWords = this.sulCache.listAllWords(); @@ -147,12 +160,16 @@ private DefaultQuery, Word> hypOutput = hypothesis.getSemantics().computeSuffixOutput(Word.epsilon(), convertedQuery.getInput()); + Word> hypOutput = hypothesis.getSemantics().computeSuffixOutput(Word.epsilon(), reducedQuery.getInput()); - if (!hypOutput.equals(convertedQuery.getOutput())) { + if (!hypOutput.equals(reducedQuery.getOutput())) { // Hyp gives different output than cache (= SUL): - counterexamples.add(convertedQuery); + counterexamples.add(reducedQuery); } } 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 index dc6cfdb3e..b0eb0810d 100644 --- 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 @@ -6,6 +6,8 @@ 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; @@ -14,6 +16,7 @@ import org.testng.annotations.Test; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Random; @@ -53,7 +56,7 @@ private CompactLocalTimerMealy buildBaseModel() { /** * Tests if the information in the cache is consistent with the output of the SUL. */ - public void testCacheConsistency() { + public void testCacheAndSULConsistency() { Random random = new Random(100); var automaton = buildBaseModel(); @@ -91,4 +94,45 @@ public void testCacheConsistency() { 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)); + } } From 48c75578c040e3539d537ea44d37a81ac7a200e0 Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Fri, 10 Oct 2025 11:48:15 +0200 Subject: [PATCH 19/27] Made the MMLT SUL an interface with default methods. --- .../de/learnlib/sul/LocalTimerMealySUL.java | 31 +++++++++---------- .../learnlib/symbol_filter/SymbolFilter.java | 2 +- .../LocalTimerMealySimulatorSUL.java | 2 +- .../LocalTimerMealyCacheConsistencyTest.java | 4 +-- .../mmlt/LocalTimerMealyTreeSULCache.java | 2 +- .../filter/cache/mmlt/TimeoutReducerSUL.java | 2 +- .../sul/LocalTimerMealyStatsSUL.java | 2 +- 7 files changed, 22 insertions(+), 23 deletions(-) diff --git a/api/src/main/java/de/learnlib/sul/LocalTimerMealySUL.java b/api/src/main/java/de/learnlib/sul/LocalTimerMealySUL.java index a77f04cc2..8ab564a7d 100644 --- a/api/src/main/java/de/learnlib/sul/LocalTimerMealySUL.java +++ b/api/src/main/java/de/learnlib/sul/LocalTimerMealySUL.java @@ -6,13 +6,12 @@ import org.checkerframework.checker.nullness.qual.Nullable; /** - * A SUL with MMLT semantics. We use this type to interface with real systems and to - * simulate MMLT models. + * Interface for a SUL with MMLT semantics. * * @param Input type for non-delaying inputs * @param Output symbol type */ -public abstract class LocalTimerMealySUL { +public interface LocalTimerMealySUL { /** * Follows the provided input word, starting at the current system state. @@ -20,7 +19,7 @@ public abstract class LocalTimerMealySUL { * * @param input Input suffix. */ - public void follow(Word> input) { + default void follow(Word> input) { this.follow(input, -1); } @@ -28,9 +27,9 @@ public void follow(Word> input) { * Follows the provided input word, starting at the current configuration. * * @param input Input suffix. - * @param maxTimeout Max. timeout to use for timeoutSymbols. + * @param maxTimeout Max. waiting time to use for timeoutSymbols. */ - public void follow(Word> input, long maxTimeout) { + default void follow(Word> input, long maxTimeout) { for (var s : input) { if (s instanceof NonDelayingInput ndi) { this.step(ndi); @@ -48,24 +47,24 @@ public void follow(Word> input, long maxTi } /** - * Provides an input to the SUL and returns the observed output. + * Provides a non-delaying input to the SUL and returns the observed output. * * @param input Input * @return SUL output. */ - public abstract LocalTimerMealyOutputSymbol step(NonDelayingInput input); + 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 there are no timers defined - * in the current location. + * 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 observed. + * @return Observed timer output with waiting time, or null, if no timeout was observed. */ @Nullable - public abstract LocalTimerMealyOutputSymbol timeoutStep(long maxTime); + LocalTimerMealyOutputSymbol timeoutStep(long maxTime); /** * Waits for one time unit and returns the observed output. @@ -74,7 +73,7 @@ public void follow(Word> input, long maxTi * The delay of this output is set to zero. */ @Nullable - public LocalTimerMealyOutputSymbol timeStep() { + default LocalTimerMealyOutputSymbol timeStep() { var res = this.timeoutStep(1); if (res != null) { return new LocalTimerMealyOutputSymbol<>(res.getSymbol()); @@ -88,7 +87,7 @@ public LocalTimerMealyOutputSymbol timeStep() { * @param input Waiting time. * @return Observed timeouts. Empty, if none. */ - public Word> collectTimeouts(TimeStepSequence input) { + default Word> collectTimeouts(TimeStepSequence input) { WordBuilder> wbOutput = new WordBuilder<>(); long remainingTime = input.getTimeSteps(); @@ -110,10 +109,10 @@ public Word> collectTimeouts(TimeStepSequence /** * Prepares the SUL for a new query. */ - public abstract void pre(); + void pre(); /** * Deinitializes the SUL. */ - public abstract void post(); + 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 index d88c23043..6e535c969 100644 --- a/api/src/main/java/de/learnlib/symbol_filter/SymbolFilter.java +++ b/api/src/main/java/de/learnlib/symbol_filter/SymbolFilter.java @@ -21,7 +21,7 @@ public interface SymbolFilter { * 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 Configuration prefix. May contain delay-symbols (= tau inputs). + * @param prefix Configuration prefix. May contain time steps. * @param symbol Queried transition * @return IGNORE if the symbol is considered ignorable, ACCEPT if it is not. */ 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 index bdcd17af0..8b2543947 100644 --- a/drivers/simulator/src/main/java/de/learnlib/driver/simulator/LocalTimerMealySimulatorSUL.java +++ b/drivers/simulator/src/main/java/de/learnlib/driver/simulator/LocalTimerMealySimulatorSUL.java @@ -16,7 +16,7 @@ * @param Non-delaying input type. * @param Output symbol type. */ -public class LocalTimerMealySimulatorSUL extends LocalTimerMealySUL { +public class LocalTimerMealySimulatorSUL implements LocalTimerMealySUL { private final LocalTimerMealy automaton; 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 index 045a537f5..03c805ae0 100644 --- 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 @@ -60,8 +60,8 @@ private DefaultQuery, Word Input type for non-delaying inputs * @param Output symbol type */ -public class LocalTimerMealyTreeSULCache extends LocalTimerMealySUL implements LearningCache.LocalTimerMealyLearningCache, GraphViewable, LearnerStatsProvider { +public class LocalTimerMealyTreeSULCache implements LocalTimerMealySUL, LearningCache.LocalTimerMealyLearningCache, GraphViewable, LearnerStatsProvider { private final LocalTimerMealySUL delegate; private final CacheTreeNode cacheRoot; 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 index b6b080a17..461d8d199 100644 --- 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 @@ -20,7 +20,7 @@ * @param Input type for non-delaying inputs * @param Output symbol type */ -public class TimeoutReducerSUL extends LocalTimerMealySUL implements LearnerStatsProvider { +public class TimeoutReducerSUL implements LocalTimerMealySUL, LearnerStatsProvider { private final LocalTimerMealySUL delegate; private final long maxDelay; 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 index 2c668730c..cd6fdbcaf 100644 --- 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 @@ -16,7 +16,7 @@ * @param Input type for non-delaying inputs * @param Output symbol type */ -public class LocalTimerMealyStatsSUL extends LocalTimerMealySUL implements LearnerStatsProvider { +public class LocalTimerMealyStatsSUL implements LocalTimerMealySUL, LearnerStatsProvider { private final LocalTimerMealySUL delegate; private StatsContainer stats; From cf765ea3fb8ebf86802c8fcc8351389d7286fcbf Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Mon, 13 Oct 2025 11:26:08 +0200 Subject: [PATCH 20/27] Made symbol filters more independent from MMLTs --- .../lstar/mmlt/LStarLocalTimerMealy.java | 8 +-- .../mmlt/LocalTimerMealyObservationTable.java | 9 +-- .../LocalTimerMealyCounterexampleHandler.java | 5 +- .../LStarLocalTimerMealyBenchmarkTests.java | 13 ++-- ...tarLocalTimerMealyCounterexampleTests.java | 2 +- .../learnlib/symbol_filter/SymbolFilter.java | 32 ++++----- .../symbol_filters/AcceptAllSymbolFilter.java | 24 +++++++ .../symbol_filters/CachedSymbolFilter.java | 44 +++++++++++++ .../symbol_filters/IgnoreAllSymbolFilter.java | 25 +++++++ .../symbol_filters/PerfectSymbolFilter.java | 34 ++++++++++ .../symbol_filters/RandomSymbolFilter.java | 52 +++++++++++++++ .../{mmlt => }/StatisticsSymbolFilter.java | 25 ++++--- .../mmlt/AcceptAllSymbolFilter.java | 25 ------- .../mmlt/CachedSymbolFilter.java | 47 -------------- .../mmlt/IgnoreAllSymbolFilter.java | 25 ------- .../LocalTimerMealyPerfectSymbolFilter.java | 32 +++++++++ .../LocalTimerMealyRandomSymbolFilter.java | 35 ++++++++++ ...LocalTimerMealyStatisticsSymbolFilter.java | 25 +++++++ .../mmlt/LocalTimerMealySymbolFilterUtil.java | 31 +++++++++ .../mmlt/PerfectSymbolFilter.java | 44 ------------- .../mmlt/RandomSymbolFilter.java | 65 ------------------- 21 files changed, 352 insertions(+), 250 deletions(-) create mode 100644 oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/AcceptAllSymbolFilter.java create mode 100644 oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/CachedSymbolFilter.java create mode 100644 oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/IgnoreAllSymbolFilter.java create mode 100644 oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/PerfectSymbolFilter.java create mode 100644 oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/RandomSymbolFilter.java rename oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/{mmlt => }/StatisticsSymbolFilter.java (61%) delete mode 100644 oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/AcceptAllSymbolFilter.java delete mode 100644 oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/CachedSymbolFilter.java delete mode 100644 oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/IgnoreAllSymbolFilter.java create mode 100644 oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/LocalTimerMealyPerfectSymbolFilter.java create mode 100644 oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/LocalTimerMealyRandomSymbolFilter.java create mode 100644 oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/LocalTimerMealyStatisticsSymbolFilter.java create mode 100644 oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/LocalTimerMealySymbolFilterUtil.java delete mode 100644 oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/PerfectSymbolFilter.java delete mode 100644 oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/RandomSymbolFilter.java 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 index f3331e456..e5ebc3cc0 100644 --- 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 @@ -27,6 +27,7 @@ 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; @@ -55,7 +56,7 @@ public class LStarLocalTimerMealy implements OTLearner, ? super Word>> closingStrategy; private final AbstractTimedQueryOracle timeOracle; - private final SymbolFilter symbolFilter; + private final SymbolFilter, NonDelayingInput> symbolFilter; private final LStarLocalTimerMealyHypDataContainer hypData; @@ -82,7 +83,7 @@ public LStarLocalTimerMealy(Alphabet> alph @NonNull List>> initialSuffixes, AbstractTimedQueryOracle timeOracle, - SymbolFilter symbolFilter) { + @NonNull SymbolFilter, NonDelayingInput> symbolFilter) { this(alphabet, modelParams, initialSuffixes, ClosingStrategies.CLOSE_SHORTEST, timeOracle, symbolFilter, AcexAnalyzers.BINARY_SEARCH_BWD); } @@ -103,8 +104,7 @@ public LStarLocalTimerMealy(Alphabet> alph List>> initialSuffixes, ClosingStrategy, ? super Word>> closingStrategy, AbstractTimedQueryOracle timeOracle, - @NonNull - SymbolFilter symbolFilter, + @NonNull SymbolFilter, NonDelayingInput> symbolFilter, AcexAnalyzer analyzer) { this.closingStrategy = closingStrategy; this.timeOracle = timeOracle; 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 index 8a39c3981..163961d83 100644 --- 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 @@ -11,6 +11,7 @@ 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; @@ -37,7 +38,7 @@ public class LocalTimerMealyObservationTable implements MutableObservation private static final Logger logger = LoggerFactory.getLogger(LocalTimerMealyObservationTable.class); - private final SymbolFilter symbolFilter; + private final SymbolFilter, NonDelayingInput> symbolFilter; private final Map>, LocationTimerInfo> timerInfoMap; // prefix -> timer info @@ -58,7 +59,7 @@ public class LocalTimerMealyObservationTable implements MutableObservation private final LocalTimerMealyOutputSymbol silentOutput; // used for symbol filtering public LocalTimerMealyObservationTable(Alphabet> alphabet, long minTimerQueryWaitTime, - SymbolFilter symbolFilter, O silentOutput) { + @NonNull SymbolFilter, NonDelayingInput> symbolFilter, O silentOutput) { this.alphabet = alphabet; this.symbolFilter = symbolFilter; @@ -285,8 +286,8 @@ public List>>> findUnclosedTransi @Override public List>>> initialize(List>> initialShortPrefixes, - List>> initialSuffixes, - MembershipOracle, Word>> oracle) { + List>> initialSuffixes, + MembershipOracle, Word>> oracle) { if (isInitialized()) { throw new IllegalStateException("Called initialize, but there are already rows present"); 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 index 442fe2fac..f2d4fdc70 100644 --- 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 @@ -16,6 +16,7 @@ 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; @@ -31,13 +32,13 @@ */ public class LocalTimerMealyCounterexampleHandler implements LearnerStatsProvider { private static final Logger logger = LoggerFactory.getLogger(LocalTimerMealyCounterexampleHandler.class); - private final SymbolFilter symbolFilter; + private final SymbolFilter, NonDelayingInput> symbolFilter; private StatsContainer stats = new DummyStatsContainer(); protected final AbstractTimedQueryOracle timeOracle; private final LocalTimerMealyCounterexampleDecompositor decompositor; - public LocalTimerMealyCounterexampleHandler(AbstractTimedQueryOracle timeOracle, AcexAnalyzer acexAnalyzer, SymbolFilter symbolFilter) { + public LocalTimerMealyCounterexampleHandler(AbstractTimedQueryOracle timeOracle, AcexAnalyzer acexAnalyzer, @NonNull SymbolFilter, NonDelayingInput> symbolFilter) { this.timeOracle = timeOracle; this.decompositor = new LocalTimerMealyCounterexampleDecompositor<>(timeOracle, acexAnalyzer); this.symbolFilter = symbolFilter; 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 index 22f23aeab..84a3999f4 100644 --- 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 @@ -11,6 +11,10 @@ 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.StatisticsSymbolFilter; import de.learnlib.oracle.symbol_filters.mmlt.*; import de.learnlib.query.DefaultQuery; import de.learnlib.statistic.container.StatsContainer; @@ -20,6 +24,7 @@ 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; @@ -113,14 +118,14 @@ private static void learnModel(String name, LocalTimerMealy a suffixes.add(Word.fromLetter(new TimeoutSymbol<>())); // Configure symbol filter: - SymbolFilter filter = new AcceptAllSymbolFilter<>(); // pass-through + SymbolFilter, NonDelayingInput> filter = new AcceptAllSymbolFilter<>(); // pass-through switch (symbolFilterMode) { - case perfect -> filter = new PerfectSymbolFilter<>(automaton); - case random -> filter = new RandomSymbolFilter<>(automaton, 0.1, new Random(seed)); + 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 StatisticsSymbolFilter<>(filter, automaton); + filter = new LocalTimerMealyStatisticsSymbolFilter<>(automaton, filter); filter = new CachedSymbolFilter<>(filter); // need to wrap to enable updates to responses var learner = new LStarLocalTimerMealy<>(alphabet, params, suffixes, timeOracle, filter); 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 index 961bc2a30..cf81228c1 100644 --- 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 @@ -3,7 +3,7 @@ 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.mmlt.AcceptAllSymbolFilter; +import de.learnlib.oracle.symbol_filters.AcceptAllSymbolFilter; import de.learnlib.query.DefaultQuery; import de.learnlib.datastructure.observationtable.writer.ObservationTableASCIIWriter; import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; diff --git a/api/src/main/java/de/learnlib/symbol_filter/SymbolFilter.java b/api/src/main/java/de/learnlib/symbol_filter/SymbolFilter.java index 6e535c969..9af1f079d 100644 --- a/api/src/main/java/de/learnlib/symbol_filter/SymbolFilter.java +++ b/api/src/main/java/de/learnlib/symbol_filter/SymbolFilter.java @@ -1,38 +1,40 @@ package de.learnlib.symbol_filter; -import net.automatalib.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; -import net.automatalib.alphabet.time.mmlt.NonDelayingInput; import net.automatalib.word.Word; /** - * Interface for a symbol filter that can be used to speed-up the learning of MMLTs. + * 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 Input type for non-delaying inputs - * @param Output symbol type + * @param Type for symbols in the prefix of the considered states + * @param Type of the queried symbols */ -public interface SymbolFilter { +public interface SymbolFilter { /** - * Predicts whether the provided symbol is not ignorable in the configuration - * identified by the provided prefix. + * Predicts whether the provided symbol is ignorable in the state + * that is addressed by the given prefix. *

- * "Ignorable" means the symbol belongs to a silent self-loop. + * 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 Configuration prefix. May contain time steps. - * @param symbol Queried transition + * @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, NonDelayingInput symbol); + SymbolFilterResponse query(Word prefix, V symbol); /** * Sets the response of the filter for the given transition to the provided response. * - * @param prefix Configuration prefix. - * @param symbol Queried transition + * @param prefix State prefix. + * @param symbol Input of the transition that should be updated. * @param response New response */ - void update(Word> prefix, NonDelayingInput symbol, SymbolFilterResponse response); + void update(Word prefix, V symbol, SymbolFilterResponse response); } 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 000000000..c972c5571 --- /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 000000000..7dc0e57d9 --- /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 000000000..aebb9a6b8 --- /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 000000000..d37da2ec7 --- /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 000000000..df301b561 --- /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/mmlt/StatisticsSymbolFilter.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/StatisticsSymbolFilter.java similarity index 61% rename from oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/StatisticsSymbolFilter.java rename to oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/StatisticsSymbolFilter.java index dbdc13730..01112f980 100644 --- a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/StatisticsSymbolFilter.java +++ b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/StatisticsSymbolFilter.java @@ -1,38 +1,35 @@ -package de.learnlib.oracle.symbol_filters.mmlt; +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.alphabet.time.mmlt.LocalTimerMealySemanticInputSymbol; -import net.automatalib.alphabet.time.mmlt.NonDelayingInput; -import net.automatalib.automaton.time.mmlt.LocalTimerMealy; import net.automatalib.word.Word; /** * Collects various statistics on symbol filtering, including false accepts + false ignores. * - * @param Input type for non-delaying inputs + * @param Type for symbols in the prefix of the considered states + * @param Type of the queried symbols */ -public class StatisticsSymbolFilter implements SymbolFilter, LearnerStatsProvider { +public abstract class StatisticsSymbolFilter implements SymbolFilter, LearnerStatsProvider { - private final SymbolFilter delegate; - private final PerfectSymbolFilter perfectFilter; + private final SymbolFilter delegate; private StatsContainer stats = new DummyStatsContainer(); - - public StatisticsSymbolFilter(SymbolFilter delegate, LocalTimerMealy sulModel) { + public StatisticsSymbolFilter(SymbolFilter delegate) { this.delegate = delegate; - this.perfectFilter = new PerfectSymbolFilter<>(sulModel); } + protected abstract SymbolFilterResponse isIgnorable(Word prefix, V symbol); + @Override - public SymbolFilterResponse query(Word> prefix, NonDelayingInput symbol) { + public SymbolFilterResponse query(Word prefix, V symbol) { stats.increaseCounter("cnt_isf_queries", "Filter: queries"); SymbolFilterResponse filterResponse = this.delegate.query(prefix, symbol); - SymbolFilterResponse expectedResponse = this.perfectFilter.query(prefix, symbol); + SymbolFilterResponse expectedResponse = this.isIgnorable(prefix, symbol); // Count false ignores, rejects + correct predictions: if (filterResponse.equals(SymbolFilterResponse.ACCEPT)) { @@ -53,7 +50,7 @@ public SymbolFilterResponse query(Word> pr } @Override - public void update(Word> prefix, NonDelayingInput symbol, SymbolFilterResponse response) { + public void update(Word prefix, V symbol, SymbolFilterResponse response) { delegate.update(prefix, symbol, response); } diff --git a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/AcceptAllSymbolFilter.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/AcceptAllSymbolFilter.java deleted file mode 100644 index 1353f7022..000000000 --- a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/AcceptAllSymbolFilter.java +++ /dev/null @@ -1,25 +0,0 @@ -package de.learnlib.oracle.symbol_filters.mmlt; - - -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 accepts all inputs. - * - * @param Input type for non-delaying inputs - */ -public class AcceptAllSymbolFilter implements SymbolFilter { - @Override - public SymbolFilterResponse query(Word> prefix, NonDelayingInput symbol) { - return SymbolFilterResponse.ACCEPT; - } - - @Override - public void update(Word> prefix, NonDelayingInput symbol, SymbolFilterResponse response) { - throw new IllegalStateException("Not supported."); - } -} diff --git a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/CachedSymbolFilter.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/CachedSymbolFilter.java deleted file mode 100644 index 394ba1cdf..000000000 --- a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/CachedSymbolFilter.java +++ /dev/null @@ -1,47 +0,0 @@ -package de.learnlib.oracle.symbol_filters.mmlt; - - -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; - -import java.util.HashMap; -import java.util.Map; - -/** - * Wrapper for a symbol filter that caches previous responses + allows caller to update these. - * - * @param Input type for non-delaying inputs - * @param Output symbol type - */ -public class CachedSymbolFilter implements SymbolFilter { - private final Map>, Boolean> previousResponses; // transition -> legal/ignore - private final SymbolFilter delegate; - - public CachedSymbolFilter(SymbolFilter delegate) { - this.delegate = delegate; - this.previousResponses = new HashMap<>(); - } - - @Override - public SymbolFilterResponse query(Word> prefix, NonDelayingInput symbol) { - var oldResponse = this.previousResponses.get(prefix.append(symbol)); - if (oldResponse != null) { - return (oldResponse) ? SymbolFilterResponse.ACCEPT : SymbolFilterResponse.IGNORE; - } - - var res = delegate.query(prefix, symbol); - this.previousResponses.put(prefix.append(symbol), res == SymbolFilterResponse.ACCEPT); - return res; - } - - @Override - public void update(Word> prefix, NonDelayingInput symbol, SymbolFilterResponse response) { - if (!this.previousResponses.containsKey(prefix.append(symbol))) { - throw new IllegalArgumentException("Can only update response if already queried."); - } - this.previousResponses.put(prefix.append(symbol), (response == SymbolFilterResponse.ACCEPT)); - } -} diff --git a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/IgnoreAllSymbolFilter.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/IgnoreAllSymbolFilter.java deleted file mode 100644 index 2d3530be3..000000000 --- a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/IgnoreAllSymbolFilter.java +++ /dev/null @@ -1,25 +0,0 @@ -package de.learnlib.oracle.symbol_filters.mmlt; - -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 symbol filter that ignores all symbols. - * - * @param Input type for non-delaying inputs - */ -public class IgnoreAllSymbolFilter implements SymbolFilter { - @Override - public SymbolFilterResponse query(Word> prefix, NonDelayingInput symbol) { - return SymbolFilterResponse.IGNORE; - } - - @Override - public void update(Word> prefix, NonDelayingInput symbol, SymbolFilterResponse response) { - throw new IllegalStateException("Not supported."); - } - -} 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 000000000..3c6fae763 --- /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 000000000..cd627496f --- /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 000000000..e37118c6b --- /dev/null +++ b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/LocalTimerMealyStatisticsSymbolFilter.java @@ -0,0 +1,25 @@ +package de.learnlib.oracle.symbol_filters.mmlt; + +import de.learnlib.oracle.symbol_filters.StatisticsSymbolFilter; +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) { + super(delegate); + 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 000000000..07874ba93 --- /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/de/learnlib/oracle/symbol_filters/mmlt/PerfectSymbolFilter.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/PerfectSymbolFilter.java deleted file mode 100644 index eb0fadf5d..000000000 --- a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/PerfectSymbolFilter.java +++ /dev/null @@ -1,44 +0,0 @@ -package de.learnlib.oracle.symbol_filters.mmlt; - - -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; - -/** - * A symbol filter that correctly accepts and ignores all transitions. - * - * @param Location type - * @param Input type for non-delaying inputs - * @param Output symbol type - */ -public class PerfectSymbolFilter implements SymbolFilter { - - private final LocalTimerMealy sulModel; - - public PerfectSymbolFilter(LocalTimerMealy sulModel) { - this.sulModel = sulModel; - } - - @Override - public SymbolFilterResponse query(Word> prefix, NonDelayingInput symbol) { - // Check if silent self-loop: - - var targetConfig = this.sulModel.getSemantics().traceInputs(prefix); - var trans = this.sulModel.getSemantics().getTransition(targetConfig, symbol); - - if (trans.output().equals(sulModel.getSemantics().getSilentOutput()) && targetConfig.equals(trans.target())) { - return SymbolFilterResponse.IGNORE; - } else { - return SymbolFilterResponse.ACCEPT; - } - } - - @Override - public void update(Word> prefix, NonDelayingInput symbol, SymbolFilterResponse response) { - throw new IllegalStateException("Not supported."); - } -} diff --git a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/RandomSymbolFilter.java b/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/RandomSymbolFilter.java deleted file mode 100644 index f5216d52f..000000000 --- a/oracles/symbol-filters/src/main/java/de/learnlib/oracle/symbol_filters/mmlt/RandomSymbolFilter.java +++ /dev/null @@ -1,65 +0,0 @@ -package de.learnlib.oracle.symbol_filters.mmlt; - - -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; - -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 RandomSymbolFilter implements SymbolFilter { - - private final double inaccurateProb; - private final Random random; - private final LocalTimerMealy sulModel; - - public RandomSymbolFilter(LocalTimerMealy sulModel, - 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; - - this.sulModel = sulModel; - } - - private boolean isSilentSelfLoop(Word> prefix, NonDelayingInput symbol) { - var targetConfig = this.sulModel.getSemantics().traceInputs(prefix); - var trans = this.sulModel.getSemantics().getTransition(targetConfig, symbol); - return trans.output().equals(sulModel.getSemantics().getSilentOutput()) && targetConfig.equals(trans.target()); - } - - @Override - public SymbolFilterResponse query(Word> prefix, NonDelayingInput symbol) { - // Check if silent self-loop: - boolean ignorable = isSilentSelfLoop(prefix, symbol); - - // 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, NonDelayingInput symbol, SymbolFilterResponse response) { - throw new IllegalStateException("Not supported."); - } -} From dfefab4c63d0d4627aeb9069415fa43af1189469 Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Mon, 13 Oct 2025 12:17:54 +0200 Subject: [PATCH 21/27] StatisticsSymbolFilter has stats container as constructor parameter. --- .../lstar/mmlt/LStarLocalTimerMealyBenchmarkTests.java | 3 +-- .../oracle/symbol_filters/StatisticsSymbolFilter.java | 3 ++- .../mmlt/LocalTimerMealyStatisticsSymbolFilter.java | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) 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 index 84a3999f4..b31e41008 100644 --- 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 @@ -14,7 +14,6 @@ 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.StatisticsSymbolFilter; import de.learnlib.oracle.symbol_filters.mmlt.*; import de.learnlib.query.DefaultQuery; import de.learnlib.statistic.container.StatsContainer; @@ -125,7 +124,7 @@ private static void learnModel(String name, LocalTimerMealy a case ignore_all -> filter = new IgnoreAllSymbolFilter<>(); } - filter = new LocalTimerMealyStatisticsSymbolFilter<>(automaton, filter); + 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); 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 index 01112f980..221c98936 100644 --- 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 @@ -18,8 +18,9 @@ public abstract class StatisticsSymbolFilter implements SymbolFilter private final SymbolFilter delegate; private StatsContainer stats = new DummyStatsContainer(); - public StatisticsSymbolFilter(SymbolFilter delegate) { + public StatisticsSymbolFilter(SymbolFilter delegate, StatsContainer stats) { this.delegate = delegate; + this.stats = stats; } protected abstract SymbolFilterResponse isIgnorable(Word prefix, V symbol); 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 index e37118c6b..5323869a0 100644 --- 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 @@ -1,6 +1,7 @@ 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; @@ -12,8 +13,8 @@ public class LocalTimerMealyStatisticsSymbolFilter extends StatisticsSy private final LocalTimerMealy automaton; - public LocalTimerMealyStatisticsSymbolFilter(LocalTimerMealy automaton, SymbolFilter, NonDelayingInput> delegate) { - super(delegate); + public LocalTimerMealyStatisticsSymbolFilter(LocalTimerMealy automaton, SymbolFilter, NonDelayingInput> delegate, StatsContainer stats) { + super(delegate, stats); this.automaton = automaton; } From efaec246f0cae331e952fe52c2e5376c98889e94 Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Mon, 13 Oct 2025 13:35:32 +0200 Subject: [PATCH 22/27] Moved several MMLT examples to test-support. --- .../lstar/mmlt/LStarLocalTimerMealy.java | 4 +- .../LStarLocalTimerMealyBenchmarkTests.java | 13 +- ...tarLocalTimerMealyCounterexampleTests.java | 15 ++- .../lstar/mmlt/LocalTimerMealyTestUtil.java | 23 ++-- .../algorithm/LocalTimerMealyModelParams.java | 79 ++++++++++-- .../equivalence/RandomWMethodEQOracle.java | 2 +- .../equivalence/RandomWpMethodEQOracle.java | 2 +- test-support/learning-examples/pom.xml | 4 + .../example/mmlt/LocalTimerMealyExamples.java | 121 ++++++++++++++++++ .../example/mmlt/LocalTimerMealyModel.java | 20 +++ .../src/main/java/module-info.java | 1 + .../src/main}/resources/mmlt/HVAC.dot | 0 .../src/main}/resources/mmlt/Oven.dot | 0 .../src/main}/resources/mmlt/SCTP.dot | 0 .../src/main/resources/mmlt/WM.dot | 0 .../src/main}/resources/mmlt/WSN.dot | 0 .../main}/resources/mmlt/sensor_collector.dot | 0 17 files changed, 252 insertions(+), 32 deletions(-) create mode 100644 test-support/learning-examples/src/main/java/de/learnlib/testsupport/example/mmlt/LocalTimerMealyExamples.java create mode 100644 test-support/learning-examples/src/main/java/de/learnlib/testsupport/example/mmlt/LocalTimerMealyModel.java rename {algorithms/active/lstar/src/test => test-support/learning-examples/src/main}/resources/mmlt/HVAC.dot (100%) rename {algorithms/active/lstar/src/test => test-support/learning-examples/src/main}/resources/mmlt/Oven.dot (100%) rename {algorithms/active/lstar/src/test => test-support/learning-examples/src/main}/resources/mmlt/SCTP.dot (100%) rename algorithms/active/lstar/src/test/resources/mmlt/WashingMachine.gv => test-support/learning-examples/src/main/resources/mmlt/WM.dot (100%) rename {algorithms/active/lstar/src/test => test-support/learning-examples/src/main}/resources/mmlt/WSN.dot (100%) rename {algorithms/active/lstar/src/test => test-support/learning-examples/src/main}/resources/mmlt/sensor_collector.dot (100%) 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 index e5ebc3cc0..2622abf45 100644 --- 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 @@ -73,7 +73,7 @@ public class LStarLocalTimerMealy implements OTLearner> alph * Instantiates a new Rivest-Schapire learner for MMLTs. * * @param alphabet Input alphabet for the semantic automaton - * @param modelParams Model parameters + * @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. 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 index b31e41008..6eaca933d 100644 --- 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 @@ -19,6 +19,7 @@ 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; @@ -85,7 +86,7 @@ private static void learnModel(String name, LocalTimerMealy a // Add some stats: var stats = new MapStatsContainer(); - stats.addTextInfo("Model", null, name); + 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()); @@ -139,8 +140,10 @@ private static void learnModel(String name, LocalTimerMealy a public void learnExamplesNoFilter() { for (String modelFile : LocalTimerMealyTestUtil.listModelFiles()) { var model = LocalTimerMealyTestUtil.automatonFromFile(modelFile); - learnModel(modelFile, model.automaton(), model.params(), FilterMode.none, 100, true); + 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 @@ -149,6 +152,8 @@ public void learnExamplesIgnoreAllFilter() { 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 @@ -157,6 +162,8 @@ public void learnExamplesPerfectFilter() { 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 @@ -165,6 +172,8 @@ public void learnExamplesRandomFilter() { 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 index cf81228c1..edd3bd9f1 100644 --- 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 @@ -6,6 +6,8 @@ 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; @@ -26,7 +28,7 @@ @Test public class LStarLocalTimerMealyCounterexampleTests { - private static void learnModel(LocalTimerMealyTestUtil.Model model, List>> counterexamples) { + private static void learnModel(LocalTimerMealyModel model, List>> counterexamples) { GrowingAlphabet> alphabet = new GrowingMapAlphabet<>(); model.automaton().getUntimedAlphabet().forEach(alphabet::addSymbol); @@ -129,7 +131,7 @@ public void testRecursiveDecomp() { @Test public void testMissingDiscriminators() { - var model = LocalTimerMealyTestUtil.automatonFromFile("sensor_collector.dot"); + var model = LocalTimerMealyExamples.SensorCollector(); // Missing discriminator at non-del in stable config: List>> cex1 = List.of( @@ -155,7 +157,8 @@ public void testMissingDiscriminators() { @Test public void testMissingResets() { - var model = LocalTimerMealyTestUtil.automatonFromFile("sensor_collector.dot", 40); + var model = LocalTimerMealyExamples.SensorCollector(); + model.params().setMaxTimerQueryWaitingTime(40); // Missing reset in stable config: List>> cex1 = List.of( @@ -185,7 +188,8 @@ public void testMissingResets() { @Test public void testMissingOneShotModelB() { // Setting max waiting = 6 -> all inferred timers are periodic: - var model = LocalTimerMealyTestUtil.automatonFromFile("sensor_collector.dot", 6); + var model = LocalTimerMealyExamples.SensorCollector(); + model.params().setMaxTimerQueryWaitingTime(6); // Missing one-shot via bad return to entry: List>> cex1 = List.of( @@ -212,7 +216,8 @@ public void testMissingOneShotModelB() { @Test public void testMissingOneShotModelA() { - var model = LocalTimerMealyTestUtil.automatonFromFile("sensor_collector.dot", 40); + var model = LocalTimerMealyExamples.SensorCollector(); + model.params().setMaxTimerQueryWaitingTime(40); // Missing one-shot via bad output: List>> cex1 = List.of( 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 index 765669919..1360c54f4 100644 --- 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 @@ -1,6 +1,7 @@ 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; @@ -20,12 +21,11 @@ /** * Utility class for loading MMLTs from resources and printing them. */ -class LocalTimerMealyTestUtil { - - record Model(LocalTimerMealy automaton, LocalTimerMealyModelParams params) { - - } +public class LocalTimerMealyTestUtil { + /** + * Prints the provided MMLT to stdout. + */ static void printModel(LocalTimerMealy model) { try { GraphDOT.write(model.transitionGraphView(true, true), System.out); @@ -33,6 +33,9 @@ static void printModel(LocalTimerMealy model) { } } + /** + * Lists all MMLT models in the resources directory. + */ static List listModelFiles() { var models = new ArrayList(); try { @@ -50,7 +53,7 @@ static List listModelFiles() { return models; } - static Model automatonFromFile(String name) { + static LocalTimerMealyModel automatonFromFile(String name) { return automatonFromFile(name, -1); } @@ -61,18 +64,14 @@ static Model automatonFromFile(String name) { * @param maxTimerQueryWaiting Maximum timer query waiting time. If set to -1, the maximum initial timer value is used. * @return The automaton model. */ - static Model automatonFromFile(String name, int maxTimerQueryWaiting) { + 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; - if (name.contains("SCTP")) { - maxTimerQueryWaitingFinal = 9000; // SCTP needs more waiting time - } - - return new Model<>(automaton, new LocalTimerMealyModelParams<>("void", maxTimeoutDelay, maxTimerQueryWaitingFinal, StringSymbolCombiner.getInstance())); + return new LocalTimerMealyModel<>(name, automaton, new LocalTimerMealyModelParams<>("void", maxTimeoutDelay, maxTimerQueryWaitingFinal, StringSymbolCombiner.getInstance())); } } diff --git a/api/src/main/java/de/learnlib/algorithm/LocalTimerMealyModelParams.java b/api/src/main/java/de/learnlib/algorithm/LocalTimerMealyModelParams.java index e33fd5978..3cf87babc 100644 --- a/api/src/main/java/de/learnlib/algorithm/LocalTimerMealyModelParams.java +++ b/api/src/main/java/de/learnlib/algorithm/LocalTimerMealyModelParams.java @@ -2,18 +2,79 @@ 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 silentOutput Silent output symbol - * @param maxTimeoutWaitingTime Maximum waiting time for a timeout symbol - * @param maxTimerQueryWaitingTime Maximum waiting time for timer queries - * @param outputCombiner Function for combining simultaneously occurring outputs of timers - * @param Output symbol type + * @param Output symbol type */ -public record LocalTimerMealyModelParams(O silentOutput, - long maxTimeoutWaitingTime, - long maxTimerQueryWaitingTime, - AbstractSymbolCombiner outputCombiner) { +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 waiting time to wait for a timeout in any configuration + * @param maxTimerQueryWaitingTime Maximum waiting time for timer 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/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 a8e201797..211026621 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 f6310b642..d23b0fa0d 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/test-support/learning-examples/pom.xml b/test-support/learning-examples/pom.xml index 87a2d91f6..a71c37641 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 000000000..1bfa66824 --- /dev/null +++ b/test-support/learning-examples/src/main/java/de/learnlib/testsupport/example/mmlt/LocalTimerMealyExamples.java @@ -0,0 +1,121 @@ +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.io.File; +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. + * This 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 collected. + * + * @return LocalTimerMealyModel + */ + public static LocalTimerMealyModel SensorCollector() { + return automatonFromFile("sensor_collector"); + } + + /** + * Returns an MMLT model of a washing machine. + * + * @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 000000000..07cba765e --- /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 ac70dd871..41817450a 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; diff --git a/algorithms/active/lstar/src/test/resources/mmlt/HVAC.dot b/test-support/learning-examples/src/main/resources/mmlt/HVAC.dot similarity index 100% rename from algorithms/active/lstar/src/test/resources/mmlt/HVAC.dot rename to test-support/learning-examples/src/main/resources/mmlt/HVAC.dot diff --git a/algorithms/active/lstar/src/test/resources/mmlt/Oven.dot b/test-support/learning-examples/src/main/resources/mmlt/Oven.dot similarity index 100% rename from algorithms/active/lstar/src/test/resources/mmlt/Oven.dot rename to test-support/learning-examples/src/main/resources/mmlt/Oven.dot diff --git a/algorithms/active/lstar/src/test/resources/mmlt/SCTP.dot b/test-support/learning-examples/src/main/resources/mmlt/SCTP.dot similarity index 100% rename from algorithms/active/lstar/src/test/resources/mmlt/SCTP.dot rename to test-support/learning-examples/src/main/resources/mmlt/SCTP.dot diff --git a/algorithms/active/lstar/src/test/resources/mmlt/WashingMachine.gv b/test-support/learning-examples/src/main/resources/mmlt/WM.dot similarity index 100% rename from algorithms/active/lstar/src/test/resources/mmlt/WashingMachine.gv rename to test-support/learning-examples/src/main/resources/mmlt/WM.dot diff --git a/algorithms/active/lstar/src/test/resources/mmlt/WSN.dot b/test-support/learning-examples/src/main/resources/mmlt/WSN.dot similarity index 100% rename from algorithms/active/lstar/src/test/resources/mmlt/WSN.dot rename to test-support/learning-examples/src/main/resources/mmlt/WSN.dot diff --git a/algorithms/active/lstar/src/test/resources/mmlt/sensor_collector.dot b/test-support/learning-examples/src/main/resources/mmlt/sensor_collector.dot similarity index 100% rename from algorithms/active/lstar/src/test/resources/mmlt/sensor_collector.dot rename to test-support/learning-examples/src/main/resources/mmlt/sensor_collector.dot From 800f5ea308f145edb9b31a94b7d568687a307102 Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Mon, 13 Oct 2025 14:37:39 +0200 Subject: [PATCH 23/27] Added an example for learning MMLTs; added module info for symbol filter module. --- .../lstar/src/main/java/module-info.java | 1 + examples/pom.xml | 4 + .../de/learnlib/example/mmlt/Example1.java | 137 ++++++++++++++++++ examples/src/main/java/module-info.java | 1 + .../src/main/java/module-info.java | 39 +++++ .../src/main/java/module-info.java | 1 + 6 files changed, 183 insertions(+) create mode 100644 examples/src/main/java/de/learnlib/example/mmlt/Example1.java create mode 100644 oracles/symbol-filters/src/main/java/module-info.java diff --git a/algorithms/active/lstar/src/main/java/module-info.java b/algorithms/active/lstar/src/main/java/module-info.java index 5ec25fb05..c4e6a3c50 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/examples/pom.xml b/examples/pom.xml index b69ee71f8..cfe2ac622 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 000000000..8f701d297 --- /dev/null +++ b/examples/src/main/java/de/learnlib/example/mmlt/Example1.java @@ -0,0 +1,137 @@ +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.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.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, 6, 12, 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); + } + + 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 30cc747eb..529134784 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/oracles/symbol-filters/src/main/java/module-info.java b/oracles/symbol-filters/src/main/java/module-info.java new file mode 100644 index 000000000..bddf05940 --- /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/test-support/learning-examples/src/main/java/module-info.java b/test-support/learning-examples/src/main/java/module-info.java index 41817450a..222665195 100644 --- a/test-support/learning-examples/src/main/java/module-info.java +++ b/test-support/learning-examples/src/main/java/module-info.java @@ -51,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; } From d4a86563100624167152552a3ad1625b4867fd2d Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Mon, 13 Oct 2025 14:40:38 +0200 Subject: [PATCH 24/27] Added an example for learning MMLTs; added module info for symbol filter module. --- examples/src/main/java/de/learnlib/example/mmlt/Example1.java | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/src/main/java/de/learnlib/example/mmlt/Example1.java b/examples/src/main/java/de/learnlib/example/mmlt/Example1.java index 8f701d297..1b6fc27e9 100644 --- a/examples/src/main/java/de/learnlib/example/mmlt/Example1.java +++ b/examples/src/main/java/de/learnlib/example/mmlt/Example1.java @@ -17,7 +17,6 @@ import de.learnlib.oracle.symbol_filters.mmlt.LocalTimerMealyStatisticsSymbolFilter; 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; From 75fd0d7e5f352c32761de8c5bbf562598dfedb1c Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Wed, 15 Oct 2025 09:33:00 +0200 Subject: [PATCH 25/27] Added more descriptions for included MMLT models --- .../LocalTimerMealyCacheConsistencyTest.java | 2 +- .../statistic/sul/LocalTimerMealyStatsSUL.java | 7 ------- .../example/mmlt/LocalTimerMealyExamples.java | 18 +++++++++++++++--- .../src/main/resources/mmlt/Oven.dot | 6 +++++- .../src/main/resources/mmlt/SCTP.dot | 2 +- .../src/main/resources/mmlt/WM.dot | 10 ++++++++++ .../src/main/resources/mmlt/WSN.dot | 6 ++++-- .../main/resources/mmlt/sensor_collector.dot | 8 ++++---- 8 files changed, 40 insertions(+), 19 deletions(-) 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 index 03c805ae0..64a8cab48 100644 --- 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 @@ -120,7 +120,7 @@ private DefaultQuery, Word ignore rest of this word: if (symIdx < queryInput.length() - 1) { - logger.warn("Ignoring at least one symbol during cache comparison."); + logger.debug("Ignoring at least one symbol during cache comparison."); } break; } 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 index cd6fdbcaf..eacbb2299 100644 --- 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 @@ -33,13 +33,6 @@ public LocalTimerMealyStatsSUL(LocalTimerMealySUL delegate, StatsContainer this.name = name; } - public long getResetCount() { - if (this.stats == null) { - throw new IllegalStateException("No stats container set up."); - } - return this.stats.getCount(withPrefix("sul_resets_counter")).get(); - } - @Override public void setStatsContainer(StatsContainer container) { this.stats = container; 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 index 1bfa66824..d7a2b894a 100644 --- 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 @@ -5,7 +5,6 @@ import net.automatalib.serialization.dot.LocalTimerMealyGraphvizParser; import net.automatalib.util.automaton.mmlt.LocalTimerMealyUtil; -import java.io.File; import java.util.List; public class LocalTimerMealyExamples { @@ -41,9 +40,9 @@ public static LocalTimerMealyModel SCTP() { * Returns an MMLT model of a sensor collector. *

      * The sensor measures particulate matter and ambient noise. - * This program automatically ends after some time. The program may be restarted at any time. + * 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 collected. + * At the end of either program, the collected data may be retrieved. * * @return LocalTimerMealyModel */ @@ -53,6 +52,19 @@ public static LocalTimerMealyModel SensorCollector() { /** * 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 */ diff --git a/test-support/learning-examples/src/main/resources/mmlt/Oven.dot b/test-support/learning-examples/src/main/resources/mmlt/Oven.dot index 3c934d0c0..4de25c32d 100644 --- a/test-support/learning-examples/src/main/resources/mmlt/Oven.dot +++ b/test-support/learning-examples/src/main/resources/mmlt/Oven.dot @@ -1,4 +1,8 @@ -// Model of an oven +// 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"]; diff --git a/test-support/learning-examples/src/main/resources/mmlt/SCTP.dot b/test-support/learning-examples/src/main/resources/mmlt/SCTP.dot index f6308a512..9f2d239ad 100644 --- a/test-support/learning-examples/src/main/resources/mmlt/SCTP.dot +++ b/test-support/learning-examples/src/main/resources/mmlt/SCTP.dot @@ -1,4 +1,4 @@ -// Model of the association in the SCTP protocol. +// Model of the endpoint association in the SCTP protocol. // Adapted from Stewart et al.: Stream Control Transmission Protocol (RFC 9260, Figure 3) digraph g { diff --git a/test-support/learning-examples/src/main/resources/mmlt/WM.dot b/test-support/learning-examples/src/main/resources/mmlt/WM.dot index 1e112ec4f..89f9ade3e 100644 --- a/test-support/learning-examples/src/main/resources/mmlt/WM.dot +++ b/test-support/learning-examples/src/main/resources/mmlt/WM.dot @@ -1,4 +1,14 @@ // 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"]; diff --git a/test-support/learning-examples/src/main/resources/mmlt/WSN.dot b/test-support/learning-examples/src/main/resources/mmlt/WSN.dot index a8288e71d..12d305ae3 100644 --- a/test-support/learning-examples/src/main/resources/mmlt/WSN.dot +++ b/test-support/learning-examples/src/main/resources/mmlt/WSN.dot @@ -1,5 +1,7 @@ -// Model of a wireless sensor node that collects data and transmits if -// if the battery has sufficient charge. +// 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"]; 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 index 240314743..9035e0a60 100644 --- a/test-support/learning-examples/src/main/resources/mmlt/sensor_collector.dot +++ b/test-support/learning-examples/src/main/resources/mmlt/sensor_collector.dot @@ -1,7 +1,7 @@ -// This sensor node is used in the diss as running example. -// p1 starts a normal measurement. part triggers a sensor for particulate matter, noise a sensor for ambient noise. -// p2 performs a self-check. -// Set maximum waiting time = 40 to reproduce bad hypothesis from diss. +// 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"] From 78c9025460d9b1d052c5b8ee258e33fe613e7fbf Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Wed, 15 Oct 2025 10:06:01 +0200 Subject: [PATCH 26/27] More info on model params --- .../algorithm/LocalTimerMealyModelParams.java | 13 +++++++++++-- .../java/de/learnlib/example/mmlt/Example1.java | 14 ++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/api/src/main/java/de/learnlib/algorithm/LocalTimerMealyModelParams.java b/api/src/main/java/de/learnlib/algorithm/LocalTimerMealyModelParams.java index 3cf87babc..01884f8d3 100644 --- a/api/src/main/java/de/learnlib/algorithm/LocalTimerMealyModelParams.java +++ b/api/src/main/java/de/learnlib/algorithm/LocalTimerMealyModelParams.java @@ -18,8 +18,17 @@ public final class LocalTimerMealyModelParams { /** * @param silentOutput Silent output symbol - * @param maxTimeoutWaitingTime Maximum waiting time to wait for a timeout in any configuration - * @param maxTimerQueryWaitingTime Maximum waiting time for timer queries + * @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, diff --git a/examples/src/main/java/de/learnlib/example/mmlt/Example1.java b/examples/src/main/java/de/learnlib/example/mmlt/Example1.java index 1b6fc27e9..18f45a0e2 100644 --- a/examples/src/main/java/de/learnlib/example/mmlt/Example1.java +++ b/examples/src/main/java/de/learnlib/example/mmlt/Example1.java @@ -21,10 +21,7 @@ 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.alphabet.time.mmlt.*; import net.automatalib.serialization.dot.GraphDOT; import net.automatalib.word.Word; @@ -95,6 +92,15 @@ public static void main(String[] args) { // 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 -> hyp.getSemantics().getInputAlphabet().stream().filter(s -> !(s instanceof TimeStepSymbol)).toList() } private static void runExperiment(LStarLocalTimerMealy learner, From 2232a1f15caf21768990251cb5b547f20d5429d0 Mon Sep 17 00:00:00 2001 From: Paul Kogel Date: Thu, 16 Oct 2025 09:07:04 +0200 Subject: [PATCH 27/27] Updated reset search oracle to check if it can return a counterexample with the provided inputs. --- .../lstar/mmlt/LStarLocalTimerMealyBenchmarkTests.java | 2 +- .../src/main/java/de/learnlib/example/mmlt/Example1.java | 4 ++-- .../learnlib/oracle/equivalence/mmlt/ResetSearchOracle.java | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) 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 index 6eaca933d..bd356ab47 100644 --- 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 @@ -108,7 +108,7 @@ private static void learnModel(String name, LocalTimerMealy a LocalTimerMealyEQOracleChain chainOracle = new LocalTimerMealyEQOracleChain<>(); chainOracle.addOracle(cacheSUL.createCacheConsistencyTest()); chainOracle.addOracle(new ResetSearchOracle<>(timeOracle, seed, 1.0, 1.0)); - chainOracle.addOracle(new LocalTimerMealyRandomWpOracle<>(timeOracle, seed, 6, 12, 100)); + 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); diff --git a/examples/src/main/java/de/learnlib/example/mmlt/Example1.java b/examples/src/main/java/de/learnlib/example/mmlt/Example1.java index 18f45a0e2..3613d6860 100644 --- a/examples/src/main/java/de/learnlib/example/mmlt/Example1.java +++ b/examples/src/main/java/de/learnlib/example/mmlt/Example1.java @@ -69,7 +69,7 @@ public static void main(String[] args) { LocalTimerMealyEQOracleChain chainOracle = new LocalTimerMealyEQOracleChain<>(); chainOracle.addOracle(cacheSUL.createCacheConsistencyTest()); chainOracle.addOracle(new ResetSearchOracle<>(timeOracle, 100, 1.0, 1.0)); - chainOracle.addOracle(new LocalTimerMealyRandomWpOracle<>(timeOracle, 100, 6, 12, 100)); + 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); @@ -100,7 +100,7 @@ public static void main(String[] args) { // 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 -> hyp.getSemantics().getInputAlphabet().stream().filter(s -> !(s instanceof TimeStepSymbol)).toList() + // with: tester.findCounterExample(hyp, hyp.getSemantics().getInputAlphabet().stream().filter(s -> !(s instanceof TimeStepSymbol)).toList()); } private static void runExperiment(LStarLocalTimerMealy learner, 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 index 4f37a5eca..a55d78df8 100644 --- 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 @@ -73,6 +73,11 @@ private List> getLoopingSymbols(S sour 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); }