diff --git a/CHANGELOG.md b/CHANGELOG.md index 6508f5bceb..5a6d521523 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * LearnLib now supports JPMS modules. All artifacts now provide a `module-info` descriptor except of the distribution artifacts (for Maven-less environments) which only provide an `Automatic-Module-Name` due to non-modular dependencies. Note that while this is a Java 9+ feature, LearnLib still supports Java 8 byte code for the remaining class files. * Added an `InterningMembershipOracle` (including refinements) to the `learnlib-cache` artifact that interns query responses to reduce memory consumption of large data structures. This exports the internal concepts of the DHC learner (which no longer interns query responses automatically). +* The `ADTLearner` has been refactored to longer use the (now-removed) `SymbolQueryOracle` but a new `AdaptiveMembershipOracle` instead which supports answering queries in parallel (thanks to [Leon Vitorovic](https://github.com/leonthalee)). ### Changed @@ -30,6 +31,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * The `de.learnlib.tooling:learnlib-annotation-processor` artifact has been dropped. The functionality has been moved to a [standalone project](https://github.com/LearnLib/build-tools). * The `de.learnlib:learnlib-rpni-edsm` and `de.learnlib:learnlib-rpni-mdl` artifacts have been dropped. The code has been merged with the `de.learnlib:learnlib-rpni` artifact. * `PropertyOracle`s can no longer set a property. This value is now immutable and must be provided during instantiation. Previously, the internal state wasn't updated accordingly if a property was overridden. +* `SymbolQueryOracle`s (and related code such as the respective caches, counters, etc.) have been removed without replacement. Equivalent functionality on the basis of the new `AdaptiveMembershipOracle`s is available instead. ## [0.17.0] - 2023-11-15 diff --git a/algorithms/active/adt/pom.xml b/algorithms/active/adt/pom.xml index 2b8000b280..f8e97deb25 100644 --- a/algorithms/active/adt/pom.xml +++ b/algorithms/active/adt/pom.xml @@ -40,6 +40,10 @@ limitations under the License. de.learnlib learnlib-api + + de.learnlib + learnlib-cache + de.learnlib learnlib-counterexamples diff --git a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/adt/ADT.java b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/adt/ADT.java index 4a9e202ec9..f2e36ce6a6 100644 --- a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/adt/ADT.java +++ b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/adt/ADT.java @@ -22,7 +22,6 @@ import de.learnlib.algorithm.adt.api.LeafSplitter; import de.learnlib.algorithm.adt.config.LeafSplitters; import de.learnlib.algorithm.adt.util.ADTUtil; -import de.learnlib.oracle.SymbolQueryOracle; import net.automatalib.word.Word; /** @@ -93,29 +92,6 @@ public void replaceNode(ADTNode oldNode, ADTNode newNode) { } } - /** - * Successively sifts a word through the ADT induced by the given node. Stops when reaching a leaf. - * - * @param oracle - * the oracle to query with inner node symbols - * @param word - * the word to sift - * @param subtree - * the node whose subtree is considered - * - * @return the leaf (see {@link ADTNode#sift(SymbolQueryOracle, Word)}) - */ - public ADTNode sift(SymbolQueryOracle oracle, Word word, ADTNode subtree) { - - ADTNode current = subtree; - - while (!ADTUtil.isLeafNode(current)) { - current = current.sift(oracle, word); - } - - return current; - } - /** * Splitting a leaf node by extending the trace leading into the node to split. * diff --git a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/adt/ADTLeafNode.java b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/adt/ADTLeafNode.java index b855da8978..efc20275c7 100644 --- a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/adt/ADTLeafNode.java +++ b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/adt/ADTLeafNode.java @@ -15,9 +15,7 @@ */ package de.learnlib.algorithm.adt.adt; -import de.learnlib.oracle.SymbolQueryOracle; import net.automatalib.graph.ads.impl.AbstractRecursiveADSLeafNode; -import net.automatalib.word.Word; import org.checkerframework.checker.nullness.qual.Nullable; /** @@ -37,11 +35,6 @@ public ADTLeafNode(@Nullable ADTNode parent, @Nullable S hypothesisStat super(parent, hypothesisState); } - @Override - public ADTNode sift(SymbolQueryOracle oracle, Word prefix) { - throw new UnsupportedOperationException("Final nodes cannot sift words"); - } - @Override public NodeType getNodeType() { return NodeType.LEAF_NODE; diff --git a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/adt/ADTNode.java b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/adt/ADTNode.java index 9d7b133604..f2373d53f2 100644 --- a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/adt/ADTNode.java +++ b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/adt/ADTNode.java @@ -19,10 +19,8 @@ import java.util.Map; import de.learnlib.algorithm.adt.util.ADTUtil; -import de.learnlib.oracle.SymbolQueryOracle; import net.automatalib.graph.ads.RecursiveADSNode; import net.automatalib.visualization.VisualizationHelper; -import net.automatalib.word.Word; /** * The ADT equivalent of {@link net.automatalib.graph.ads.ADSNode}. In contrast to regular adaptive distinguishing @@ -38,23 +36,16 @@ public interface ADTNode extends RecursiveADSNode> { /** - * Utility method, that sifts a given word through {@code this} ADTNode. If {@code this} node is a
  • symbol - * node, the symbol is applied to the system under learning and the corresponding child node (based on the observed - * output) is returned. If no matching child node is found, a new leaf node is returned instead
  • reset - * node, the system under learning is reset and the provided prefix is reapplied to the system
  • leaf node, - * an exception is thrown
+ * Convenience method for directly accessing this node's {@link #getChildren() children}. * - * @param oracle - * the oracle used to query the system under learning - * @param prefix - * the prefix to be re-applied after encountering a reset node + * @param output + * the output symbol to determine the child to returned * - * @return the corresponding child node - * - * @throws UnsupportedOperationException - * when invoked on a leaf node (see {@link #getNodeType()}). + * @return the child node that is mapped to given output. May be {@code null}, */ - ADTNode sift(SymbolQueryOracle oracle, Word prefix); + default ADTNode getChild(O output) { + return getChildren().get(output); + } // default methods for graph interface @Override diff --git a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/adt/ADTResetNode.java b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/adt/ADTResetNode.java index c0de4b6eb6..c4c2c9ec2f 100644 --- a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/adt/ADTResetNode.java +++ b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/adt/ADTResetNode.java @@ -18,8 +18,6 @@ import java.util.Collections; import java.util.Map; -import de.learnlib.oracle.SymbolQueryOracle; -import net.automatalib.word.Word; import org.checkerframework.checker.nullness.qual.Nullable; /** @@ -76,17 +74,6 @@ public void setHypothesisState(S state) { throw new UnsupportedOperationException("Reset nodes cannot reference a hypothesis state"); } - @Override - public ADTNode sift(SymbolQueryOracle oracle, Word prefix) { - oracle.reset(); - - for (I i : prefix) { - oracle.query(i); - } - - return successor; - } - @Override public NodeType getNodeType() { return NodeType.RESET_NODE; diff --git a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/adt/ADTSymbolNode.java b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/adt/ADTSymbolNode.java index ba89a47cee..7f648fea07 100644 --- a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/adt/ADTSymbolNode.java +++ b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/adt/ADTSymbolNode.java @@ -15,9 +15,7 @@ */ package de.learnlib.algorithm.adt.adt; -import de.learnlib.oracle.SymbolQueryOracle; import net.automatalib.graph.ads.impl.AbstractRecursiveADSSymbolNode; -import net.automatalib.word.Word; import org.checkerframework.checker.nullness.qual.Nullable; /** @@ -37,21 +35,6 @@ public ADTSymbolNode(@Nullable ADTNode parent, I symbol) { super(parent, symbol); } - @Override - public ADTNode sift(SymbolQueryOracle oracle, Word prefix) { - final O o = oracle.query(super.getSymbol()); - - final ADTNode successor = super.getChildren().get(o); - - if (successor == null) { - final ADTNode result = new ADTLeafNode<>(this, null); - super.getChildren().put(o, result); - return result; - } - - return successor; - } - @Override public NodeType getNodeType() { return NodeType.SYMBOL_NODE; diff --git a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/config/LeafSplitters.java b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/config/LeafSplitters.java index 4be1ac0f83..7e75269a2d 100644 --- a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/config/LeafSplitters.java +++ b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/config/LeafSplitters.java @@ -159,7 +159,7 @@ public static ADTNode splitParent(ADTNode nodeToSpli newIter.next(); newSuffixOutput = oldIter.next(); - adsIter = adsIter.getChildren().get(newSuffixOutput); + adsIter = adsIter.getChild(newSuffixOutput); } final ADTNode continuedADS = new ADTSymbolNode<>(adsIter.getParent(), suffixIter.next()); diff --git a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/ADSAmbiguityQuery.java b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/ADSAmbiguityQuery.java new file mode 100644 index 0000000000..e6304b6457 --- /dev/null +++ b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/ADSAmbiguityQuery.java @@ -0,0 +1,81 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.algorithm.adt.learner; + +import java.util.ArrayDeque; +import java.util.Deque; + +import de.learnlib.algorithm.adt.adt.ADTNode; +import de.learnlib.algorithm.adt.automaton.ADTState; +import net.automatalib.word.Word; + +/** + * Utility class to resolve ADS ambiguities. This query simply tracks the current ADT node for the given inputs. + * + * @param + * input symbol type + * @param + * output symbol type + */ +class ADSAmbiguityQuery extends AbstractAdaptiveQuery { + + private final Word accessSequence; + private final Deque oneShotPrefix; + + private int asIndex; + private boolean inOneShot; + + ADSAmbiguityQuery(Word accessSequence, Word oneShotPrefix, ADTNode, I, O> root) { + super(root); + this.accessSequence = accessSequence; + this.oneShotPrefix = new ArrayDeque<>(oneShotPrefix.asList()); + this.asIndex = 0; + this.inOneShot = false; + } + + @Override + public I getInput() { + if (this.asIndex < this.accessSequence.length()) { + return this.accessSequence.getSymbol(this.asIndex); + } else { + this.inOneShot = !this.oneShotPrefix.isEmpty(); + if (this.inOneShot) { + return oneShotPrefix.poll(); + } else { + return this.currentADTNode.getSymbol(); + } + } + } + + @Override + public Response processOutput(O out) { + if (this.asIndex < this.accessSequence.length()) { + asIndex++; + return Response.SYMBOL; + } else if (this.inOneShot) { + return Response.SYMBOL; + } else { + return super.processOutput(out); + } + } + + @Override + protected void resetProgress() { + this.asIndex = 0; + } +} + + diff --git a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/ADSVerificationQuery.java b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/ADSVerificationQuery.java new file mode 100644 index 0000000000..c722bf7bf2 --- /dev/null +++ b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/ADSVerificationQuery.java @@ -0,0 +1,107 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.algorithm.adt.learner; + +import java.util.Objects; + +import de.learnlib.algorithm.adt.automaton.ADTState; +import de.learnlib.query.AdaptiveQuery; +import de.learnlib.query.DefaultQuery; +import net.automatalib.word.Word; +import net.automatalib.word.WordBuilder; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Utility class to verify ADSs. This query tracks the current ADT node for the given inputs and compares it with an + * expected output, potentially constructing a counterexample from the observed data. + * + * @param + * input symbol type + * @param + * output symbol type + */ +class ADSVerificationQuery implements AdaptiveQuery { + + private final Word prefix; + private final Word suffix; + private final Word expectedOutput; + private final WordBuilder outputBuilder; + private final ADTState state; + + private final int prefixLength; + private final int suffixLength; + private int idx; + private @Nullable DefaultQuery> counterexample; + + ADSVerificationQuery(Word prefix, Word suffix, Word expectedSuffixOutput, ADTState state) { + this.prefix = prefix; + this.suffix = suffix; + this.expectedOutput = expectedSuffixOutput; + this.outputBuilder = new WordBuilder<>(suffix.size()); + this.state = state; + + this.prefixLength = prefix.length(); + this.suffixLength = suffix.length(); + this.idx = 0; + } + + @Override + public I getInput() { + if (idx < prefixLength) { + return prefix.getSymbol(idx); + } else { + return suffix.getSymbol(idx - prefixLength); + } + } + + @Override + public Response processOutput(O out) { + if (idx < prefixLength) { + idx++; + return Response.SYMBOL; + } else { + outputBuilder.append(out); + + if (!Objects.equals(out, expectedOutput.getSymbol(idx - prefixLength))) { + counterexample = + new DefaultQuery<>(prefix, suffix.prefix(outputBuilder.size()), outputBuilder.toWord()); + return Response.FINISHED; + } else if (outputBuilder.size() < suffixLength) { + idx++; + return Response.SYMBOL; + } else { + return Response.FINISHED; + } + } + } + + @Nullable + DefaultQuery> getCounterexample() { + return counterexample; + } + + ADTState getState() { + return state; + } + + Word getSuffix() { + return suffix; + } + + Word getExpectedOutput() { + return expectedOutput; + } +} diff --git a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/ADTAdaptiveQuery.java b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/ADTAdaptiveQuery.java new file mode 100644 index 0000000000..ee120fc3e3 --- /dev/null +++ b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/ADTAdaptiveQuery.java @@ -0,0 +1,94 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.algorithm.adt.learner; + +import de.learnlib.algorithm.adt.adt.ADTNode; +import de.learnlib.algorithm.adt.automaton.ADTState; +import de.learnlib.algorithm.adt.automaton.ADTTransition; +import de.learnlib.algorithm.adt.util.ADTUtil; +import net.automatalib.word.Word; + +/** + * Utility class to close transitions. This query simply tracks the current ADT node for the access sequence of the + * given transition and sets its output if the respective input symbol is traversed. + * + * @param + * input symbol type + * @param + * output symbol type + */ +class ADTAdaptiveQuery extends AbstractAdaptiveQuery { + + private final ADTTransition transition; + private final Word accessSequence; + + private int asIndex; + + ADTAdaptiveQuery(ADTTransition transition, ADTNode, I, O> root) { + super(root); + this.transition = transition; + this.accessSequence = transition.getSource().getAccessSequence(); + this.asIndex = 0; + } + + @Override + public I getInput() { + if (this.asIndex <= this.accessSequence.length()) { + + if (asIndex == this.accessSequence.length()) { + return transition.getInput(); + } + + return this.accessSequence.getSymbol(this.asIndex); + } else { + return super.currentADTNode.getSymbol(); + } + } + + @Override + public Response processOutput(O out) { + if (this.asIndex <= this.accessSequence.length()) { + if (this.asIndex == this.accessSequence.length()) { + this.transition.setOutput(out); + } + + // if the ADT only consists of a leaf, we just set the transition output + if (ADTUtil.isLeafNode(super.currentADTNode)) { + return Response.FINISHED; + } + + asIndex++; + return Response.SYMBOL; + } else { + return super.processOutput(out); + } + } + + @Override + protected void resetProgress() { + this.asIndex = 0; + } + + ADTTransition getTransition() { + return transition; + } + + Word getAccessSequence() { + return this.accessSequence; + } +} + + diff --git a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/ADTLearner.java b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/ADTLearner.java index ad4262044e..178378c723 100644 --- a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/ADTLearner.java +++ b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/ADTLearner.java @@ -17,12 +17,13 @@ import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; -import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.Queue; import java.util.Set; @@ -49,10 +50,10 @@ import de.learnlib.algorithm.adt.model.ObservationTree; import de.learnlib.algorithm.adt.model.ReplacementResult; import de.learnlib.algorithm.adt.util.ADTUtil; -import de.learnlib.algorithm.adt.util.SQOOTBridge; import de.learnlib.counterexample.LocalSuffixFinders; import de.learnlib.logging.Category; -import de.learnlib.oracle.SymbolQueryOracle; +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.oracle.MembershipOracle.MealyMembershipOracle; import de.learnlib.query.DefaultQuery; import de.learnlib.tooling.annotation.builder.GenerateBuilder; import de.learnlib.util.MQUtil; @@ -61,7 +62,6 @@ import net.automatalib.automaton.transducer.MealyMachine; import net.automatalib.common.util.Pair; import net.automatalib.word.Word; -import net.automatalib.word.WordBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -81,7 +81,8 @@ public class ADTLearner implements LearningAlgorithm.MealyLearner, private static final Logger LOGGER = LoggerFactory.getLogger(ADTLearner.class); private final Alphabet alphabet; - private final SQOOTBridge oracle; + private final AdaptiveMembershipOracle oracle; + private final MealyMembershipOracle mqo; private final LeafSplitter leafSplitter; private final ADTExtender adtExtender; private final SubtreeReplacer subtreeReplacer; @@ -93,7 +94,7 @@ public class ADTLearner implements LearningAlgorithm.MealyLearner, private ADT, I, O> adt; public ADTLearner(Alphabet alphabet, - SymbolQueryOracle oracle, + AdaptiveMembershipOracle oracle, LeafSplitter leafSplitter, ADTExtender adtExtender, SubtreeReplacer subtreeReplacer) { @@ -102,15 +103,16 @@ public ADTLearner(Alphabet alphabet, @GenerateBuilder(defaults = BuilderDefaults.class) public ADTLearner(Alphabet alphabet, - SymbolQueryOracle oracle, + AdaptiveMembershipOracle oracle, LeafSplitter leafSplitter, ADTExtender adtExtender, SubtreeReplacer subtreeReplacer, boolean useObservationTree) { this.alphabet = alphabet; - this.observationTree = new ObservationTree<>(this.alphabet); - this.oracle = new SQOOTBridge<>(this.observationTree, oracle, useObservationTree); + this.observationTree = new ObservationTree<>(this.alphabet, oracle, useObservationTree); + this.oracle = this.observationTree; + this.mqo = new Adaptive2MembershipWrapper<>(oracle); this.leafSplitter = leafSplitter; this.adtExtender = adtExtender; @@ -129,7 +131,6 @@ public void startLearning() { final ADTState initialState = this.hypothesis.addInitialState(); initialState.setAccessSequence(Word.epsilon()); this.observationTree.initialize(initialState); - this.oracle.initialize(); this.adt.initialize(initialState); for (I i : this.alphabet) { @@ -163,7 +164,7 @@ public boolean refineHypothesis(DefaultQuery> ce) { // subtree replacements may reactivate old CEs for (DefaultQuery> oldCE : this.allCounterExamples) { - if (!this.hypothesis.computeOutput(oldCE.getInput()).equals(oldCE.getOutput())) { + if (MQUtil.isCounterexample(oldCE, this.hypothesis)) { this.openCounterExamples.add(oldCE); } } @@ -181,10 +182,8 @@ public boolean refineHypothesisInternal(DefaultQuery> ceQuery) { } // Determine a counterexample decomposition (u, a, v) - final int suffixIdx = LocalSuffixFinders.RIVEST_SCHAPIRE.findSuffixIndex(ceQuery, - this.hypothesis, - this.hypothesis, - this.oracle); + final int suffixIdx = + LocalSuffixFinders.RIVEST_SCHAPIRE.findSuffixIndex(ceQuery, this.hypothesis, this.hypothesis, this.mqo); if (suffixIdx == -1) { throw new IllegalStateException(); @@ -238,8 +237,8 @@ public boolean refineHypothesisInternal(DefaultQuery> ceQuery) { newNode = this.adt.extendLeaf(nodeToSplit, completeSplitter, oldOutput, newOutput, this.leafSplitter); } else { // directly insert into observation tree, because we use it for finding a splitter - this.observationTree.addTrace(uaState, v, this.oracle.answerQuery(uaAccessSequence, v)); - this.observationTree.addTrace(newState, v, this.oracle.answerQuery(uAccessSequenceWithA, v)); + this.observationTree.addTrace(uaState, v, this.mqo.answerQuery(uaAccessSequence, v)); + this.observationTree.addTrace(newState, v, this.mqo.answerQuery(uAccessSequenceWithA, v)); // in doubt, we will always find v final Word otSepWord = this.observationTree.findSeparatingWord(uaState, newState); @@ -297,58 +296,23 @@ public boolean refineHypothesisInternal(DefaultQuery> ceQuery) { */ private void closeTransitions() { while (!this.openTransitions.isEmpty()) { - this.closeTransition(this.openTransitions.poll()); - } - } - - /** - * Close the given transitions by means of sifting the associated long prefix through the ADT. - * - * @param transition - * the transition to close - */ - private void closeTransition(ADTTransition transition) { - - if (!transition.needsSifting()) { - return; - } - - final Word accessSequence = transition.getSource().getAccessSequence(); - final I symbol = transition.getInput(); - - this.oracle.reset(); - for (I i : accessSequence) { - this.oracle.query(i); - } - transition.setOutput(this.oracle.query(symbol)); + final Collection> queries = new ArrayList<>(this.openTransitions.size()); - final Word longPrefix = accessSequence.append(symbol); - final ADTNode, I, O> finalNode = - this.adt.sift(this.oracle, longPrefix, transition.getSiftNode()); - - assert ADTUtil.isLeafNode(finalNode); - - final ADTState targetState; - - // new state discovered while sifting - if (finalNode.getHypothesisState() == null) { - targetState = this.hypothesis.addState(); - targetState.setAccessSequence(longPrefix); - - finalNode.setHypothesisState(targetState); - transition.setIsSpanningTreeEdge(true); + //create a query object for every transition + for (ADTTransition transition : this.openTransitions) { + if (transition.needsSifting()) { + queries.add(new ADTAdaptiveQuery<>(transition, transition.getSiftNode())); + } + } - this.observationTree.addState(targetState, longPrefix, transition.getOutput()); + this.openTransitions.clear(); + this.oracle.processQueries(queries); - for (I i : this.alphabet) { - this.openTransitions.add(this.hypothesis.createOpenTransition(targetState, i, this.adt.getRoot())); + for (ADTAdaptiveQuery query : queries) { + processAnsweredQuery(query); } - } else { - targetState = finalNode.getHypothesisState(); } - - transition.setTarget(targetState); } @Override @@ -361,7 +325,9 @@ public void closeTransition(ADTState state, I input) { final ADTNode, I, O> ads = transition.getSiftNode(); final int oldNumberOfFinalStates = ADTUtil.collectLeaves(ads).size(); - this.closeTransition(transition); + final ADTAdaptiveQuery query = new ADTAdaptiveQuery<>(transition, transition.getSiftNode()); + this.oracle.processQueries(Collections.singleton(query)); + processAnsweredQuery(query); final int newNumberOfFinalStates = ADTUtil.collectLeaves(ads).size(); @@ -371,6 +337,50 @@ public void closeTransition(ADTState state, I input) { } } + private void processAnsweredQuery(ADTAdaptiveQuery query) { + if (query.needsPostProcessing()) { + final ADTNode, I, O> parent = query.getCurrentADTNode(); + final O out = query.getTempOut(); + final ADTNode, I, O> succ = parent.getChild(out); + + // first time we process the successor + if (succ == null) { + // add new state to the hypothesis and set the accessSequence + final ADTState newState = this.hypothesis.addState(); + final Word longPrefix = query.getAccessSequence().append(query.getTransition().getInput()); + newState.setAccessSequence(longPrefix); + + // configure the transition + final ADTTransition transition = query.getTransition(); + transition.setTarget(newState); + transition.setIsSpanningTreeEdge(true); + + // add new leaf node to ADT + final ADTNode, I, O> result = new ADTLeafNode<>(parent, newState); + parent.getChildren().put(out, result); + + // add the observations to the observation tree + O transitionOutput = query.getTransition().getOutput(); + this.observationTree.addState(newState, longPrefix, transitionOutput); + + // query successors + for (I i : this.alphabet) { + this.openTransitions.add(this.hypothesis.createOpenTransition(newState, i, this.adt.getRoot())); + } + } else { + assert ADTUtil.isLeafNode(succ); + // state has been created before, just update target + query.getTransition().setTarget(succ.getHypothesisState()); + } + } else { + // update target + final ADTTransition transition = query.getTransition(); + final ADTNode, I, O> adtNode = query.getCurrentADTNode(); + assert ADTUtil.isLeafNode(adtNode); + transition.setTarget(adtNode.getHypothesisState()); + } + } + @Override public boolean isTransitionDefined(ADTState state, I input) { final ADTTransition transition = this.hypothesis.getTransition(state, input); @@ -386,7 +396,7 @@ public void addAlphabetSymbol(I symbol) { } this.hypothesis.addAlphabetSymbol(symbol); - this.observationTree.getObservationTree().addAlphabetSymbol(symbol); + this.observationTree.addAlphabetSymbol(symbol); // check if we already have information about the symbol (then the transition is defined) so we don't post // redundant queries @@ -423,7 +433,6 @@ public void resume(ADTLearnerState, I, O> state) { this.observationTree.initialize(this.hypothesis.getStates(), ADTState::getAccessSequence, this.hypothesis::computeOutput); - this.oracle.initialize(); } } @@ -648,59 +657,38 @@ private ADTNode, I, O> verifyADS(ADTNode, I, O> no .forEach(x -> traces.put(x.getHypothesisState(), ADTUtil.buildTraceForNode(x))); final Pair, Word> parentTrace = ADTUtil.buildTraceForNode(nodeToReplace); - final Word parentInput = parentTrace.getFirst(); - final Word parentOutput = parentTrace.getSecond(); ADTNode, I, O> result = null; - // validate - for (Map.Entry, Pair, Word>> entry : traces.entrySet()) { - final ADTState state = entry.getKey(); - final Word accessSequence = state.getAccessSequence(); - - this.oracle.reset(); - accessSequence.forEach(this.oracle::query); - parentInput.forEach(this.oracle::query); + final List> queries = new ArrayList<>(traces.size()); - final Word adsInput = entry.getValue().getFirst(); - final Word adsOutput = entry.getValue().getSecond(); - - final WordBuilder inputWb = new WordBuilder<>(adsInput.size()); - final WordBuilder outputWb = new WordBuilder<>(adsInput.size()); - - final Iterator inputIter = adsInput.iterator(); - final Iterator outputIter = adsOutput.iterator(); - - boolean equal = true; - while (equal && inputIter.hasNext()) { - final I in = inputIter.next(); - final O realOut = this.oracle.query(in); - final O expectedOut = outputIter.next(); + for (Entry, Pair, Word>> e : traces.entrySet()) { + final ADTState state = e.getKey(); + final Pair, Word> ads = e.getValue(); + queries.add(new ADSVerificationQuery<>(state.getAccessSequence().concat(parentTrace.getFirst()), + ads.getFirst(), + ads.getSecond(), + state)); + } - inputWb.append(in); - outputWb.append(realOut); + this.oracle.processQueries(queries); - if (!expectedOut.equals(realOut)) { - equal = false; - } - } + for (ADSVerificationQuery query : queries) { + final ADTNode, I, O> trace; + final DefaultQuery> ce = query.getCounterexample(); - final Word traceInput = inputWb.toWord(); - final Word traceOutput = outputWb.toWord(); - - if (!equal) { - this.openCounterExamples.add(new DefaultQuery<>(accessSequence.concat(parentInput, traceInput), - this.hypothesis.computeOutput(state.getAccessSequence()) - .concat(parentOutput, traceOutput))); + if (ce != null) { + this.openCounterExamples.add(ce); + trace = ADTUtil.buildADSFromObservation(ce.getSuffix(), ce.getOutput(), query.getState()); + } else { + trace = ADTUtil.buildADSFromObservation(query.getSuffix(), query.getExpectedOutput(), query.getState()); } - final ADTNode, I, O> trace = ADTUtil.buildADSFromObservation(traceInput, traceOutput, state); - if (result == null) { result = trace; } else { if (!ADTUtil.mergeADS(result, trace)) { - this.resolveAmbiguities(nodeToReplace, result, state, cachedLeaves); + this.resolveAmbiguities(nodeToReplace, result, query.getState(), cachedLeaves); } } } @@ -732,39 +720,26 @@ private void resolveAmbiguities(ADTNode, I, O> nodeToReplace, Set, I, O>> cachedLeaves) { final Pair, Word> parentTrace = ADTUtil.buildTraceForNode(nodeToReplace); - final Word parentInput = parentTrace.getFirst(); - final Word effectiveAccessSequence = state.getAccessSequence().concat(parentInput); + final ADSAmbiguityQuery query = + new ADSAmbiguityQuery<>(state.getAccessSequence(), parentTrace.getFirst(), newADS); - this.oracle.reset(); - effectiveAccessSequence.forEach(this.oracle::query); - - ADTNode, I, O> iter = newADS; - while (!ADTUtil.isLeafNode(iter)) { - - if (ADTUtil.isResetNode(iter)) { - this.oracle.reset(); - state.getAccessSequence().forEach(this.oracle::query); - iter = iter.getChildren().values().iterator().next(); - } else { - final O output = this.oracle.query(iter.getSymbol()); - final ADTNode, I, O> succ = iter.getChildren().get(output); + this.oracle.processQuery(query); - if (succ == null) { - final ADTNode, I, O> newFinal = new ADTLeafNode<>(iter, state); - iter.getChildren().put(output, newFinal); - return; - } - - iter = succ; - } + if (query.needsPostProcessing()) { + final ADTNode, I, O> prev = query.getCurrentADTNode(); + final ADTNode, I, O> newFinal = new ADTLeafNode<>(prev, state); + prev.getChildren().put(query.getTempOut(), newFinal); + return; } + final ADTNode, I, O> finalNode = query.getCurrentADTNode(); ADTNode, I, O> oldReference = null, newReference = null; + for (ADTNode, I, O> leaf : cachedLeaves) { final ADTState hypState = leaf.getHypothesisState(); assert hypState != null; - if (hypState.equals(iter.getHypothesisState())) { + if (hypState.equals(finalNode.getHypothesisState())) { oldReference = leaf; } else if (hypState.equals(state)) { newReference = leaf; @@ -784,7 +759,7 @@ private void resolveAmbiguities(ADTNode, I, O> nodeToReplace, final Word newOutputTrace = lcaTrace.getSecond().append(lcaResult.secondOutput); final ADTNode, I, O> oldTrace = - ADTUtil.buildADSFromObservation(sepWord, oldOutputTrace, iter.getHypothesisState()); + ADTUtil.buildADSFromObservation(sepWord, oldOutputTrace, finalNode.getHypothesisState()); final ADTNode, I, O> newTrace = ADTUtil.buildADSFromObservation(sepWord, newOutputTrace, state); if (!ADTUtil.mergeADS(oldTrace, newTrace)) { @@ -792,9 +767,9 @@ private void resolveAmbiguities(ADTNode, I, O> nodeToReplace, } final ADTNode, I, O> reset = new ADTResetNode<>(oldTrace); - final ADTNode, I, O> parent = iter.getParent(); + final ADTNode, I, O> parent = finalNode.getParent(); assert parent != null; - final O parentOutput = ADTUtil.getOutputForSuccessor(parent, iter); + final O parentOutput = ADTUtil.getOutputForSuccessor(parent, finalNode); parent.getChildren().put(parentOutput, reset); reset.setParent(parent); diff --git a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/AbstractAdaptiveQuery.java b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/AbstractAdaptiveQuery.java new file mode 100644 index 0000000000..b3c440cbc3 --- /dev/null +++ b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/AbstractAdaptiveQuery.java @@ -0,0 +1,74 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.algorithm.adt.learner; + +import de.learnlib.algorithm.adt.adt.ADTNode; +import de.learnlib.algorithm.adt.automaton.ADTState; +import de.learnlib.algorithm.adt.util.ADTUtil; +import de.learnlib.query.AdaptiveQuery; + +/** + * Utility class to share common implementations. + * + * @param + * input symbol type + * @param + * output symbol type + */ +abstract class AbstractAdaptiveQuery implements AdaptiveQuery { + + protected ADTNode, I, O> currentADTNode; + private O tempOut; + + AbstractAdaptiveQuery(ADTNode, I, O> currentADTNode) { + this.currentADTNode = currentADTNode; + } + + @Override + public Response processOutput(O out) { + + final ADTNode, I, O> succ = currentADTNode.getChild(out); + + if (succ == null) { + this.tempOut = out; + return Response.FINISHED; + } else if (ADTUtil.isResetNode(succ)) { + this.currentADTNode = succ.getChild(null); + resetProgress(); + return Response.RESET; + } else if (ADTUtil.isSymbolNode(succ)) { + this.currentADTNode = succ; + return Response.SYMBOL; + } else { + this.currentADTNode = succ; + return Response.FINISHED; + } + } + + protected abstract void resetProgress(); + + boolean needsPostProcessing() { + return this.tempOut != null; + } + + ADTNode, I, O> getCurrentADTNode() { + return currentADTNode; + } + + O getTempOut() { + return tempOut; + } +} diff --git a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/Adaptive2MembershipWrapper.java b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/Adaptive2MembershipWrapper.java new file mode 100644 index 0000000000..d9367385d6 --- /dev/null +++ b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/Adaptive2MembershipWrapper.java @@ -0,0 +1,59 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.algorithm.adt.learner; + +import java.util.ArrayList; +import java.util.Collection; + +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.oracle.MembershipOracle.MealyMembershipOracle; +import de.learnlib.query.AdaptiveQuery; +import de.learnlib.query.Query; +import de.learnlib.util.mealy.PresetAdaptiveQuery; +import net.automatalib.word.Word; + +/** + * Utility class to answer regular {@link Query queries} with an {@link AdaptiveMembershipOracle}. + * + * @param + * input symbol type + * @param + * output symbol type + */ +class Adaptive2MembershipWrapper implements MealyMembershipOracle { + + private final AdaptiveMembershipOracle oracle; + + Adaptive2MembershipWrapper(AdaptiveMembershipOracle oracle) { + this.oracle = oracle; + } + + @Override + public void processQueries(Collection>> queries) { + + Collection> adaptiveQueries = new ArrayList<>(); + + for (Query> query : queries) { + if (query.getSuffix().isEmpty()) { + query.answer(Word.epsilon()); + } else { + adaptiveQueries.add(new PresetAdaptiveQuery<>(query)); + } + } + + this.oracle.processQueries(adaptiveQueries); + } +} diff --git a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/model/ObservationTree.java b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/model/ObservationTree.java index 1005672e1e..c9b5566f29 100644 --- a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/model/ObservationTree.java +++ b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/model/ObservationTree.java @@ -17,18 +17,19 @@ import java.util.Collection; import java.util.HashMap; -import java.util.Iterator; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.function.Function; import de.learnlib.algorithm.LearningAlgorithm; import de.learnlib.algorithm.adt.adt.ADTNode; import de.learnlib.algorithm.adt.util.ADTUtil; +import de.learnlib.filter.cache.mealy.AdaptiveQueryCache; +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.query.AdaptiveQuery; import net.automatalib.alphabet.Alphabet; -import net.automatalib.automaton.transducer.impl.FastMealy; -import net.automatalib.automaton.transducer.impl.FastMealyState; +import net.automatalib.alphabet.SupportsGrowingAlphabet; +import net.automatalib.automaton.transducer.MealyMachine; import net.automatalib.common.util.Pair; import net.automatalib.util.automaton.equivalence.NearLinearEquivalenceTest; import net.automatalib.word.Word; @@ -46,24 +47,29 @@ * @param * output alphabet type */ -public class ObservationTree { +public class ObservationTree implements AdaptiveMembershipOracle, SupportsGrowingAlphabet { private final Alphabet alphabet; - private final FastMealy observationTree; + private final AdaptiveMembershipOracle delegate; - private final Map> nodeToObservationMap; + private final AdaptiveQueryCache cache; + + private final Map nodeToObservationMap; + + public ObservationTree(Alphabet alphabet, AdaptiveMembershipOracle delegate, boolean useCache) { + this.cache = new AdaptiveQueryCache<>(delegate, alphabet); + + if (useCache) { + this.delegate = this.cache; + } else { + this.delegate = delegate; + } - public ObservationTree(Alphabet alphabet) { this.alphabet = alphabet; - this.observationTree = new FastMealy<>(alphabet); this.nodeToObservationMap = new HashMap<>(); } - public FastMealy getObservationTree() { - return observationTree; - } - /** * Initialize the observation tree with initial hypothesis state. Usually used during {@link * LearningAlgorithm#startLearning()} @@ -72,7 +78,7 @@ public FastMealy getObservationTree() { * the initial state of the hypothesis */ public void initialize(S state) { - final FastMealyState init = this.observationTree.addInitialState(); + final Integer init = this.cache.getCache().getInitialState(); this.nodeToObservationMap.put(state, init); } @@ -89,11 +95,11 @@ public void initialize(S state) { public void initialize(Collection states, Function> asFunction, Function, Word> outputFunction) { - final FastMealyState init = this.observationTree.addInitialState(); + final Integer init = this.cache.getCache().getInitialState(); for (S s : states) { final Word as = asFunction.apply(s); - final FastMealyState treeNode = this.addTrace(init, as, outputFunction.apply(as)); + final Integer treeNode = this.addTrace(init, as, outputFunction.apply(as)); this.nodeToObservationMap.put(s, treeNode); } } @@ -112,32 +118,8 @@ public void addTrace(S state, Word input, Word output) { this.addTrace(this.nodeToObservationMap.get(state), input, output); } - private FastMealyState addTrace(FastMealyState state, Word input, Word output) { - - assert input.length() == output.length() : "Traces differ in length"; - - final Iterator inputIter = input.iterator(); - final Iterator outputIter = output.iterator(); - FastMealyState iter = state; - - while (inputIter.hasNext()) { - - final I nextInput = inputIter.next(); - final O nextOuput = outputIter.next(); - final FastMealyState nextState; - - if (this.observationTree.getTransition(iter, nextInput) == null) { - nextState = this.observationTree.addState(); - this.observationTree.addTransition(iter, nextInput, nextState, nextOuput); - } else { - assert Objects.equals(nextOuput, this.observationTree.getOutput(iter, nextInput)) : "Inconsistent observations"; - nextState = this.observationTree.getSuccessor(iter, nextInput); - } - - iter = nextState; - } - - return iter; + private Integer addTrace(Integer state, Word input, Word output) { + return this.cache.insert(state, input, output); } /** @@ -151,7 +133,7 @@ private FastMealyState addTrace(FastMealyState state, Word input, Word< */ public void addTrace(S state, ADTNode adtNode) { - final FastMealyState internalState = this.nodeToObservationMap.get(state); + final Integer internalState = this.nodeToObservationMap.get(state); ADTNode adsIter = adtNode; @@ -179,17 +161,8 @@ public void addState(S newState, Word accessSequence, O output) { final Word prefix = accessSequence.prefix(accessSequence.length() - 1); final I sym = accessSequence.lastSymbol(); - final FastMealyState pred = - this.observationTree.getSuccessor(this.observationTree.getInitialState(), prefix); - final FastMealyState target; - - assert pred != null; - if (pred.getTransitionObject(alphabet.getSymbolIndex(sym)) == null) { - target = this.observationTree.addState(); - this.observationTree.addTransition(pred, sym, target, output); - } else { - target = this.observationTree.getSuccessor(pred, sym); - } + final Integer pred = this.cache.getCache().getState(prefix); + final Integer target = this.cache.insert(pred, Word.fromLetter(sym), Word.fromLetter(output)); this.nodeToObservationMap.put(newState, target); } @@ -209,15 +182,16 @@ public void addState(S newState, Word accessSequence, O output) { */ public Optional> findSeparatingWord(S s1, S s2, Word prefix) { - final FastMealyState n1 = this.nodeToObservationMap.get(s1); - final FastMealyState n2 = this.nodeToObservationMap.get(s2); + final MealyMachine cache = this.cache.getCache(); + + final Integer n1 = this.nodeToObservationMap.get(s1); + final Integer n2 = this.nodeToObservationMap.get(s2); - final FastMealyState s1Succ = this.observationTree.getSuccessor(n1, prefix); - final FastMealyState s2Succ = this.observationTree.getSuccessor(n2, prefix); + final Integer s1Succ = cache.getSuccessor(n1, prefix); + final Integer s2Succ = cache.getSuccessor(n2, prefix); if (s1Succ != null && s2Succ != null) { - final Word sepWord = - NearLinearEquivalenceTest.findSeparatingWord(this.observationTree, s1Succ, s2Succ, alphabet, true); + final Word sepWord = NearLinearEquivalenceTest.findSeparatingWord(cache, s1Succ, s2Succ, alphabet, true); if (sepWord != null) { return Optional.of(sepWord); @@ -239,10 +213,10 @@ public Optional> findSeparatingWord(S s1, S s2, Word prefix) { */ public Word findSeparatingWord(S s1, S s2) { - final FastMealyState n1 = this.nodeToObservationMap.get(s1); - final FastMealyState n2 = this.nodeToObservationMap.get(s2); + final Integer n1 = this.nodeToObservationMap.get(s1); + final Integer n2 = this.nodeToObservationMap.get(s2); - return NearLinearEquivalenceTest.findSeparatingWord(this.observationTree, n1, n2, this.alphabet, true); + return NearLinearEquivalenceTest.findSeparatingWord(this.cache.getCache(), n1, n2, this.alphabet, true); } /** @@ -257,8 +231,18 @@ public Word findSeparatingWord(S s1, S s2) { * @return the previously stored output behavior of the system under learning */ public Word trace(S s, Word input) { - final FastMealyState q = this.nodeToObservationMap.get(s); - return this.observationTree.computeStateOutput(q, input); + final Integer q = this.nodeToObservationMap.get(s); + return this.cache.getCache().computeStateOutput(q, input); } + @Override + public void processQueries(Collection> queries) { + this.delegate.processQueries(queries); + } + + @Override + public void addAlphabetSymbol(I i) { + this.alphabet.asGrowingAlphabetOrThrowException().add(i); + this.cache.addAlphabetSymbol(i); + } } diff --git a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/util/SQOOTBridge.java b/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/util/SQOOTBridge.java deleted file mode 100644 index bfbe2a3488..0000000000 --- a/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/util/SQOOTBridge.java +++ /dev/null @@ -1,120 +0,0 @@ -/* Copyright (C) 2013-2024 TU Dortmund University - * This file is part of LearnLib, http://www.learnlib.de/. - * - * 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. - */ -package de.learnlib.algorithm.adt.util; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -import de.learnlib.algorithm.adt.model.ObservationTree; -import de.learnlib.oracle.SymbolQueryOracle; -import net.automatalib.automaton.transducer.impl.FastMealy; -import net.automatalib.automaton.transducer.impl.FastMealyState; - -/** - * A utility class that links an observation tree with a symbol query oracle, meaning that all queries to the symbol - * query oracle will be stored in the observation tree. Additionally, if a query can be answered by the observation tree - * (and caching is enabled) the delegated symbol query oracle will not be queried. - * - * @param - * input alphabet type - * @param - * output alphabet type - */ -public class SQOOTBridge implements SymbolQueryOracle { - - private final FastMealy observationTree; - - private final SymbolQueryOracle delegate; - - private final boolean enableCache; - - private final List currentTrace; - - private FastMealyState currentState; - - private boolean currentTraceValid; - - public SQOOTBridge(ObservationTree observationTree, - SymbolQueryOracle delegate, - boolean enableCache) { - this.observationTree = observationTree.getObservationTree(); - this.delegate = delegate; - this.enableCache = enableCache; - this.currentTrace = enableCache ? new ArrayList<>() : Collections.emptyList(); - } - - public void initialize() { - this.currentState = this.observationTree.getInitialState(); - this.currentTraceValid = enableCache; - } - - @Override - public O query(I i) { - - if (this.currentTraceValid) { - final FastMealyState succ = this.observationTree.getSuccessor(this.currentState, i); - - if (succ != null) { - final O output = this.observationTree.getOutput(this.currentState, i); - this.currentTrace.add(i); - this.currentState = succ; - return output; - } else { - this.currentTraceValid = false; - this.delegate.reset(); - - for (I trace : this.currentTrace) { - this.delegate.query(trace); - } - } - } - - final O output = this.delegate.query(i); - - final FastMealyState nextState; - final FastMealyState succ = this.observationTree.getSuccessor(this.currentState, i); - - if (succ == null) { - final FastMealyState newState = this.observationTree.addState(); - nextState = newState; - - if (this.enableCache) { - this.observationTree.addTransition(this.currentState, i, newState, output); - } - } else { - assert Objects.equals(output, this.observationTree.getOutput(this.currentState, i)) : "Inconsistent observations"; - nextState = succ; - } - - this.currentState = nextState; - - return output; - } - - @Override - public void reset() { - this.currentState = this.observationTree.getInitialState(); - - if (this.enableCache) { - this.currentTrace.clear(); - this.currentTraceValid = true; - } else { - this.delegate.reset(); - } - } -} diff --git a/algorithms/active/adt/src/main/java/module-info.java b/algorithms/active/adt/src/main/java/module-info.java index 30517de158..356e0c80d3 100644 --- a/algorithms/active/adt/src/main/java/module-info.java +++ b/algorithms/active/adt/src/main/java/module-info.java @@ -33,6 +33,7 @@ requires de.learnlib.api; requires de.learnlib.common.counterexample; requires de.learnlib.common.util; + requires de.learnlib.filter.cache; requires net.automatalib.api; requires net.automatalib.common.smartcollection; requires net.automatalib.common.util; diff --git a/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/it/ADTIT.java b/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/it/ADTIT.java index 9647aeaa43..870a6b4959 100644 --- a/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/it/ADTIT.java +++ b/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/it/ADTIT.java @@ -41,7 +41,7 @@ protected void addLearnerVariants(Alphabet alphabet, final ADTLearnerBuilder builder = new ADTLearnerBuilder<>(); builder.setAlphabet(alphabet); - builder.setOracle(new MQ2SQWrapper<>(mqOracle)); + builder.setOracle(new MQ2AQWrapper<>(mqOracle)); final List leafSplitters = Arrays.asList(LeafSplitters.DEFAULT_SPLITTER, LeafSplitters.EXTEND_PARENT); diff --git a/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/it/MQ2SQWrapper.java b/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/it/MQ2AQWrapper.java similarity index 52% rename from algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/it/MQ2SQWrapper.java rename to algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/it/MQ2AQWrapper.java index 45e1a1de0a..4a159d925b 100644 --- a/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/it/MQ2SQWrapper.java +++ b/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/it/MQ2AQWrapper.java @@ -15,29 +15,45 @@ */ package de.learnlib.algorithm.adt.it; +import java.util.Collection; + +import de.learnlib.oracle.AdaptiveMembershipOracle; import de.learnlib.oracle.MembershipOracle; -import de.learnlib.oracle.SymbolQueryOracle; +import de.learnlib.query.AdaptiveQuery; +import de.learnlib.query.AdaptiveQuery.Response; import net.automatalib.word.Word; import net.automatalib.word.WordBuilder; -public class MQ2SQWrapper implements SymbolQueryOracle { +public class MQ2AQWrapper implements AdaptiveMembershipOracle { final WordBuilder wb; final MembershipOracle> oracle; - public MQ2SQWrapper(MembershipOracle> oracle) { + public MQ2AQWrapper(MembershipOracle> oracle) { this.oracle = oracle; this.wb = new WordBuilder<>(); } @Override - public O query(I i) { - this.wb.append(i); - return this.oracle.answerQuery(wb.toWord()).lastSymbol(); + public void processQueries(Collection> adaptiveQueries) { + for (AdaptiveQuery q : adaptiveQueries) { + processQuery(q); + } } - @Override - public void reset() { - this.wb.clear(); + public void processQuery(AdaptiveQuery query) { + wb.clear(); + Response response; + + do { + wb.append(query.getInput()); + final O out = this.oracle.answerQuery(wb.toWord()).lastSymbol(); + + response = query.processOutput(out); + + if (response == Response.RESET) { + wb.clear(); + } + } while (response != Response.FINISHED); } } diff --git a/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/learner/ADTGrowingAlphabetTest.java b/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/learner/ADTGrowingAlphabetTest.java index ecabff514b..26aed052a9 100644 --- a/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/learner/ADTGrowingAlphabetTest.java +++ b/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/learner/ADTGrowingAlphabetTest.java @@ -22,8 +22,8 @@ import de.learnlib.algorithm.adt.config.LeafSplitters; import de.learnlib.algorithm.adt.config.SubtreeReplacers; import de.learnlib.driver.simulator.MealySimulatorSUL; -import de.learnlib.oracle.SymbolQueryOracle; -import de.learnlib.oracle.membership.SULSymbolQueryOracle; +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.oracle.membership.SULAdaptiveOracle; import de.learnlib.testsupport.AbstractGrowingAlphabetTest; import net.automatalib.alphabet.Alphabet; import net.automatalib.alphabet.impl.Alphabets; @@ -32,7 +32,7 @@ import net.automatalib.word.Word; public class ADTGrowingAlphabetTest - extends AbstractGrowingAlphabetTest, MealyMachine, SymbolQueryOracle, Integer, Word> { + extends AbstractGrowingAlphabetTest, MealyMachine, AdaptiveMembershipOracle, Integer, Word> { @Override protected Alphabet getInitialAlphabet() { @@ -50,12 +50,12 @@ protected Collection getAlphabetExtensions() { } @Override - protected SymbolQueryOracle getOracle(MealyMachine target) { - return new SULSymbolQueryOracle<>(new MealySimulatorSUL<>(target)); + protected AdaptiveMembershipOracle getOracle(MealyMachine target) { + return new SULAdaptiveOracle<>(new MealySimulatorSUL<>(target)); } @Override - protected ADTLearner getLearner(SymbolQueryOracle oracle, + protected ADTLearner getLearner(AdaptiveMembershipOracle oracle, Alphabet alphabet) { return new ADTLearner<>(alphabet, oracle, diff --git a/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/learner/ADTResumableLearnerTest.java b/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/learner/ADTResumableLearnerTest.java index ec7e283cd0..4d30800ce9 100644 --- a/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/learner/ADTResumableLearnerTest.java +++ b/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/learner/ADTResumableLearnerTest.java @@ -19,8 +19,8 @@ import de.learnlib.algorithm.adt.automaton.ADTState; import de.learnlib.driver.simulator.MealySimulatorSUL; -import de.learnlib.oracle.SymbolQueryOracle; -import de.learnlib.oracle.membership.SULSymbolQueryOracle; +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.oracle.membership.SULAdaptiveOracle; import de.learnlib.testsupport.AbstractResumableLearnerTest; import net.automatalib.alphabet.Alphabet; import net.automatalib.alphabet.impl.Alphabets; @@ -29,7 +29,7 @@ import net.automatalib.word.Word; public class ADTResumableLearnerTest - extends AbstractResumableLearnerTest, MealyMachine, SymbolQueryOracle, Integer, Word, ADTLearnerState, Integer, Character>> { + extends AbstractResumableLearnerTest, MealyMachine, AdaptiveMembershipOracle, Integer, Word, ADTLearnerState, Integer, Character>> { @Override protected Alphabet getInitialAlphabet() { @@ -42,12 +42,12 @@ protected Alphabet getInitialAlphabet() { } @Override - protected SymbolQueryOracle getOracle(MealyMachine target) { - return new SULSymbolQueryOracle<>(new MealySimulatorSUL<>(target)); + protected AdaptiveMembershipOracle getOracle(MealyMachine target) { + return new SULAdaptiveOracle<>(new MealySimulatorSUL<>(target)); } @Override - protected ADTLearner getLearner(SymbolQueryOracle oracle, + protected ADTLearner getLearner(AdaptiveMembershipOracle oracle, Alphabet alphabet) { return new ADTLearnerBuilder().withAlphabet(alphabet).withOracle(oracle).create(); } diff --git a/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/learner/ADTVisualizationTest.java b/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/learner/ADTVisualizationTest.java index 7dff9348a0..eac086b5a4 100644 --- a/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/learner/ADTVisualizationTest.java +++ b/algorithms/active/adt/src/test/java/de/learnlib/algorithm/adt/learner/ADTVisualizationTest.java @@ -18,7 +18,7 @@ import java.io.IOException; import java.io.StringWriter; -import de.learnlib.oracle.membership.SULSymbolQueryOracle; +import de.learnlib.oracle.membership.SULAdaptiveOracle; import de.learnlib.sul.SUL; import de.learnlib.testsupport.AbstractVisualizationTest; import de.learnlib.testsupport.example.mealy.ExampleCoffeeMachine.Input; @@ -35,7 +35,7 @@ protected ADTLearner getLearnerBuilder(@UnderInitialization ADTVi Alphabet alphabet, SUL sul) { return new ADTLearnerBuilder().withAlphabet(alphabet) - .withOracle(new SULSymbolQueryOracle<>(sul)) + .withOracle(new SULAdaptiveOracle<>(sul)) .create(); } diff --git a/api/src/main/java/de/learnlib/oracle/AdaptiveMembershipOracle.java b/api/src/main/java/de/learnlib/oracle/AdaptiveMembershipOracle.java new file mode 100644 index 0000000000..b2aed5d3ff --- /dev/null +++ b/api/src/main/java/de/learnlib/oracle/AdaptiveMembershipOracle.java @@ -0,0 +1,67 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.oracle; + +import java.util.Collection; +import java.util.Collections; + +import de.learnlib.query.AdaptiveQuery; +import de.learnlib.query.AdaptiveQuery.Response; + +/** + * An adaptive variation of the {@link MembershipOracle} that is tailored towards answering + * {@link AdaptiveQuery adaptive queries}. + * + * @param + * input symbol type + * @param + * output symbol type + */ +public interface AdaptiveMembershipOracle extends BatchProcessor> { + + /** + * Processes a single query. When this method returns, the provided inputs of the {@link AdaptiveQuery#getInput()} + * method will have been evaluated on the system under learning and its responses will have been forwarded to the + * {@link AdaptiveQuery#processOutput(Object)} method until the method has returned {@link Response#FINISHED}. + *

+ * The default implementation of this method will simply wrap the provided {@link AdaptiveQuery} in a singleton + * {@link Collection} using {@link Collections#singleton(Object)}. Implementations in subclasses should override + * this method to circumvent the Collection object creation, if possible. + * + * @param query + * the query to process + */ + default void processQuery(AdaptiveQuery query) { + processQueries(Collections.singleton(query)); + } + + /** + * Processes the specified collection of queries. When this method returns, the provided inputs of the + * {@link AdaptiveQuery#getInput()} method will have been evaluated on the system under learning and its responses + * will have been forwarded to the {@link AdaptiveQuery#processOutput(Object)} method until the method has returned + * {@link Response#FINISHED}. + * + * @param queries + * the queries to process + */ + void processQueries(Collection> queries); + + @Override + default void processBatch(Collection> batch) { + processQueries(batch); + } +} + diff --git a/api/src/main/java/de/learnlib/oracle/ParallelAdaptiveOracle.java b/api/src/main/java/de/learnlib/oracle/ParallelAdaptiveOracle.java new file mode 100644 index 0000000000..cf2105bc2a --- /dev/null +++ b/api/src/main/java/de/learnlib/oracle/ParallelAdaptiveOracle.java @@ -0,0 +1,26 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.oracle; + +/** + * {@link ParallelOracle} equivalent for {@link AdaptiveMembershipOracle}s. + * + * @param + * input symbol type + * @param + * output symbol type + */ +public interface ParallelAdaptiveOracle extends ThreadPool, AdaptiveMembershipOracle {} diff --git a/api/src/main/java/de/learnlib/oracle/ParallelOmegaOracle.java b/api/src/main/java/de/learnlib/oracle/ParallelOmegaOracle.java index baa01c806c..eb1086458d 100644 --- a/api/src/main/java/de/learnlib/oracle/ParallelOmegaOracle.java +++ b/api/src/main/java/de/learnlib/oracle/ParallelOmegaOracle.java @@ -16,7 +16,7 @@ package de.learnlib.oracle; /** - * {@link ParallelOracle} equivalent for {@link OmegaMembershipOracle}. + * {@link ParallelOracle} equivalent for {@link OmegaMembershipOracle}s. * * @param * oracle state type diff --git a/api/src/main/java/de/learnlib/oracle/SingleAdaptiveMembershipOracle.java b/api/src/main/java/de/learnlib/oracle/SingleAdaptiveMembershipOracle.java new file mode 100644 index 0000000000..68f84ed646 --- /dev/null +++ b/api/src/main/java/de/learnlib/oracle/SingleAdaptiveMembershipOracle.java @@ -0,0 +1,39 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.oracle; + +import java.util.Collection; + +import de.learnlib.query.AdaptiveQuery; + +/** + * An {@link AdaptiveMembershipOracle} that answers single queries. + * + * @see AdaptiveMembershipOracle + * @see SingleQueryOracle + */ +public interface SingleAdaptiveMembershipOracle extends AdaptiveMembershipOracle { + + @Override + default void processQueries(Collection> queries) { + for (AdaptiveQuery query : queries) { + processQuery(query); + } + } + + @Override + void processQuery(AdaptiveQuery query); +} diff --git a/api/src/main/java/de/learnlib/oracle/SymbolQueryOracle.java b/api/src/main/java/de/learnlib/oracle/SymbolQueryOracle.java deleted file mode 100644 index 27c41928fd..0000000000 --- a/api/src/main/java/de/learnlib/oracle/SymbolQueryOracle.java +++ /dev/null @@ -1,71 +0,0 @@ -/* Copyright (C) 2013-2024 TU Dortmund University - * This file is part of LearnLib, http://www.learnlib.de/. - * - * 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. - */ -package de.learnlib.oracle; - -import java.util.Collection; - -import de.learnlib.oracle.MembershipOracle.MealyMembershipOracle; -import de.learnlib.query.Query; -import net.automatalib.word.Word; -import net.automatalib.word.WordBuilder; - -/** - * Symbol query interface. Semantically similar to {@link MealyMembershipOracle}, but allows to pose queries - * symbol-wise. - * - * @param - * input alphabet type - * @param - * output alphabet type - */ -public interface SymbolQueryOracle extends MealyMembershipOracle { - - /** - * Query the system under learning for a new symbol. This is a stateful operation. - * - * @param i - * the symbol to query - * - * @return the observed output - */ - O query(I i); - - /** - * Reset the system under learning. - */ - void reset(); - - @Override - default void processQueries(Collection>> queries) { - - final WordBuilder wb = new WordBuilder<>(); - - for (Query> q : queries) { - reset(); - - for (I i : q.getPrefix()) { - query(i); - } - - for (I i : q.getSuffix()) { - wb.append(query(i)); - } - - q.answer(wb.toWord()); - wb.clear(); - } - } -} diff --git a/api/src/main/java/de/learnlib/query/AdaptiveQuery.java b/api/src/main/java/de/learnlib/query/AdaptiveQuery.java new file mode 100644 index 0000000000..e4dbe69c60 --- /dev/null +++ b/api/src/main/java/de/learnlib/query/AdaptiveQuery.java @@ -0,0 +1,74 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.query; + +/** + * An adaptive query is a variation of the (regular) {@link Query} that allows one to dynamically select the symbols to + * query based on responses to previous symbols. + *

+ * After {@link #getInput() fetching} the current input symbol that should be evaluated on the system under learning, + * its respective output must be {@link #processOutput(Object) processed} by the query object in order to determine the + * next action to make. This essentially establishes a symbol-wise dialogue between the query object and the system + * under learning and makes adaptive queries inherently stateful. + *

+ * For the semantics of this conversation, see the different {@link Response} values and their respective + * documentation. + * + * @param + * input symbol type + * @param + * output symbol type + */ +public interface AdaptiveQuery { + + /** + * Returns the current input symbol that should be evaluated on the system under learning. + * + * @return the current input symbol + */ + I getInput(); + + /** + * Processes the output of the system under learning to the latest returned input symbol. + * + * @param out + * the output of the system under learning + * + * @return the next action to make + */ + Response processOutput(O out); + + /** + * The different types of responses when processing outputs from the system under learning. + */ + enum Response { + /** + * Indicates that the query is finished and no more symbols should be processed. After returning this value, the + * behavior of {@link #getInput()} is undefined and may as well throw an exception. + */ + FINISHED, + /** + * Indicates that the system under learning should be reset to its initial state. After returning this value, + * {@link #getInput()} must return the next input symbol that should be queried after the reset. + */ + RESET, + /** + * Indicates that further symbols follow. After returning this value, {@link #getInput()} must return the next + * input symbol that should be queried. + */ + SYMBOL + } +} diff --git a/commons/util/src/main/java/de/learnlib/util/mealy/PresetAdaptiveQuery.java b/commons/util/src/main/java/de/learnlib/util/mealy/PresetAdaptiveQuery.java new file mode 100644 index 0000000000..8226da23b3 --- /dev/null +++ b/commons/util/src/main/java/de/learnlib/util/mealy/PresetAdaptiveQuery.java @@ -0,0 +1,79 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.util.mealy; + +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.query.AdaptiveQuery; +import de.learnlib.query.Query; +import net.automatalib.word.Word; +import net.automatalib.word.WordBuilder; + +/** + * Wraps a given (non-empty) {@link Query} as an {@link AdaptiveQuery} so that it can be answered by an + * {@link AdaptiveMembershipOracle}. + * + * @param + * input symbol type + * @param + * output symbol type + */ +public class PresetAdaptiveQuery implements AdaptiveQuery { + + private final WordBuilder builder; + private final Query> query; + + private final Word prefix; + private final Word suffix; + + private int prefixIdx; + private int suffixIdx; + + public PresetAdaptiveQuery(Query> query) { + this.builder = new WordBuilder<>(); + this.query = query; + this.prefix = query.getPrefix(); + this.suffix = query.getSuffix(); + this.prefixIdx = 0; + this.suffixIdx = 0; + } + + @Override + public I getInput() { + if (prefixIdx < prefix.size()) { + return prefix.getSymbol(prefixIdx); + } else { + return suffix.getSymbol(suffixIdx); + } + } + + @Override + public Response processOutput(O out) { + if (prefixIdx < prefix.size()) { + prefixIdx++; + } else { + suffixIdx++; + builder.add(out); + + if (suffixIdx >= suffix.size()) { + query.answer(builder.toWord()); + return Response.FINISHED; + } + } + + return Response.SYMBOL; + } +} + diff --git a/filters/cache/pom.xml b/filters/cache/pom.xml index 96fe6e53b4..e93156fc7f 100644 --- a/filters/cache/pom.xml +++ b/filters/cache/pom.xml @@ -103,6 +103,11 @@ limitations under the License. learnlib-test-support test + + de.learnlib + learnlib-util + test + org.testng @@ -120,6 +125,7 @@ limitations under the License. @{argLine} + --add-reads=de.learnlib.filter.cache=de.learnlib.common.util --add-reads=de.learnlib.filter.cache=de.learnlib.filter.statistic --add-reads=de.learnlib.filter.cache=de.learnlib.oracle.membership --add-reads=de.learnlib.filter.cache=de.learnlib.oracle.parallelism diff --git a/filters/cache/src/main/java/de/learnlib/filter/cache/mealy/AdaptiveQueryCache.java b/filters/cache/src/main/java/de/learnlib/filter/cache/mealy/AdaptiveQueryCache.java new file mode 100644 index 0000000000..e8165c266c --- /dev/null +++ b/filters/cache/src/main/java/de/learnlib/filter/cache/mealy/AdaptiveQueryCache.java @@ -0,0 +1,288 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.filter.cache.mealy; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Deque; +import java.util.List; +import java.util.Objects; + +import de.learnlib.Resumable; +import de.learnlib.filter.cache.LearningCache; +import de.learnlib.filter.cache.mealy.AdaptiveQueryCache.AdaptiveQueryCacheState; +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.oracle.BatchProcessor; +import de.learnlib.oracle.EquivalenceOracle; +import de.learnlib.query.AdaptiveQuery; +import de.learnlib.query.AdaptiveQuery.Response; +import de.learnlib.query.DefaultQuery; +import net.automatalib.alphabet.Alphabet; +import net.automatalib.alphabet.SupportsGrowingAlphabet; +import net.automatalib.automaton.impl.CompactTransition; +import net.automatalib.automaton.transducer.MealyMachine; +import net.automatalib.automaton.transducer.impl.CompactMealy; +import net.automatalib.util.automaton.equivalence.NearLinearEquivalenceTest; +import net.automatalib.word.Word; +import net.automatalib.word.WordBuilder; + +/** + * A cache for an {@link AdaptiveMembershipOracle}. Upon construction, it is provided with a delegate oracle. Queries + * that can be answered from the cache are answered directly, others are forwarded to the delegate oracle. Queried + * symbols that have to be delegated are incorporated into the cache directly. + *

+ * Internally, an incrementally growing tree (in form of a mealy automaton) is used for caching. + *

+ * Note that due to the step-wise processing of {@link AdaptiveQuery adaptive queries}, duplicates within a single + * {@link BatchProcessor#processBatch(Collection) batch} cannot be cached. If you want to maximize cache efficiency, you + * would have to give up on potential parallelization and pose queries one by one. + * + * @param + * input symbol type + * @param + * output symbol type + */ +public class AdaptiveQueryCache implements AdaptiveMembershipOracle, + LearningCache, I, Word>, + SupportsGrowingAlphabet, + Resumable> { + + private final AdaptiveMembershipOracle delegate; + private CompactMealy cache; + + public AdaptiveQueryCache(AdaptiveMembershipOracle delegate, Alphabet alphabet) { + this.delegate = delegate; + this.cache = new CompactMealy<>(alphabet); + this.cache.addInitialState(); + } + + @Override + public void processQueries(Collection> queries) { + + final Deque> queue = new ArrayDeque<>(queries); + final List unanswered = new ArrayList<>(queue.size()); + + while (!queue.isEmpty()) { + + // try to answer queries from cache + cacheLoop: + while (!queue.isEmpty()) { + final AdaptiveQuery query = queue.poll(); + final WordBuilder trace = new WordBuilder<>(); + Integer curr = this.cache.getInitialState(); + Response response; + + do { + final I input = query.getInput(); + final CompactTransition trans = this.cache.getTransition(curr, input); + + trace.add(input); + + if (trans == null) { + unanswered.add(new TrackingQuery(query, trace)); + continue cacheLoop; + } + + final O output = this.cache.getTransitionOutput(trans); + response = query.processOutput(output); + + if (response == Response.RESET) { + curr = this.cache.getInitialState(); + trace.clear(); + } else { + curr = this.cache.getSuccessor(trans); + } + } while (response != Response.FINISHED); + } + + // delegate non-answered queries + this.delegate.processQueries(unanswered); + + // feed back information into cache + for (TrackingQuery query : unanswered) { + final Word input = query.inputBuilder.toWord(); + final Word output = query.outputBuilder.toWord(); + + assert input.length() == output.length(); + + insert(input, output); + + if (!query.isFinished) { // re-queue reset successor + queue.add(query.delegate); + } + } + + unanswered.clear(); + } + } + + @Override + public EquivalenceOracle, I, Word> createCacheConsistencyTest() { + return (hypothesis, alphabet) -> { + //TODO: If the hypothesis has undefined transitions, but the cache doesn't, it is a clear counterexample! + final Word sepWord = NearLinearEquivalenceTest.findSeparatingWord(cache, hypothesis, alphabet, true); + + if (sepWord != null) { + return new DefaultQuery<>(sepWord, cache.computeOutput(sepWord)); + } + + return null; + }; + } + + @Override + public AdaptiveQueryCacheState suspend() { + return new AdaptiveQueryCacheState<>(cache); + } + + @Override + public void resume(AdaptiveQueryCacheState state) { + this.cache = state.getCache(); + } + + @Override + public void addAlphabetSymbol(I symbol) { + this.cache.addAlphabetSymbol(symbol); + } + + /** + * Returns a (structural) view of the cache in form of a {@link MealyMachine}. + * + * @return a view of the cache + */ + public MealyMachine getCache() { + return this.cache; + } + + /** + * Inserts the given trace of input symbols and associates the trace of given output symbols with it. + * + * @param input + * the sequence of input symbols + * @param output + * the sequence of output symbols + * + * @return the identifier of the state reached by the input sequence + */ + public Integer insert(Word input, Word output) { + return insert(this.cache.getInitialState(), input, output); + } + + /** + * Inserts the given trace of input symbols at the given cache state and associates the trace of given output + * symbols with it. + * + * @param state + * the (cache) state at which the traces should be inserted + * @param input + * the sequence of input symbols + * @param output + * the sequence of output symbols + * + * @return the identifier of the state reached by the input sequence + */ + public Integer insert(Integer state, Word input, Word output) { + assert input.length() == output.length(); + + Integer curr = state; + + for (int i = 0; i < input.size(); i++) { + final I in = input.getSymbol(i); + final O out = output.getSymbol(i); + final CompactTransition trans = this.cache.getTransition(curr, in); + + if (trans == null) { + Integer next = this.cache.addState(); + this.cache.addTransition(curr, in, next, out); + curr = next; + } else { + assert Objects.equals(out, this.cache.getTransitionOutput(trans)) : "Inconsistent observations"; + curr = this.cache.getSuccessor(trans); + } + } + + return curr; + } + + private class TrackingQuery implements AdaptiveQuery { + + private final AdaptiveQuery delegate; + private final WordBuilder inputBuilder; + private final WordBuilder outputBuilder; + + private final int prefixLength; + private int prefixIdx; + private boolean isFinished; + + TrackingQuery(AdaptiveQuery delegate, WordBuilder inputBuilder) { + this.delegate = delegate; + this.inputBuilder = inputBuilder; + this.outputBuilder = new WordBuilder<>(); + this.prefixLength = inputBuilder.size(); + this.prefixIdx = 0; + this.isFinished = false; + } + + @Override + public I getInput() { + // we are still processing the backlog + if (prefixIdx < prefixLength) { + return inputBuilder.getSymbol(prefixIdx); + } + + final I input = delegate.getInput(); + inputBuilder.append(input); + return input; + } + + @Override + public Response processOutput(O out) { + outputBuilder.append(out); + prefixIdx++; + + // in case of backlog, the last but one input hasn't been processed yet + if (prefixIdx < prefixLength) { + return Response.SYMBOL; + } + + final Response response = delegate.processOutput(out); + + switch (response) { + case FINISHED: + isFinished = true; + return Response.FINISHED; + case RESET: + return Response.FINISHED; + default: + return response; + } + } + } + + public static class AdaptiveQueryCacheState { + + private final CompactMealy cache; + + AdaptiveQueryCacheState(CompactMealy cache) { + this.cache = cache; + } + + CompactMealy getCache() { + return cache; + } + } +} diff --git a/filters/cache/src/main/java/de/learnlib/filter/cache/mealy/MealyCaches.java b/filters/cache/src/main/java/de/learnlib/filter/cache/mealy/MealyCaches.java index 316e0f9e10..6db01c9555 100644 --- a/filters/cache/src/main/java/de/learnlib/filter/cache/mealy/MealyCaches.java +++ b/filters/cache/src/main/java/de/learnlib/filter/cache/mealy/MealyCaches.java @@ -15,8 +15,8 @@ */ package de.learnlib.filter.cache.mealy; +import de.learnlib.oracle.AdaptiveMembershipOracle; import de.learnlib.oracle.MembershipOracle; -import de.learnlib.oracle.SymbolQueryOracle; import net.automatalib.alphabet.Alphabet; import net.automatalib.common.util.mapping.Mapping; import net.automatalib.incremental.mealy.dag.IncrementalMealyDAGBuilder; @@ -195,7 +195,7 @@ public static MealyCacheOracle createDynamicTreeCache(Mapping MealyCacheOracle createDynamicTreeCache(Mapping SymbolQueryCache createSymbolQueryCache(Alphabet alphabet, - SymbolQueryOracle mqOracle) { - return new SymbolQueryCache<>(mqOracle, alphabet); + public static AdaptiveQueryCache createAdaptiveQueryCache(Alphabet alphabet, + AdaptiveMembershipOracle mqOracle) { + return new AdaptiveQueryCache<>(mqOracle, alphabet); } } diff --git a/filters/cache/src/main/java/de/learnlib/filter/cache/mealy/SymbolQueryCache.java b/filters/cache/src/main/java/de/learnlib/filter/cache/mealy/SymbolQueryCache.java deleted file mode 100644 index 66808f99f3..0000000000 --- a/filters/cache/src/main/java/de/learnlib/filter/cache/mealy/SymbolQueryCache.java +++ /dev/null @@ -1,163 +0,0 @@ -/* Copyright (C) 2013-2024 TU Dortmund University - * This file is part of LearnLib, http://www.learnlib.de/. - * - * 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. - */ -package de.learnlib.filter.cache.mealy; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Objects; - -import de.learnlib.Resumable; -import de.learnlib.filter.cache.LearningCacheOracle.MealyLearningCacheOracle; -import de.learnlib.filter.cache.mealy.SymbolQueryCache.SymbolQueryCacheState; -import de.learnlib.oracle.EquivalenceOracle; -import de.learnlib.oracle.SymbolQueryOracle; -import de.learnlib.query.DefaultQuery; -import net.automatalib.alphabet.Alphabet; -import net.automatalib.alphabet.SupportsGrowingAlphabet; -import net.automatalib.automaton.transducer.MealyMachine; -import net.automatalib.automaton.transducer.impl.CompactMealy; -import net.automatalib.util.automaton.equivalence.NearLinearEquivalenceTest; -import net.automatalib.word.Word; -import org.checkerframework.checker.nullness.qual.Nullable; - -/** - * A cache for a {@link SymbolQueryOracle}. Upon construction, it is provided with a delegate oracle. Queries that can - * be answered from the cache are answered directly, others are forwarded to the delegate oracle. Queried symbols that - * have to be delegated are incorporated into the cache directly. - *

- * Internally, an incrementally growing tree (in form of a mealy automaton) is used for caching. - * - * @param - * input alphabet type - * @param - * output alphabet type - */ -public class SymbolQueryCache - implements SymbolQueryOracle, MealyLearningCacheOracle, SupportsGrowingAlphabet, Resumable> { - - private CompactMealy cache; - private final SymbolQueryOracle delegate; - - private final List currentTrace; - private Integer currentState; - private boolean currentTraceValid; - - SymbolQueryCache(SymbolQueryOracle delegate, Alphabet alphabet) { - this.delegate = delegate; - this.cache = new CompactMealy<>(alphabet); - this.currentState = this.cache.addInitialState(); - - this.currentTrace = new ArrayList<>(); - this.currentTraceValid = false; - } - - @Override - public O query(I i) { - - if (this.currentTraceValid) { - final Integer succ = this.cache.getSuccessor(this.currentState, i); - - if (succ != null) { - final O output = this.cache.getOutput(this.currentState, i); - assert output != null; - this.currentTrace.add(i); - this.currentState = succ; - return output; - } else { - this.currentTraceValid = false; - this.delegate.reset(); - - this.currentTrace.forEach(this.delegate::query); - } - } - - final O output = this.delegate.query(i); - - final Integer nextState; - final Integer succ = this.cache.getSuccessor(this.currentState, i); - - if (succ == null) { - final Integer newState = this.cache.addState(); - this.cache.addTransition(this.currentState, i, newState, output); - nextState = newState; - } else { - assert Objects.equals(this.cache.getOutput(this.currentState, i), output); - nextState = succ; - } - - this.currentState = nextState; - - return output; - } - - @Override - public void reset() { - Integer init = this.cache.getInitialState(); - assert init != null; - this.currentState = init; - this.currentTrace.clear(); - this.currentTraceValid = true; - } - - @Override - public EquivalenceOracle, I, Word> createCacheConsistencyTest() { - return this::findCounterexample; - } - - private @Nullable DefaultQuery> findCounterexample(MealyMachine hypothesis, - Collection alphabet) { - /* - TODO: potential optimization: If the hypothesis has undefined transitions, but the cache doesn't, it is a clear - counterexample! - */ - final Word sepWord = NearLinearEquivalenceTest.findSeparatingWord(cache, hypothesis, alphabet, true); - - if (sepWord != null) { - return new DefaultQuery<>(sepWord, cache.computeOutput(sepWord)); - } - - return null; - } - - @Override - public SymbolQueryCacheState suspend() { - return new SymbolQueryCacheState<>(cache); - } - - @Override - public void resume(SymbolQueryCacheState state) { - this.cache = state.getCache(); - } - - @Override - public void addAlphabetSymbol(I symbol) { - this.cache.addAlphabetSymbol(symbol); - } - - public static class SymbolQueryCacheState { - - private final CompactMealy cache; - - SymbolQueryCacheState(CompactMealy cache) { - this.cache = cache; - } - - CompactMealy getCache() { - return cache; - } - } -} diff --git a/filters/cache/src/test/java/de/learnlib/filter/cache/mealy/AdaptiveQueryCacheTest.java b/filters/cache/src/test/java/de/learnlib/filter/cache/mealy/AdaptiveQueryCacheTest.java new file mode 100644 index 0000000000..039c7d109f --- /dev/null +++ b/filters/cache/src/test/java/de/learnlib/filter/cache/mealy/AdaptiveQueryCacheTest.java @@ -0,0 +1,133 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.filter.cache.mealy; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import de.learnlib.driver.simulator.MealySimulatorSUL; +import de.learnlib.filter.cache.AbstractCacheTest; +import de.learnlib.filter.cache.CacheTestUtils; +import de.learnlib.filter.cache.LearningCacheOracle.MealyLearningCacheOracle; +import de.learnlib.filter.cache.mealy.AdaptiveQueryCacheTest.Wrapper; +import de.learnlib.filter.statistic.oracle.CounterAdaptiveQueryOracle; +import de.learnlib.oracle.EquivalenceOracle; +import de.learnlib.oracle.membership.SULAdaptiveOracle; +import de.learnlib.query.AdaptiveQuery; +import de.learnlib.query.Query; +import de.learnlib.util.mealy.PresetAdaptiveQuery; +import net.automatalib.alphabet.Alphabet; +import net.automatalib.alphabet.SupportsGrowingAlphabet; +import net.automatalib.alphabet.impl.GrowingMapAlphabet; +import net.automatalib.automaton.transducer.MealyMachine; +import net.automatalib.word.Word; +import org.testng.annotations.Test; + +public class AdaptiveQueryCacheTest + extends AbstractCacheTest, MealyMachine, Character, Word> { + + private final CounterAdaptiveQueryOracle counter; + + public AdaptiveQueryCacheTest() { + counter = + new CounterAdaptiveQueryOracle<>(new SULAdaptiveOracle<>(new MealySimulatorSUL<>(CacheTestUtils.MEALY))); + } + + @Test(enabled = false) + @Override + public void testDuplicatesInBatch() { + // adaptive queries don't support duplicate detection in batches + } + + @Override + protected Alphabet getAlphabet() { + return new GrowingMapAlphabet<>(CacheTestUtils.INPUT_ALPHABET); + } + + @Override + protected MealyMachine getTargetModel() { + return CacheTestUtils.MEALY; + } + + @Override + protected MealyMachine getInvalidTargetModel() { + return CacheTestUtils.MEALY_INVALID; + } + + @Override + protected Wrapper getCachedOracle() { + return new Wrapper<>(MealyCaches.createAdaptiveQueryCache(getAlphabet(), counter)); + } + + @Override + protected Wrapper getResumedOracle(Wrapper original) { + final AdaptiveQueryCache fresh = + MealyCaches.createAdaptiveQueryCache(getAlphabet(), counter); + serializeResumable(original.delegate, fresh); + return new Wrapper<>(fresh); + } + + @Override + protected long getNumberOfPosedQueries() { + return counter.getResetCount(); + } + + @Override + protected boolean supportsPrefixes() { + return true; + } + + @Override + protected Alphabet getExtensionAlphabet() { + return CacheTestUtils.EXTENSION_ALPHABET; + } + + @Override + protected boolean supportsGrowing() { + return true; + } + + protected static final class Wrapper implements MealyLearningCacheOracle, SupportsGrowingAlphabet { + + private final AdaptiveQueryCache delegate; + + private Wrapper(AdaptiveQueryCache delegate) { + this.delegate = delegate; + } + + @Override + public EquivalenceOracle, I, Word> createCacheConsistencyTest() { + return delegate.createCacheConsistencyTest(); + } + + @Override + public void processQueries(Collection>> queries) { + final List> mapped = new ArrayList<>(queries.size()); + + for (Query> q : queries) { + mapped.add(new PresetAdaptiveQuery<>(q)); + } + + this.delegate.processQueries(mapped); + } + + @Override + public void addAlphabetSymbol(I i) { + this.delegate.addAlphabetSymbol(i); + } + } +} diff --git a/filters/cache/src/test/java/de/learnlib/filter/cache/mealy/SymbolQueryCacheTest.java b/filters/cache/src/test/java/de/learnlib/filter/cache/mealy/SymbolQueryCacheTest.java deleted file mode 100644 index e80b1fa6b9..0000000000 --- a/filters/cache/src/test/java/de/learnlib/filter/cache/mealy/SymbolQueryCacheTest.java +++ /dev/null @@ -1,84 +0,0 @@ -/* Copyright (C) 2013-2024 TU Dortmund University - * This file is part of LearnLib, http://www.learnlib.de/. - * - * 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. - */ -package de.learnlib.filter.cache.mealy; - -import de.learnlib.driver.simulator.MealySimulatorSUL; -import de.learnlib.filter.cache.AbstractCacheTest; -import de.learnlib.filter.cache.CacheTestUtils; -import de.learnlib.filter.statistic.oracle.CounterSymbolQueryOracle; -import de.learnlib.oracle.membership.SULSymbolQueryOracle; -import net.automatalib.alphabet.Alphabet; -import net.automatalib.alphabet.impl.GrowingMapAlphabet; -import net.automatalib.automaton.transducer.MealyMachine; -import net.automatalib.word.Word; - -public class SymbolQueryCacheTest - extends AbstractCacheTest, MealyMachine, Character, Word> { - - private final CounterSymbolQueryOracle counter; - - public SymbolQueryCacheTest() { - counter = - new CounterSymbolQueryOracle<>(new SULSymbolQueryOracle<>(new MealySimulatorSUL<>(CacheTestUtils.MEALY))); - } - - @Override - protected Alphabet getAlphabet() { - return new GrowingMapAlphabet<>(CacheTestUtils.INPUT_ALPHABET); - } - - @Override - protected MealyMachine getTargetModel() { - return CacheTestUtils.MEALY; - } - - @Override - protected MealyMachine getInvalidTargetModel() { - return CacheTestUtils.MEALY_INVALID; - } - - @Override - protected SymbolQueryCache getCachedOracle() { - return MealyCaches.createSymbolQueryCache(getAlphabet(), counter); - } - - @Override - protected SymbolQueryCache getResumedOracle(SymbolQueryCache original) { - final SymbolQueryCache fresh = MealyCaches.createSymbolQueryCache(getAlphabet(), counter); - serializeResumable(original, fresh); - return fresh; - } - - @Override - protected long getNumberOfPosedQueries() { - return counter.getResetCount(); - } - - @Override - protected boolean supportsPrefixes() { - return true; - } - - @Override - protected Alphabet getExtensionAlphabet() { - return CacheTestUtils.EXTENSION_ALPHABET; - } - - @Override - protected boolean supportsGrowing() { - return true; - } -} diff --git a/filters/statistics/src/main/java/de/learnlib/filter/statistic/oracle/CounterAdaptiveQueryOracle.java b/filters/statistics/src/main/java/de/learnlib/filter/statistic/oracle/CounterAdaptiveQueryOracle.java new file mode 100644 index 0000000000..10ce15194e --- /dev/null +++ b/filters/statistics/src/main/java/de/learnlib/filter/statistic/oracle/CounterAdaptiveQueryOracle.java @@ -0,0 +1,91 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.filter.statistic.oracle; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.query.AdaptiveQuery; +import de.learnlib.query.AdaptiveQuery.Response; + +/** + * A simple wrapper for counting the number of {@link Response#RESET resets} and {@link Response#SYMBOL symbols} of an + * {@link AdaptiveMembershipOracle}. + * + * @param + * input symbol type + * @param + * output symbol type + */ +public class CounterAdaptiveQueryOracle implements AdaptiveMembershipOracle { + + private final AdaptiveMembershipOracle delegate; + private final AtomicLong resetCounter = new AtomicLong(); + private final AtomicLong symbolCounter = new AtomicLong(); + + public CounterAdaptiveQueryOracle(AdaptiveMembershipOracle delegate) { + this.delegate = delegate; + } + + public long getResetCount() { + return resetCounter.get(); + } + + public long getSymbolCount() { + return symbolCounter.get(); + } + + @Override + public void processQueries(Collection> queries) { + final List wrappers = new ArrayList<>(queries.size()); + for (AdaptiveQuery q : queries) { + wrappers.add(new CountingQuery(q)); + } + + this.delegate.processQueries(wrappers); + + } + + private class CountingQuery implements AdaptiveQuery { + + private final AdaptiveQuery delegate; + + CountingQuery(AdaptiveQuery delegate) { + this.delegate = delegate; + } + + @Override + public I getInput() { + return delegate.getInput(); + } + + @Override + public Response processOutput(O out) { + symbolCounter.incrementAndGet(); + + final Response response = delegate.processOutput(out); + + if (response != Response.SYMBOL) { + resetCounter.incrementAndGet(); + } + + return response; + } + } +} diff --git a/filters/statistics/src/main/java/de/learnlib/filter/statistic/oracle/CounterSymbolQueryOracle.java b/filters/statistics/src/main/java/de/learnlib/filter/statistic/oracle/CounterSymbolQueryOracle.java deleted file mode 100644 index 6fdaabe691..0000000000 --- a/filters/statistics/src/main/java/de/learnlib/filter/statistic/oracle/CounterSymbolQueryOracle.java +++ /dev/null @@ -1,60 +0,0 @@ -/* Copyright (C) 2013-2024 TU Dortmund University - * This file is part of LearnLib, http://www.learnlib.de/. - * - * 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. - */ -package de.learnlib.filter.statistic.oracle; - -import java.util.concurrent.atomic.AtomicLong; - -import de.learnlib.oracle.SymbolQueryOracle; - -/** - * A simple wrapper for counting the number of {@link SymbolQueryOracle#reset() resets} and - * {@link SymbolQueryOracle#query(Object) symbol queries} of a {@link SymbolQueryOracle}. - * - * @param - * input symbol type - * @param - * output symbol type - */ -public class CounterSymbolQueryOracle implements SymbolQueryOracle { - - private final SymbolQueryOracle delegate; - private final AtomicLong resetCounter = new AtomicLong(); - private final AtomicLong symbolCounter = new AtomicLong(); - - public CounterSymbolQueryOracle(SymbolQueryOracle delegate) { - this.delegate = delegate; - } - - @Override - public O query(I i) { - symbolCounter.incrementAndGet(); - return delegate.query(i); - } - - @Override - public void reset() { - resetCounter.incrementAndGet(); - delegate.reset(); - } - - public long getResetCount() { - return resetCounter.get(); - } - - public long getSymbolCount() { - return symbolCounter.get(); - } -} diff --git a/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/SULAdaptiveOracle.java b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/SULAdaptiveOracle.java new file mode 100644 index 0000000000..e4e636c149 --- /dev/null +++ b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/SULAdaptiveOracle.java @@ -0,0 +1,63 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.oracle.membership; + +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.oracle.SingleAdaptiveMembershipOracle; +import de.learnlib.query.AdaptiveQuery; +import de.learnlib.query.AdaptiveQuery.Response; +import de.learnlib.sul.SUL; + +/** + * A wrapper that allows to use a {@link SUL} where a {@link AdaptiveMembershipOracle} is expected. + *

+ * This oracle is not thread-safe. + * + * @param + * input alphabet type + * @param + * output alphabet type + */ +public class SULAdaptiveOracle implements SingleAdaptiveMembershipOracle { + + private final SUL sul; + + public SULAdaptiveOracle(SUL sul) { + this.sul = sul; + } + + @Override + public void processQuery(AdaptiveQuery query) { + sul.pre(); + + Response response; + + do { + final I in = query.getInput(); + final O out = sul.step(in); + + response = query.processOutput(out); + + if (response == Response.RESET) { + sul.post(); + sul.pre(); + } + } while (response != Response.FINISHED); + + sul.post(); + } +} + diff --git a/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/SULSymbolQueryOracle.java b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/SULSymbolQueryOracle.java deleted file mode 100644 index 670633b49a..0000000000 --- a/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/SULSymbolQueryOracle.java +++ /dev/null @@ -1,72 +0,0 @@ -/* Copyright (C) 2013-2024 TU Dortmund University - * This file is part of LearnLib, http://www.learnlib.de/. - * - * 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. - */ -package de.learnlib.oracle.membership; - -import de.learnlib.oracle.SymbolQueryOracle; -import de.learnlib.sul.SUL; - -/** - * A wrapper that allows to use a {@link SUL} where a {@link SymbolQueryOracle} is expected. - *

- * Implementation note: The contract of {@link SymbolQueryOracle} does not make any assumptions about when its - * {@link SymbolQueryOracle#reset() reset} method is called. However, from a {@link SUL} perspective it is desirable to - * call its {@link SUL#post() post} method once querying is done. Therefore, multiple calls to {@code this.}{@link - * SULSymbolQueryOracle#reset()} will {@link SUL#post() close} the underlying {@link SUL} only once, so that the {@link - * SUL} can be shutdown by {@code this} oracle from outside, after the learning process has finished. - *

- * This oracle is not thread-safe. - * - * @param - * input alphabet type - * @param - * output alphabet type - */ -public class SULSymbolQueryOracle implements SymbolQueryOracle { - - private final SUL sul; - - private boolean preRequired; - private boolean postRequired; - - public SULSymbolQueryOracle(SUL sul) { - this.sul = sul; - this.preRequired = true; - } - - @Override - public O query(I i) { - if (preRequired) { - this.sul.pre(); - this.preRequired = false; - this.postRequired = true; - } - - return queryInternal(i); - } - - @Override - public void reset() { - if (postRequired) { - this.sul.post(); - this.postRequired = false; - } - this.preRequired = true; - } - - protected O queryInternal(I i) { - return this.sul.step(i); - } -} diff --git a/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/StateLocalInputSULAdaptiveOracle.java b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/StateLocalInputSULAdaptiveOracle.java new file mode 100644 index 0000000000..d4b6591a62 --- /dev/null +++ b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/StateLocalInputSULAdaptiveOracle.java @@ -0,0 +1,85 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.oracle.membership; + +import java.util.Collection; +import java.util.Collections; + +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.query.AdaptiveQuery; +import de.learnlib.query.AdaptiveQuery.Response; +import de.learnlib.sul.StateLocalInputSUL; + +/** + * A {@link AdaptiveMembershipOracle} wrapper for {@link StateLocalInputSUL}s. + *

+ * This oracle is not thread-safe. + * + * @param + * input symbol type + * @param + * output symbol type + * + * @see SULAdaptiveOracle + */ +public class StateLocalInputSULAdaptiveOracle extends SULAdaptiveOracle + implements AdaptiveMembershipOracle { + + private final StateLocalInputSUL sul; + private final O undefinedOutput; + + private boolean fetchRequired; + + public StateLocalInputSULAdaptiveOracle(StateLocalInputSUL sul, O undefinedOutput) { + super(sul); + this.sul = sul; + this.undefinedOutput = undefinedOutput; + this.fetchRequired = true; + } + + @Override + public void processQuery(AdaptiveQuery query) { + + sul.pre(); + + Response response; + + do { + final Collection inputs = this.fetchRequired ? sul.currentlyEnabledInputs() : Collections.emptyList(); + final I in = query.getInput(); + + final O out; + + if (inputs.contains(in)) { + out = sul.step(in); + } else { + this.fetchRequired = false; + out = undefinedOutput; + } + + response = query.processOutput(out); + + if (response == Response.RESET) { + fetchRequired = true; + sul.post(); + sul.pre(); + } + } while (response != Response.FINISHED); + + fetchRequired = true; + sul.post(); + } +} diff --git a/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/StateLocalInputSULSymbolQueryOracle.java b/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/StateLocalInputSULSymbolQueryOracle.java deleted file mode 100644 index a75b514467..0000000000 --- a/oracles/membership-oracles/src/main/java/de/learnlib/oracle/membership/StateLocalInputSULSymbolQueryOracle.java +++ /dev/null @@ -1,69 +0,0 @@ -/* Copyright (C) 2013-2024 TU Dortmund University - * This file is part of LearnLib, http://www.learnlib.de/. - * - * 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. - */ - -package de.learnlib.oracle.membership; - -import java.util.Collection; -import java.util.Collections; - -import de.learnlib.oracle.SymbolQueryOracle; -import de.learnlib.sul.StateLocalInputSUL; - -/** - * A {@link SymbolQueryOracle} wrapper for {@link StateLocalInputSUL}s. See {@link SULSymbolQueryOracle}. - *

- * This oracle is not thread-safe. - * - * @param - * input symbol type - * @param - * output symbol type - * - * @see SULSymbolQueryOracle - */ -public class StateLocalInputSULSymbolQueryOracle extends SULSymbolQueryOracle - implements SymbolQueryOracle { - - private final StateLocalInputSUL sul; - private final O undefinedOutput; - - private boolean fetchRequired; - - public StateLocalInputSULSymbolQueryOracle(StateLocalInputSUL sul, O undefinedOutput) { - super(sul); - this.sul = sul; - this.undefinedOutput = undefinedOutput; - this.fetchRequired = true; - } - - @Override - public void reset() { - super.reset(); - this.fetchRequired = true; - } - - @Override - protected O queryInternal(I i) { - final Collection enabledInputs = this.fetchRequired ? sul.currentlyEnabledInputs() : Collections.emptyList(); - - if (enabledInputs.contains(i)) { - return sul.step(i); - } else { - this.fetchRequired = false; - return undefinedOutput; - } - } -} diff --git a/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/AdaptiveTestQuery.java b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/AdaptiveTestQuery.java new file mode 100644 index 0000000000..dff5279628 --- /dev/null +++ b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/AdaptiveTestQuery.java @@ -0,0 +1,74 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.oracle.membership; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; + +import de.learnlib.query.AdaptiveQuery; +import net.automatalib.word.Word; +import net.automatalib.word.WordBuilder; + +public class AdaptiveTestQuery implements AdaptiveQuery { + + private final Deque> inputs; + private final List> outputs; + + @SafeVarargs + public AdaptiveTestQuery(Word... inputs) { + this.inputs = new ArrayDeque<>(inputs.length); + this.outputs = new ArrayList<>(inputs.length); + + for (Word input : inputs) { + this.inputs.add(new ArrayDeque<>(input.asList())); + } + + this.outputs.add(new WordBuilder<>()); + } + + @Override + public I getInput() { + return inputs.peek().peek(); + } + + @Override + public Response processOutput(O out) { + final Deque input = this.inputs.peekFirst(); + final WordBuilder output = this.outputs.get(this.outputs.size() - 1); + + input.removeFirst(); + output.add(out); + + if (input.isEmpty()) { + this.inputs.removeFirst(); + + if (this.inputs.isEmpty()) { + return Response.FINISHED; + } else { + this.outputs.add(new WordBuilder<>(this.inputs.peekFirst().size())); + return Response.RESET; + } + } + + return Response.SYMBOL; + } + + public List> getOutputs() { + return outputs; + } +} diff --git a/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/SULSymbolQueryOracleTest.java b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/SULAdaptiveOracleTest.java similarity index 72% rename from oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/SULSymbolQueryOracleTest.java rename to oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/SULAdaptiveOracleTest.java index acaeb39343..014c8ad0f5 100644 --- a/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/SULSymbolQueryOracleTest.java +++ b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/SULAdaptiveOracleTest.java @@ -15,6 +15,8 @@ */ package de.learnlib.oracle.membership; +import java.util.Collections; +import java.util.List; import java.util.Random; import de.learnlib.driver.simulator.MealySimulatorSUL; @@ -23,12 +25,13 @@ import net.automatalib.alphabet.Alphabet; import net.automatalib.alphabet.impl.Alphabets; import net.automatalib.word.Word; +import net.automatalib.word.WordBuilder; import org.mockito.Mockito; import org.testng.Assert; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; -public class SULSymbolQueryOracleTest { +public class SULAdaptiveOracleTest { private ExampleRandomMealy example; private SUL sul; @@ -42,45 +45,45 @@ public void setUp() { } @Test - public void testResetIdempotency() { + public void testNoopIdempotency() { final SUL mock = Mockito.spy(sul); - final SULSymbolQueryOracle oracle = new SULSymbolQueryOracle<>(mock); + final SULAdaptiveOracle oracle = new SULAdaptiveOracle<>(mock); Mockito.verify(mock, Mockito.times(0)).pre(); Mockito.verify(mock, Mockito.times(0)).post(); - oracle.reset(); - oracle.reset(); - oracle.reset(); + oracle.processQueries(Collections.emptyList()); + oracle.processQueries(Collections.emptyList()); + oracle.processQueries(Collections.emptyList()); Mockito.verify(mock, Mockito.times(0)).pre(); Mockito.verify(mock, Mockito.times(0)).post(); } @Test - public void testQueriesAndCleanUp() { + public void testQueries() { final SUL mock = Mockito.spy(sul); - final SULSymbolQueryOracle oracle = new SULSymbolQueryOracle<>(mock); + final SULAdaptiveOracle oracle = new SULAdaptiveOracle<>(mock); Mockito.verify(mock, Mockito.times(0)).pre(); Mockito.verify(mock, Mockito.times(0)).post(); + Mockito.verify(mock, Mockito.times(0)).step(Mockito.anyChar()); final Word i1 = Word.fromString("abcabcabc"); - final Word o1 = oracle.answerQuery(i1); - oracle.reset(); // cleanup + final Word i2 = Word.fromString("cba"); + final AdaptiveTestQuery query = new AdaptiveTestQuery<>(i1, i2); - Assert.assertEquals(o1, example.getReferenceAutomaton().computeOutput(i1)); - Mockito.verify(mock, Mockito.times(1)).pre(); - Mockito.verify(mock, Mockito.times(1)).post(); - Mockito.verify(mock, Mockito.times(i1.size())).step(Mockito.anyChar()); + oracle.processQuery(query); - final Word i2 = Word.fromString("cba"); - final Word o2 = oracle.answerQuery(i2); - oracle.reset(); // cleanup - oracle.reset(); // twice + final List> outputs = query.getOutputs(); + Assert.assertEquals(outputs.size(), 2); + final Word o1 = outputs.get(0).toWord(); + final Word o2 = outputs.get(1).toWord(); + + Assert.assertEquals(o1, example.getReferenceAutomaton().computeOutput(i1)); Assert.assertEquals(o2, example.getReferenceAutomaton().computeOutput(i2)); Mockito.verify(mock, Mockito.times(2)).pre(); Mockito.verify(mock, Mockito.times(2)).post(); diff --git a/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/StateLocalInputSULSymbolQueryOracleTest.java b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/StateLocalInputSULAdaptiveOracleTest.java similarity index 74% rename from oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/StateLocalInputSULSymbolQueryOracleTest.java rename to oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/StateLocalInputSULAdaptiveOracleTest.java index c3b999096a..367e71a6a8 100644 --- a/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/StateLocalInputSULSymbolQueryOracleTest.java +++ b/oracles/membership-oracles/src/test/java/de/learnlib/oracle/membership/StateLocalInputSULAdaptiveOracleTest.java @@ -16,6 +16,7 @@ package de.learnlib.oracle.membership; import java.util.Collections; +import java.util.List; import java.util.Random; import de.learnlib.driver.simulator.StateLocalInputMealySimulatorSUL; @@ -24,12 +25,13 @@ import net.automatalib.alphabet.Alphabet; import net.automatalib.alphabet.impl.Alphabets; import net.automatalib.word.Word; +import net.automatalib.word.WordBuilder; import org.mockito.Mockito; import org.testng.Assert; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; -public class StateLocalInputSULSymbolQueryOracleTest { +public class StateLocalInputSULAdaptiveOracleTest { private ExampleRandomStateLocalInputMealy example; private StateLocalInputSUL sul; @@ -47,23 +49,25 @@ public void setUp() { } @Test - public void testResetIdempotency() { + public void testNoopIdempotency() { final StateLocalInputSUL mock = Mockito.spy(sul); Mockito.doAnswer(invocation -> Collections.singleton('a')).when(mock).currentlyEnabledInputs(); - final SULSymbolQueryOracle oracle = - new StateLocalInputSULSymbolQueryOracle<>(mock, example.getUndefinedOutput()); + final SULAdaptiveOracle oracle = + new StateLocalInputSULAdaptiveOracle<>(mock, example.getUndefinedOutput()); Mockito.verify(mock, Mockito.times(0)).pre(); Mockito.verify(mock, Mockito.times(0)).post(); + Mockito.verify(mock, Mockito.times(0)).step(Mockito.anyChar()); Mockito.verify(mock, Mockito.times(0)).currentlyEnabledInputs(); - oracle.reset(); - oracle.reset(); - oracle.reset(); + oracle.processQueries(Collections.emptyList()); + oracle.processQueries(Collections.emptyList()); + oracle.processQueries(Collections.emptyList()); Mockito.verify(mock, Mockito.times(0)).pre(); Mockito.verify(mock, Mockito.times(0)).post(); + Mockito.verify(mock, Mockito.times(0)).step(Mockito.anyChar()); Mockito.verify(mock, Mockito.times(0)).currentlyEnabledInputs(); } @@ -72,33 +76,34 @@ public void testQueriesAndCleanUp() { final StateLocalInputSUL mock = Mockito.spy(sul); Mockito.doAnswer(invocation -> Collections.singleton('a')).when(mock).currentlyEnabledInputs(); - final SULSymbolQueryOracle oracle = - new StateLocalInputSULSymbolQueryOracle<>(mock, example.getUndefinedOutput()); + final SULAdaptiveOracle oracle = + new StateLocalInputSULAdaptiveOracle<>(mock, example.getUndefinedOutput()); Mockito.verify(mock, Mockito.times(0)).pre(); Mockito.verify(mock, Mockito.times(0)).post(); + Mockito.verify(mock, Mockito.times(0)).step(Mockito.anyChar()); + Mockito.verify(mock, Mockito.times(0)).currentlyEnabledInputs(); final Word i1 = Word.fromString("abcabcabc"); - final Word o1 = oracle.answerQuery(i1); - oracle.reset(); // cleanup + final Word i2 = Word.fromString("aaaaa"); + final AdaptiveTestQuery query = new AdaptiveTestQuery<>(i1, i2); + + oracle.processQuery(query); + + final List> outputs = query.getOutputs(); + Assert.assertEquals(outputs.size(), 2); + + final Word o1 = outputs.get(0).toWord(); + final Word o2 = outputs.get(1).toWord(); Assert.assertEquals(o1.firstSymbol(), example.getReferenceAutomaton().computeOutput(i1).firstSymbol()); Assert.assertEquals(o1.subWord(1), Word.fromList(Collections.nCopies(i1.size() - 1, example.getUndefinedOutput()))); - Mockito.verify(mock, Mockito.times(1)).pre(); - Mockito.verify(mock, Mockito.times(1)).post(); - Mockito.verify(mock, Mockito.times(2)).currentlyEnabledInputs(); - Mockito.verify(mock, Mockito.times(1)).step(Mockito.anyChar()); - - final Word i2 = Word.fromString("aaaaa"); - final Word o2 = oracle.answerQuery(i2); - oracle.reset(); // cleanup - oracle.reset(); // twice - Assert.assertEquals(o2, example.getReferenceAutomaton().computeOutput(i2)); + Mockito.verify(mock, Mockito.times(2)).pre(); Mockito.verify(mock, Mockito.times(2)).post(); - Mockito.verify(mock, Mockito.times(2 + i2.size())).currentlyEnabledInputs(); Mockito.verify(mock, Mockito.times(1 + i2.size())).step(Mockito.anyChar()); + Mockito.verify(mock, Mockito.times(2 + i2.size())).currentlyEnabledInputs(); } } diff --git a/oracles/parallelism/src/main/java/de/learnlib/oracle/parallelism/DynamicParallelAdaptiveOracle.java b/oracles/parallelism/src/main/java/de/learnlib/oracle/parallelism/DynamicParallelAdaptiveOracle.java new file mode 100644 index 0000000000..92b5c0585a --- /dev/null +++ b/oracles/parallelism/src/main/java/de/learnlib/oracle/parallelism/DynamicParallelAdaptiveOracle.java @@ -0,0 +1,50 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.oracle.parallelism; + +import java.util.Collection; +import java.util.concurrent.ExecutorService; +import java.util.function.Supplier; + +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.oracle.ParallelAdaptiveOracle; +import de.learnlib.query.AdaptiveQuery; +import org.checkerframework.checker.index.qual.NonNegative; + +/** + * A specialized {@link AbstractDynamicBatchProcessor} for {@link AdaptiveMembershipOracle}s that implements + * {@link ParallelAdaptiveOracle}. + * + * @param + * input symbol type + * @param + * output symbol type + */ +public class DynamicParallelAdaptiveOracle + extends AbstractDynamicBatchProcessor, AdaptiveMembershipOracle> + implements ParallelAdaptiveOracle { + + public DynamicParallelAdaptiveOracle(Supplier> oracleSupplier, + @NonNegative int batchSize, + ExecutorService executor) { + super(oracleSupplier, batchSize, executor); + } + + @Override + public void processQueries(Collection> queries) { + processBatch(queries); + } +} diff --git a/oracles/parallelism/src/main/java/de/learnlib/oracle/parallelism/DynamicParallelAdaptiveOracleBuilder.java b/oracles/parallelism/src/main/java/de/learnlib/oracle/parallelism/DynamicParallelAdaptiveOracleBuilder.java new file mode 100644 index 0000000000..2b1d6472b0 --- /dev/null +++ b/oracles/parallelism/src/main/java/de/learnlib/oracle/parallelism/DynamicParallelAdaptiveOracleBuilder.java @@ -0,0 +1,50 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.oracle.parallelism; + +import java.util.Collection; +import java.util.concurrent.ExecutorService; +import java.util.function.Supplier; + +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.query.AdaptiveQuery; + +/** + * A specialized {@link AbstractDynamicBatchProcessorBuilder} for {@link AdaptiveMembershipOracle}s. + * + * @param + * input symbol type + * @param + * output symbol type + */ +public class DynamicParallelAdaptiveOracleBuilder + extends AbstractDynamicBatchProcessorBuilder, AdaptiveMembershipOracle, DynamicParallelAdaptiveOracle> { + + public DynamicParallelAdaptiveOracleBuilder(Supplier> oracleSupplier) { + super(oracleSupplier); + } + + public DynamicParallelAdaptiveOracleBuilder(Collection> oracles) { + super(oracles); + } + + @Override + protected DynamicParallelAdaptiveOracle buildOracle(Supplier> supplier, + int batchSize, + ExecutorService executorService) { + return new DynamicParallelAdaptiveOracle<>(supplier, batchSize, executorService); + } +} diff --git a/oracles/parallelism/src/main/java/de/learnlib/oracle/parallelism/ParallelOracleBuilders.java b/oracles/parallelism/src/main/java/de/learnlib/oracle/parallelism/ParallelOracleBuilders.java index e9ae0976a1..1e93575238 100644 --- a/oracles/parallelism/src/main/java/de/learnlib/oracle/parallelism/ParallelOracleBuilders.java +++ b/oracles/parallelism/src/main/java/de/learnlib/oracle/parallelism/ParallelOracleBuilders.java @@ -18,10 +18,12 @@ import java.util.Collection; import java.util.function.Supplier; +import de.learnlib.oracle.AdaptiveMembershipOracle; import de.learnlib.oracle.MembershipOracle; import de.learnlib.oracle.OmegaMembershipOracle; import de.learnlib.oracle.ThreadPool.PoolPolicy; import de.learnlib.oracle.membership.AbstractSULOmegaOracle; +import de.learnlib.oracle.membership.SULAdaptiveOracle; import de.learnlib.oracle.membership.SULOracle; import de.learnlib.oracle.membership.StateLocalInputSULOracle; import de.learnlib.sul.ObservableSUL; @@ -85,7 +87,7 @@ private ParallelOracleBuilders() { */ public static DynamicParallelOracleBuilder> newDynamicParallelOracle(SUL sul) { checkFork(sul); - return new DynamicParallelOracleBuilder<>(toSupplier(sul)); + return newDynamicParallelOracle(toSupplier(sul)); } /** @@ -95,8 +97,8 @@ public static DynamicParallelOracleBuilder> newDynamicParallel * @param sul * the sul instance for spawning new thread-specific membership oracle instances * @param undefinedInput - * the input symbol used for responding to inputs that are not {@link StateLocalInputSUL#currentlyEnabledInputs() - * enabled}. + * the input symbol used for responding to inputs that are not + * {@link StateLocalInputSUL#currentlyEnabledInputs() enabled}. * @param * input symbol type * @param @@ -165,8 +167,8 @@ public static DynamicParallelOracleBuilder newDynamicParallelOracle } /** - * Creates a {@link DynamicParallelOracleBuilder} using the provided {@code sul} as a supplier. This requires that - * the sul is {@link SUL#canFork() forkable}. + * Creates a {@link DynamicParallelOmegaOracleBuilder} using the provided {@code sul} as a supplier. This requires + * that the sul is {@link SUL#canFork() forkable}. * * @param sul * the sul instance for spawning new thread-specific omega membership oracle instances @@ -179,8 +181,7 @@ public static DynamicParallelOracleBuilder newDynamicParallelOracle */ public static DynamicParallelOmegaOracleBuilder> newDynamicParallelOmegaOracle(ObservableSUL sul) { checkFork(sul); - // instantiate inner supplier to resolve generics - return new DynamicParallelOmegaOracleBuilder<>(toSupplier(sul)::get); + return newDynamicParallelOmegaOracle(toSupplier(sul)::get); } /** @@ -245,6 +246,80 @@ public static DynamicParallelOmegaOracleBuilder newDynamicPar return new DynamicParallelOmegaOracleBuilder<>(oracles); } + /** + * Creates a {@link DynamicParallelAdaptiveOracleBuilder} using the provided {@code sul} as a supplier. This + * requires that the sul is {@link SUL#canFork() forkable}. + * + * @param sul + * the sul instance for spawning new thread-specific omega membership oracle instances + * @param + * input symbol type + * @param + * output symbol type + * + * @return a preconfigured oracle builder + */ + public static DynamicParallelAdaptiveOracleBuilder newDynamicParallelAdaptiveOracle(SUL sul) { + checkFork(sul); + return newDynamicParallelAdaptiveOracle(toAdaptiveSupplier(sul)); + } + + /** + * Creates a {@link DynamicParallelAdaptiveOracleBuilder} using the provided supplier. + * + * @param oracleSupplier + * the supplier for spawning new thread-specific membership oracle instances + * @param + * input symbol type + * @param + * output symbol type + * + * @return a preconfigured oracle builder + */ + public static DynamicParallelAdaptiveOracleBuilder newDynamicParallelAdaptiveOracle(Supplier> oracleSupplier) { + return new DynamicParallelAdaptiveOracleBuilder<>(oracleSupplier); + } + + /** + * Convenience method for {@link #newDynamicParallelAdaptiveOracle(Collection)}. + * + * @param firstOracle + * the first (mandatory) oracle + * @param otherOracles + * further (optional) oracles to be used by other threads + * @param + * input symbol type + * @param + * output symbol type + * + * @return a preconfigured oracle builder + */ + @SafeVarargs + public static DynamicParallelAdaptiveOracleBuilder newDynamicParallelAdaptiveOracle( + AdaptiveMembershipOracle firstOracle, + AdaptiveMembershipOracle... otherOracles) { + return newDynamicParallelAdaptiveOracle(CollectionUtil.list(firstOracle, otherOracles)); + } + + /** + * Creates a {@link DynamicParallelAdaptiveOracleBuilder} using the provided collection of membership oracles. The + * resulting parallel oracle will always use a {@link PoolPolicy#FIXED} pool policy and spawn a separate thread for + * each of the provided oracles (so that the oracles do not need to care about synchronization if they don't share + * state). + * + * @param oracles + * the oracle instances to distribute the queries to + * @param + * input symbol type + * @param + * output symbol type + * + * @return the preconfigured oracle builder + */ + public static DynamicParallelAdaptiveOracleBuilder newDynamicParallelAdaptiveOracle(Collection> oracles) { + return new DynamicParallelAdaptiveOracleBuilder<>(oracles); + } + /** * Creates a {@link StaticParallelOracleBuilder} using the provided {@code sul} as a supplier. This requires that * the sul is {@link SUL#canFork() forkable}. @@ -260,7 +335,7 @@ public static DynamicParallelOmegaOracleBuilder newDynamicPar */ public static StaticParallelOracleBuilder> newStaticParallelOracle(SUL sul) { checkFork(sul); - return new StaticParallelOracleBuilder<>(toSupplier(sul)); + return newStaticParallelOracle(toSupplier(sul)); } /** @@ -356,8 +431,7 @@ public static StaticParallelOracleBuilder newStaticParallelOracle(C */ public static StaticParallelOmegaOracleBuilder> newStaticParallelOmegaOracle(ObservableSUL sul) { checkFork(sul); - // instantiate inner supplier to resolve generics - return new StaticParallelOmegaOracleBuilder<>(toSupplier(sul)::get); + return newStaticParallelOmegaOracle(toSupplier(sul)::get); } /** @@ -421,6 +495,80 @@ public static StaticParallelOmegaOracleBuilder newStaticParal return new StaticParallelOmegaOracleBuilder<>(oracles); } + /** + * Creates a {@link StaticParallelAdaptiveOracleBuilder} using the provided {@code sul} as a supplier. This requires + * that the sul is {@link SUL#canFork() forkable}. + * + * @param sul + * the sul instance for spawning new thread-specific omega membership oracle instances + * @param + * input symbol type + * @param + * output domain type + * + * @return a preconfigured oracle builder + */ + public static StaticParallelAdaptiveOracleBuilder newStaticParallelAdaptiveOracle(SUL sul) { + checkFork(sul); + return newStaticParallelAdaptiveOracle(toAdaptiveSupplier(sul)); + } + + /** + * Creates a {@link StaticParallelAdaptiveOracleBuilder} using the provided supplier. + * + * @param oracleSupplier + * the supplier for spawning new thread-specific membership oracle instances + * @param + * input symbol type + * @param + * output symbol type + * + * @return a preconfigured oracle builder + */ + public static StaticParallelAdaptiveOracleBuilder newStaticParallelAdaptiveOracle(Supplier> oracleSupplier) { + return new StaticParallelAdaptiveOracleBuilder<>(oracleSupplier); + } + + /** + * Convenience method for {@link #newStaticParallelAdaptiveOracle(Collection)}. + * + * @param firstOracle + * the first (mandatory) oracle + * @param otherOracles + * further (optional) oracles to be used by other threads + * @param + * input symbol type + * @param + * output symbol type + * + * @return a preconfigured oracle builder + */ + @SafeVarargs + public static StaticParallelAdaptiveOracleBuilder newStaticParallelAdaptiveOracle( + AdaptiveMembershipOracle firstOracle, + AdaptiveMembershipOracle... otherOracles) { + return newStaticParallelAdaptiveOracle(CollectionUtil.list(firstOracle, otherOracles)); + } + + /** + * Creates a {@link StaticParallelAdaptiveOracleBuilder} using the provided collection of membership oracles. The + * resulting parallel oracle will always use a {@link PoolPolicy#FIXED} pool policy and spawn a separate thread for + * each of the provided oracles (so that the oracles do not need to care about synchronization if they don't share + * state). + * + * @param oracles + * the oracle instances to distribute the queries to + * @param + * input symbol type + * @param + * output symbol type + * + * @return the preconfigured oracle builder + */ + public static StaticParallelAdaptiveOracleBuilder newStaticParallelAdaptiveOracle(Collection> oracles) { + return new StaticParallelAdaptiveOracleBuilder<>(oracles); + } + private static Supplier> toSupplier(SUL sul) { return () -> new SULOracle<>(sul.fork()); } @@ -434,6 +582,10 @@ private static Supplier>> toSuppli return () -> AbstractSULOmegaOracle.newOracle(sul.fork()); } + private static Supplier> toAdaptiveSupplier(SUL sul) { + return () -> new SULAdaptiveOracle<>(sul.fork()); + } + private static void checkFork(SUL sul) { if (!sul.canFork()) { throw new IllegalArgumentException("SUL must be forkable for parallel processing"); diff --git a/oracles/parallelism/src/main/java/de/learnlib/oracle/parallelism/StaticParallelAdaptiveOracle.java b/oracles/parallelism/src/main/java/de/learnlib/oracle/parallelism/StaticParallelAdaptiveOracle.java new file mode 100644 index 0000000000..2bbe2a1392 --- /dev/null +++ b/oracles/parallelism/src/main/java/de/learnlib/oracle/parallelism/StaticParallelAdaptiveOracle.java @@ -0,0 +1,48 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.oracle.parallelism; + +import java.util.Collection; + +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.oracle.ParallelAdaptiveOracle; +import de.learnlib.query.AdaptiveQuery; +import org.checkerframework.checker.index.qual.NonNegative; + +/** + * A specialized {@link AbstractStaticBatchProcessor} for {@link AdaptiveMembershipOracle}s that implements + * {@link ParallelAdaptiveOracle}. + * + * @param + * input symbol type + * @param + * output symbol type + */ +public class StaticParallelAdaptiveOracle + extends AbstractStaticBatchProcessor, AdaptiveMembershipOracle> + implements ParallelAdaptiveOracle { + + public StaticParallelAdaptiveOracle(Collection> oracles, + @NonNegative int minBatchSize, + PoolPolicy policy) { + super(oracles, minBatchSize, policy); + } + + @Override + public void processQueries(Collection> queries) { + processBatch(queries); + } +} diff --git a/oracles/parallelism/src/main/java/de/learnlib/oracle/parallelism/StaticParallelAdaptiveOracleBuilder.java b/oracles/parallelism/src/main/java/de/learnlib/oracle/parallelism/StaticParallelAdaptiveOracleBuilder.java new file mode 100644 index 0000000000..8027aafb96 --- /dev/null +++ b/oracles/parallelism/src/main/java/de/learnlib/oracle/parallelism/StaticParallelAdaptiveOracleBuilder.java @@ -0,0 +1,50 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.oracle.parallelism; + +import java.util.Collection; +import java.util.function.Supplier; + +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.oracle.ThreadPool.PoolPolicy; +import de.learnlib.query.AdaptiveQuery; + +/** + * A specialized {@link AbstractStaticBatchProcessorBuilder} for {@link AdaptiveMembershipOracle}s. + * + * @param + * input symbol type + * @param + * output symbol type + */ +public class StaticParallelAdaptiveOracleBuilder + extends AbstractStaticBatchProcessorBuilder, AdaptiveMembershipOracle, StaticParallelAdaptiveOracle> { + + public StaticParallelAdaptiveOracleBuilder(Supplier> oracleSupplier) { + super(oracleSupplier); + } + + public StaticParallelAdaptiveOracleBuilder(Collection> oracles) { + super(oracles); + } + + @Override + protected StaticParallelAdaptiveOracle buildOracle(Collection> oracleInstances, + int minBatchSize, + PoolPolicy poolPolicy) { + return new StaticParallelAdaptiveOracle<>(oracleInstances, minBatchSize, poolPolicy); + } +} diff --git a/oracles/parallelism/src/test/java/de/learnlib/oracle/parallelism/AbstractDynamicParallelAdaptiveOracleTest.java b/oracles/parallelism/src/test/java/de/learnlib/oracle/parallelism/AbstractDynamicParallelAdaptiveOracleTest.java new file mode 100644 index 0000000000..553e891291 --- /dev/null +++ b/oracles/parallelism/src/test/java/de/learnlib/oracle/parallelism/AbstractDynamicParallelAdaptiveOracleTest.java @@ -0,0 +1,148 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.oracle.parallelism; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.oracle.ParallelAdaptiveOracle; +import de.learnlib.oracle.ThreadPool.PoolPolicy; +import de.learnlib.query.AdaptiveQuery; +import de.learnlib.query.AdaptiveQuery.Response; +import org.testng.Assert; +import org.testng.annotations.Test; + +@Test +public abstract class AbstractDynamicParallelAdaptiveOracleTest { + + @Test(dataProvider = "policies", dataProviderClass = Utils.class) + public void testEmpty(PoolPolicy poolPolicy) { + ParallelAdaptiveOracle oracle = getBuilder().withPoolPolicy(poolPolicy).create(); + + try { + oracle.processQueries(Collections.emptyList()); + } finally { + oracle.shutdownNow(); + } + } + + @Test(dataProvider = "policies", dataProviderClass = Utils.class) + public void testDistinctQueries(PoolPolicy poolPolicy) { + ParallelAdaptiveOracle oracle = + getBuilder().withBatchSize(1).withPoolSize(4).withPoolPolicy(poolPolicy).create(); + + try { + List> queries = createQueries(100); + + oracle.processQueries(queries); + + for (AnswerOnceQuery query : queries) { + Assert.assertEquals(query.counter.get(), 0); + } + } finally { + oracle.shutdown(); + } + } + + @Test(dataProvider = "policies", dataProviderClass = Utils.class, expectedExceptions = IllegalStateException.class) + public void testDuplicateQueries(PoolPolicy poolPolicy) { + ParallelAdaptiveOracle oracle = + getBuilder().withBatchSize(1).withPoolSize(4).withPoolPolicy(poolPolicy).create(); + try { + List> queries = new ArrayList<>(createQueries(100)); + queries.add(queries.get(0)); + + oracle.processQueries(queries); + } finally { + oracle.shutdown(); + } + } + + protected abstract DynamicParallelAdaptiveOracleBuilder getBuilder(); + + protected static List> createQueries(int numQueries) { + List> queries = new ArrayList<>(numQueries); + + for (int i = 0; i < numQueries; i++) { + queries.add(new AnswerOnceQuery<>(3)); + } + + return queries; + } + + static class NullOracle implements AdaptiveMembershipOracle { + + @Override + public void processQueries(Collection> adaptiveQueries) { + for (AdaptiveQuery q : adaptiveQueries) { + Response response; + do { + response = q.processOutput(null); + } while (response != Response.FINISHED); + } + } + } + + static final class AnswerOnceQuery implements AdaptiveQuery { + + private final AtomicInteger counter; + private final List outputs; + private final UUID id; + + AnswerOnceQuery(int count) { + this(count, null); + } + + AnswerOnceQuery(int count, UUID id) { + this.counter = new AtomicInteger(count); + this.id = id; + this.outputs = Collections.synchronizedList(new ArrayList<>(count)); + } + + @Override + public Void getInput() { + return null; + } + + @Override + public Response processOutput(D out) { + this.outputs.add(out); + final int i = counter.decrementAndGet(); + + if (i == 0) { + return Response.FINISHED; + } else if (i > 0) { + return Response.SYMBOL; + } else { + throw new IllegalStateException("Query was answered more often than it should have been"); + } + } + + public List getOutputs() { + return outputs; + } + + public UUID getId() { + return id; + } + } + +} diff --git a/oracles/parallelism/src/test/java/de/learnlib/oracle/parallelism/AbstractStaticParallelAdaptiveOracleTest.java b/oracles/parallelism/src/test/java/de/learnlib/oracle/parallelism/AbstractStaticParallelAdaptiveOracleTest.java new file mode 100644 index 0000000000..c20eb2dc38 --- /dev/null +++ b/oracles/parallelism/src/test/java/de/learnlib/oracle/parallelism/AbstractStaticParallelAdaptiveOracleTest.java @@ -0,0 +1,210 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.oracle.parallelism; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import de.learnlib.oracle.AdaptiveMembershipOracle; +import de.learnlib.oracle.ThreadPool.PoolPolicy; +import de.learnlib.oracle.parallelism.AbstractDynamicParallelAdaptiveOracleTest.AnswerOnceQuery; +import de.learnlib.oracle.parallelism.Utils.Analysis; +import de.learnlib.query.AdaptiveQuery; +import org.testng.Assert; +import org.testng.annotations.Test; + +public abstract class AbstractStaticParallelAdaptiveOracleTest { + + private static final int NUM_ANSWERS = 1; + + @Test(dataProvider = "policies", dataProviderClass = Utils.class) + public void testZeroQueries(PoolPolicy policy) { + StaticParallelAdaptiveOracle oracle = getOracle(policy); + oracle.processQueries(Collections.emptyList()); + Analysis ana = analyze(Collections.emptyList()); + Utils.sanityCheck(ana); + Assert.assertEquals(ana.involvedOracles.size(), 0); + oracle.shutdownNow(); + } + + @Test(dataProvider = "policies", dataProviderClass = Utils.class) + public void testLessThanMin(PoolPolicy policy) { + StaticParallelAdaptiveOracle oracle = getOracle(policy); + List> queries = createQueries(Utils.MIN_BATCH_SIZE - 1); + oracle.processQueries(queries); + Analysis ana = analyze(queries); + Utils.sanityCheck(ana); + Assert.assertEquals(ana.involvedOracles.size(), 1); + oracle.shutdown(); + } + + @Test(dataProvider = "policies", dataProviderClass = Utils.class) + public void testMin(PoolPolicy policy) { + StaticParallelAdaptiveOracle oracle = getOracle(policy); + List> queries = createQueries(Utils.MIN_BATCH_SIZE); + oracle.processQueries(queries); + Analysis ana = analyze(queries); + Utils.sanityCheck(ana); + Assert.assertEquals(ana.involvedOracles.size(), 1); + oracle.shutdown(); + } + + @Test(dataProvider = "policies", dataProviderClass = Utils.class) + public void testLessThanTwoBatches(PoolPolicy policy) { + StaticParallelAdaptiveOracle oracle = getOracle(policy); + List> queries = createQueries(2 * Utils.MIN_BATCH_SIZE - 1); + oracle.processQueries(queries); + Analysis ana = analyze(queries); + Utils.sanityCheck(ana); + Assert.assertEquals(ana.involvedOracles.size(), 1); + oracle.shutdown(); + } + + @Test(dataProvider = "policies", dataProviderClass = Utils.class) + public void testLessThanSixBatches(PoolPolicy policy) { + StaticParallelAdaptiveOracle oracle = getOracle(policy); + List> queries = createQueries(5 * Utils.MIN_BATCH_SIZE + Utils.MIN_BATCH_SIZE / 2); + oracle.processQueries(queries); + Analysis ana = analyze(queries); + Utils.sanityCheck(ana); + Assert.assertEquals(ana.involvedOracles.size(), 5); + oracle.shutdown(); + } + + @Test(dataProvider = "policies", dataProviderClass = Utils.class) + public void testFullLoad(PoolPolicy policy) { + StaticParallelAdaptiveOracle oracle = getOracle(policy); + List> queries = createQueries(2 * Utils.NUM_ORACLES * Utils.MIN_BATCH_SIZE); + oracle.processQueries(queries); + Analysis ana = analyze(queries); + Utils.sanityCheck(ana); + Assert.assertEquals(ana.involvedOracles.size(), Utils.NUM_ORACLES); + oracle.shutdown(); + } + + protected abstract StaticParallelAdaptiveOracleBuilder getBuilder(); + + protected abstract TestOutput extractTestOutput(D output); + + protected TestMembershipOracle[] getOracles() { + TestMembershipOracle[] oracles = new TestMembershipOracle[Utils.NUM_ORACLES]; + for (int i = 0; i < Utils.NUM_ORACLES; i++) { + oracles[i] = new TestMembershipOracle(i); + } + + return oracles; + } + + private StaticParallelAdaptiveOracle getOracle(PoolPolicy poolPolicy) { + return getBuilder().withMinBatchSize(Utils.MIN_BATCH_SIZE) + .withNumInstances(Utils.NUM_ORACLES) + .withPoolPolicy(poolPolicy) + .create(); + } + + private List> createQueries(int num) { + List> result = new ArrayList<>(num); + for (int i = 0; i < num; i++) { + result.add(new AnswerOnceQuery<>(NUM_ANSWERS, UUID.randomUUID())); + } + return result; + } + + private Analysis analyze(Collection> queries) { + List oracles = new ArrayList<>(); + Map> seqIds = new HashMap<>(); + Map incorrectAnswers = new HashMap<>(); + + for (AnswerOnceQuery qry : queries) { + List outputs = qry.getOutputs(); + Assert.assertEquals(outputs.size(), NUM_ANSWERS); + D output = outputs.get(0); + TestOutput out = extractTestOutput(output); + Assert.assertNotNull(out); + int oracleId = out.oracleId; + List seqIdList = seqIds.get(oracleId); + if (seqIdList == null) { + oracles.add(oracleId); + seqIdList = new ArrayList<>(); + seqIds.put(oracleId, seqIdList); + incorrectAnswers.put(oracleId, 0); + } + + int seqId = out.batchSeqId; + seqIdList.add(seqId); + + if (!qry.getId().equals(out.id)) { + incorrectAnswers.put(oracleId, incorrectAnswers.get(oracleId) + 1); + } + } + + int minBatchSize = -1; + int maxBatchSize = -1; + for (List batch : seqIds.values()) { + if (minBatchSize == -1) { + maxBatchSize = batch.size(); + minBatchSize = maxBatchSize; + } else { + if (batch.size() < minBatchSize) { + minBatchSize = batch.size(); + } + if (batch.size() > maxBatchSize) { + maxBatchSize = batch.size(); + } + } + } + + return new Analysis(oracles, seqIds, incorrectAnswers, minBatchSize, maxBatchSize); + } + + static final class TestOutput { + + public final int oracleId; + public final int batchSeqId; + public final UUID id; + + TestOutput(int oracleId, int batchSeqId, UUID id) { + this.oracleId = oracleId; + this.batchSeqId = batchSeqId; + this.id = id; + } + } + + static final class TestMembershipOracle implements AdaptiveMembershipOracle { + + private final int oracleId; + + TestMembershipOracle(int oracleId) { + this.oracleId = oracleId; + } + + @Override + public void processQueries(Collection> queries) { + int batchSeqId = 0; + for (AdaptiveQuery q : queries) { + for (int i = 0; i < NUM_ANSWERS; i++) { + q.processOutput(new TestOutput(oracleId, batchSeqId++, ((AnswerOnceQuery) q).getId())); + } + } + } + } + +} diff --git a/oracles/parallelism/src/test/java/de/learnlib/oracle/parallelism/DynamicParallelAdaptiveOracleTest.java b/oracles/parallelism/src/test/java/de/learnlib/oracle/parallelism/DynamicParallelAdaptiveOracleTest.java new file mode 100644 index 0000000000..9f13b117fc --- /dev/null +++ b/oracles/parallelism/src/test/java/de/learnlib/oracle/parallelism/DynamicParallelAdaptiveOracleTest.java @@ -0,0 +1,119 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.oracle.parallelism; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import de.learnlib.oracle.ParallelAdaptiveOracle; +import de.learnlib.oracle.ThreadPool.PoolPolicy; +import de.learnlib.query.AdaptiveQuery; +import org.testng.annotations.Test; + +public class DynamicParallelAdaptiveOracleTest extends AbstractDynamicParallelAdaptiveOracleTest { + + @Override + protected DynamicParallelAdaptiveOracleBuilder getBuilder() { + return ParallelOracleBuilders.newDynamicParallelAdaptiveOracle(Arrays.asList(new NullOracle(), + new NullOracle(), + new NullOracle())); + } + + @Test(dataProvider = "policies", dataProviderClass = Utils.class, timeOut = 2000) + public void testThreadCreation(PoolPolicy poolPolicy) { + + final List> queries = createQueries(10); + final int expectedThreads = queries.size(); + + final CountDownLatch latch = new CountDownLatch(expectedThreads); + final NullOracle[] oracles = new NullOracle[expectedThreads]; + + for (int i = 0; i < expectedThreads; i++) { + oracles[i] = new NullOracle() { + + @Override + public void processQueries(Collection> queries) { + try { + latch.countDown(); + latch.await(); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + super.processQueries(queries); + } + }; + } + + final ParallelAdaptiveOracle oracle = ParallelOracleBuilders.newDynamicParallelAdaptiveOracle( + oracles[0], + Arrays.copyOfRange(oracles, 1, oracles.length)) + .withBatchSize(1) + .withPoolSize(oracles.length) + .withPoolPolicy(poolPolicy) + .create(); + + try { + // this method only returns, if 'expectedThreads' threads are spawned, which all decrease the shared latch + oracle.processQueries(queries); + } finally { + oracle.shutdown(); + } + } + + @Test(dataProvider = "policies", dataProviderClass = Utils.class, timeOut = 2000) + public void testThreadScheduling(PoolPolicy poolPolicy) { + + final List> queries = createQueries(10); + final CountDownLatch latch = new CountDownLatch(queries.size() - 1); + + final NullOracle awaitingOracle = new NullOracle() { + + @Override + public void processQueries(Collection> queries) { + try { + latch.await(); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + super.processQueries(queries); + } + }; + + final NullOracle countDownOracle = new NullOracle() { + + @Override + public void processQueries(Collection> queries) { + latch.countDown(); + super.processQueries(queries); + } + }; + + final ParallelAdaptiveOracle oracle = + ParallelOracleBuilders.newDynamicParallelAdaptiveOracle(awaitingOracle, countDownOracle) + .withPoolSize(2) + .withPoolPolicy(poolPolicy) + .create(); + + try { + // this method only returns, if the countDownOracle was scheduled 9 times to unblock the awaitingOracle + oracle.processQueries(queries); + } finally { + oracle.shutdown(); + } + } +} diff --git a/oracles/parallelism/src/test/java/de/learnlib/oracle/parallelism/DynamicParallelAdaptiveSupplierTest.java b/oracles/parallelism/src/test/java/de/learnlib/oracle/parallelism/DynamicParallelAdaptiveSupplierTest.java new file mode 100644 index 0000000000..a8400f3415 --- /dev/null +++ b/oracles/parallelism/src/test/java/de/learnlib/oracle/parallelism/DynamicParallelAdaptiveSupplierTest.java @@ -0,0 +1,24 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.oracle.parallelism; + +public class DynamicParallelAdaptiveSupplierTest extends AbstractDynamicParallelAdaptiveOracleTest { + + @Override + protected DynamicParallelAdaptiveOracleBuilder getBuilder() { + return ParallelOracleBuilders.newDynamicParallelAdaptiveOracle(NullOracle::new); + } +} diff --git a/oracles/parallelism/src/test/java/de/learnlib/oracle/parallelism/StaticParallelAdaptiveOracleTest.java b/oracles/parallelism/src/test/java/de/learnlib/oracle/parallelism/StaticParallelAdaptiveOracleTest.java new file mode 100644 index 0000000000..2828bea7e5 --- /dev/null +++ b/oracles/parallelism/src/test/java/de/learnlib/oracle/parallelism/StaticParallelAdaptiveOracleTest.java @@ -0,0 +1,35 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.oracle.parallelism; + +import java.util.Arrays; + +import de.learnlib.oracle.parallelism.AbstractStaticParallelAdaptiveOracleTest.TestOutput; + +public class StaticParallelAdaptiveOracleTest extends AbstractStaticParallelAdaptiveOracleTest { + + @Override + protected StaticParallelAdaptiveOracleBuilder getBuilder() { + TestMembershipOracle[] oracles = getOracles(); + return ParallelOracleBuilders.newStaticParallelAdaptiveOracle(oracles[0], + Arrays.copyOfRange(oracles, 1, oracles.length)); + } + + @Override + protected TestOutput extractTestOutput(TestOutput output) { + return output; + } +} diff --git a/oracles/parallelism/src/test/java/de/learnlib/oracle/parallelism/StaticParallelAdaptiveSupplierTest.java b/oracles/parallelism/src/test/java/de/learnlib/oracle/parallelism/StaticParallelAdaptiveSupplierTest.java new file mode 100644 index 0000000000..838f0c871d --- /dev/null +++ b/oracles/parallelism/src/test/java/de/learnlib/oracle/parallelism/StaticParallelAdaptiveSupplierTest.java @@ -0,0 +1,32 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of LearnLib, http://www.learnlib.de/. + * + * 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. + */ +package de.learnlib.oracle.parallelism; + +import de.learnlib.oracle.parallelism.AbstractDynamicBatchProcessorBuilder.StaticOracleProvider; +import de.learnlib.oracle.parallelism.AbstractStaticParallelAdaptiveOracleTest.TestOutput; + +public class StaticParallelAdaptiveSupplierTest extends AbstractStaticParallelAdaptiveOracleTest { + + @Override + protected StaticParallelAdaptiveOracleBuilder getBuilder() { + return ParallelOracleBuilders.newStaticParallelAdaptiveOracle(new StaticOracleProvider<>(getOracles())); + } + + @Override + protected TestOutput extractTestOutput(TestOutput output) { + return output; + } +} diff --git a/pom.xml b/pom.xml index 6868f35c1a..da3617a7c6 100644 --- a/pom.xml +++ b/pom.xml @@ -128,6 +128,14 @@ limitations under the License. Developer + + Leon Vitorovic + TU Dortmund University, Chair of Programming Systems + leon.vitorovic@tu-dortmund.de + + Developer (Graduate) + + Stephan Windmüller stephan.windmueller@tu-dortmund.de