
de.learnlib.algorithms.ttt.base.AbstractTTTLearner Maven / Gradle / Ivy
/* Copyright (C) 2013-2019 TU Dortmund
* 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.algorithms.ttt.base;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nonnull;
import com.google.common.collect.Iterators;
import de.learnlib.acex.AcexAnalyzer;
import de.learnlib.acex.analyzers.AcexAnalyzers;
import de.learnlib.api.AccessSequenceProvider;
import de.learnlib.api.Resumable;
import de.learnlib.api.algorithm.LearningAlgorithm;
import de.learnlib.api.oracle.MembershipOracle;
import de.learnlib.api.query.DefaultQuery;
import de.learnlib.counterexamples.acex.OutInconsPrefixTransformAcex;
import de.learnlib.datastructure.discriminationtree.SplitData;
import net.automatalib.SupportsGrowingAlphabet;
import net.automatalib.automata.fsa.DFA;
import net.automatalib.commons.smartcollections.ElementReference;
import net.automatalib.commons.smartcollections.UnorderedCollection;
import net.automatalib.exception.GrowingAlphabetNotSupportedException;
import net.automatalib.words.Alphabet;
import net.automatalib.words.Word;
import net.automatalib.words.impl.Alphabets;
/**
* The TTT learning algorithm for {@link DFA}.
*
* @param
* input symbol type
*
* @author Malte Isberner
*/
public abstract class AbstractTTTLearner
implements LearningAlgorithm, SupportsGrowingAlphabet, Resumable> {
protected final Alphabet alphabet;
protected final MembershipOracle oracle;
protected final AcexAnalyzer analyzer;
/**
* Open transitions, i.e., transitions that possibly point to a non-leaf node in the discrimination tree.
*/
protected final IncomingList openTransitions = new IncomingList<>();
/**
* The blocks during a split operation. A block is a maximal subtree of the discrimination tree containing temporary
* discriminators at its root.
*/
protected final BlockList blockList = new BlockList<>();
private final Collection> eventListeners = new UnorderedCollection<>();
protected AbstractTTTHypothesis hypothesis;
protected BaseTTTDiscriminationTree dtree;
protected AbstractTTTLearner(Alphabet alphabet,
MembershipOracle oracle,
AbstractTTTHypothesis hypothesis,
BaseTTTDiscriminationTree dtree,
AcexAnalyzer analyzer) {
this.alphabet = alphabet;
this.hypothesis = hypothesis;
this.oracle = oracle;
this.dtree = dtree;
this.analyzer = analyzer;
}
/**
* Marks a node, and propagates the label up to all nodes on the path from the block root to this node.
*
* @param node
* the node to mark
* @param label
* the label to mark the node with
*/
private static void markAndPropagate(AbstractBaseDTNode node, D label) {
AbstractBaseDTNode curr = node;
while (curr != null && curr.getSplitData() != null) {
if (!curr.getSplitData().mark(label)) {
return;
}
curr = curr.getParent();
}
}
/**
* Moves all transition from the "incoming" list (for a given label) of an old node to the "incoming" list of a new
* node.
*
* @param newNode
* the new node
* @param oldNode
* the old node
* @param label
* the label to consider
*/
private static void moveIncoming(AbstractBaseDTNode newNode,
AbstractBaseDTNode oldNode,
D label) {
newNode.getIncoming().insertAllIncoming(oldNode.getSplitData().getIncoming(label));
}
/**
* Establish the connection between a node in the discrimination tree and a state of the hypothesis.
*
* @param dtNode
* the node in the discrimination tree
* @param state
* the state in the hypothesis
*/
protected static void link(AbstractBaseDTNode dtNode, TTTState state) {
assert dtNode.isLeaf();
dtNode.setData(state);
state.dtLeaf = dtNode;
}
/*
* Private helper methods.
*/
@Override
public void startLearning() {
if (hypothesis.isInitialized()) {
throw new IllegalStateException();
}
TTTState init = hypothesis.initialize();
AbstractBaseDTNode initNode = dtree.sift(init.getAccessSequence(), false);
link(initNode, init);
initializeState(init);
closeTransitions();
}
@Override
public boolean refineHypothesis(DefaultQuery ceQuery) {
if (!refineHypothesisSingle(ceQuery)) {
return false;
}
while (refineHypothesisSingle(ceQuery)) {}
return true;
}
/**
* Initializes a state. Creates its outgoing transition objects, and adds them to the "open" list.
*
* @param state
* the state to initialize
*/
protected void initializeState(TTTState state) {
for (int i = 0; i < alphabet.size(); i++) {
I sym = alphabet.getSymbol(i);
TTTTransition trans = createTransition(state, sym);
trans.setNonTreeTarget(dtree.getRoot());
state.setTransition(i, trans);
openTransitions.insertIncoming(trans);
}
}
protected TTTTransition createTransition(TTTState state, I sym) {
return new TTTTransition<>(state, sym);
}
/**
* Performs a single refinement of the hypothesis, i.e., without repeated counterexample evaluation. The parameter
* and return value have the same significance as in {@link #refineHypothesis(DefaultQuery)}.
*
* @param ceQuery
* the counterexample (query) to be used for refinement
*
* @return {@code true} if the hypothesis was refined, {@code false} otherwise
*/
protected boolean refineHypothesisSingle(DefaultQuery ceQuery) {
TTTState state = getAnyState(ceQuery.getPrefix());
D out = computeHypothesisOutput(state, ceQuery.getSuffix());
if (Objects.equals(out, ceQuery.getOutput())) {
return false;
}
OutputInconsistency outIncons =
new OutputInconsistency<>(state, ceQuery.getSuffix(), ceQuery.getOutput());
do {
splitState(outIncons);
closeTransitions();
while (finalizeAny()) {
closeTransitions();
}
outIncons = findOutputInconsistency();
} while (outIncons != null);
assert allNodesFinal();
return true;
}
/**
* Splits a state in the hypothesis, using a temporary discriminator. The state to be split is identified by an
* incoming non-tree transition. This transition is subsequently turned into a spanning tree transition.
*
* @param transition
* the transition
* @param tempDiscriminator
* the temporary discriminator
*/
private void splitState(TTTTransition transition, Word tempDiscriminator, D oldOut, D newOut) {
assert !transition.isTree();
notifyPreSplit(transition, tempDiscriminator);
AbstractBaseDTNode dtNode = transition.getNonTreeTarget();
assert dtNode.isLeaf();
TTTState oldState = dtNode.getData();
assert oldState != null;
TTTState newState = makeTree(transition);
AbstractBaseDTNode.SplitResult children = split(dtNode, tempDiscriminator, oldOut, newOut);
dtNode.setTemp(true);
link(children.nodeOld, oldState);
link(children.nodeNew, newState);
if (dtNode.getParent() == null || !dtNode.getParent().isTemp()) {
blockList.insertBlock(dtNode);
}
notifyPostSplit(transition, tempDiscriminator);
}
private void splitState(OutputInconsistency outIncons) {
OutInconsPrefixTransformAcex acex = deriveAcex(outIncons);
try {
int breakpoint = analyzer.analyzeAbstractCounterexample(acex);
assert !acex.testEffects(breakpoint, breakpoint + 1);
Word suffix = outIncons.suffix;
TTTState predState = getDeterministicState(outIncons.srcState, suffix.prefix(breakpoint));
TTTState succState = getDeterministicState(outIncons.srcState, suffix.prefix(breakpoint + 1));
assert getDeterministicState(predState, Word.fromLetter(suffix.getSymbol(breakpoint))) == succState;
I sym = suffix.getSymbol(breakpoint);
Word splitSuffix = suffix.subWord(breakpoint + 1);
TTTTransition trans = predState.getTransition(alphabet.getSymbolIndex(sym));
assert !trans.isTree();
D oldOut = acex.effect(breakpoint + 1);
D newOut = succEffect(acex.effect(breakpoint));
splitState(trans, splitSuffix, oldOut, newOut);
} catch (HypothesisChangedException ignored) {
}
}
protected OutInconsPrefixTransformAcex deriveAcex(OutputInconsistency outIncons) {
TTTState source = outIncons.srcState;
Word suffix = outIncons.suffix;
OutInconsPrefixTransformAcex acex = new OutInconsPrefixTransformAcex<>(suffix,
oracle,
w -> getDeterministicState(source,
w).getAccessSequence());
acex.setEffect(0, outIncons.targetOut);
return acex;
}
protected abstract D succEffect(D effect);
/**
* Chooses a block root, and finalizes the corresponding discriminator.
*
* @return {@code true} if a splittable block root was found, {@code false} otherwise.
*/
protected boolean finalizeAny() {
GlobalSplitter splitter = findSplitterGlobal();
if (splitter != null) {
finalizeDiscriminator(splitter.blockRoot, splitter.localSplitter);
return true;
}
return false;
}
protected TTTState getDeterministicState(TTTState start, Word word) {
TTTState lastSingleton = start;
int lastSingletonIndex = 0;
Set> states = Collections.singleton(start);
int i = 1;
for (I sym : word) {
Set> nextStates = getNondetSuccessors(states, sym);
if (nextStates.size() == 1) {
lastSingleton = nextStates.iterator().next();
lastSingletonIndex = i;
}
states = nextStates;
i++;
}
if (lastSingletonIndex == word.length()) {
return lastSingleton;
}
TTTState curr = lastSingleton;
for (I sym : word.subWord(lastSingletonIndex)) {
TTTTransition trans = curr.getTransition(alphabet.getSymbolIndex(sym));
curr = requireSuccessor(trans);
}
return curr;
}
protected Set> getNondetSuccessors(Collection extends TTTState> states, I sym) {
Set> result = new HashSet<>();
int symIdx = alphabet.getSymbolIndex(sym);
for (TTTState state : states) {
TTTTransition trans = state.getTransition(symIdx);
if (trans.isTree()) {
result.add(trans.getTreeTarget());
} else {
AbstractBaseDTNode tgtNode = trans.getNonTreeTarget();
Iterators.addAll(result, tgtNode.subtreeStatesIterator());
}
}
return result;
}
protected TTTState getAnySuccessor(TTTState state, I sym) {
int symIdx = alphabet.getSymbolIndex(sym);
TTTTransition trans = state.getTransition(symIdx);
if (trans.isTree()) {
return trans.getTreeTarget();
}
return trans.getNonTreeTarget().subtreeStatesIterator().next();
}
protected TTTState getAnySuccessor(TTTState state, Iterable extends I> suffix) {
TTTState curr = state;
for (I sym : suffix) {
curr = getAnySuccessor(curr, sym);
}
return curr;
}
protected TTTTransition getStateTransition(TTTState state, I sym) {
int idx = alphabet.getSymbolIndex(sym);
return state.getTransition(idx);
}
private TTTState requireSuccessor(TTTTransition trans) {
if (trans.isTree()) {
return trans.getTreeTarget();
}
AbstractBaseDTNode newTgtNode = updateDTTarget(trans, true);
if (newTgtNode.getData() == null) {
makeTree(trans);
closeTransitions();
// FIXME: using exception handling for this is not very nice, but it appears there
// is no quicker way to abort counterexample analysis
throw new HypothesisChangedException();
}
return newTgtNode.getData();
}
/**
* Determines a global splitter, i.e., a splitter for any block. This method may (but is not required to) employ
* heuristics to obtain a splitter with a relatively short suffix length.
*
* @return a splitter for any of the blocks
*/
private GlobalSplitter findSplitterGlobal() {
// TODO: Make global option
boolean optimizeGlobal = true;
AbstractBaseDTNode bestBlockRoot = null;
Splitter bestSplitter = null;
for (AbstractBaseDTNode blockRoot : blockList) {
Splitter splitter = findSplitter(blockRoot);
if (splitter != null) {
if (bestSplitter == null || splitter.getDiscriminatorLength() < bestSplitter.getDiscriminatorLength()) {
bestSplitter = splitter;
bestBlockRoot = blockRoot;
}
if (!optimizeGlobal) {
break;
}
}
}
if (bestSplitter == null) {
return null;
}
return new GlobalSplitter<>(bestBlockRoot, bestSplitter);
}
/**
* Determines a (local) splitter for a given block. This method may (but is not required to) employ heuristics to
* obtain a splitter with a relatively short suffix.
*
* @param blockRoot
* the root of the block
*
* @return a splitter for this block, or {@code null} if no such splitter could be found.
*/
private Splitter findSplitter(AbstractBaseDTNode blockRoot) {
int alphabetSize = alphabet.size();
Object[] properties = new Object[alphabetSize];
@SuppressWarnings("unchecked")
AbstractBaseDTNode[] lcas = new AbstractBaseDTNode[alphabetSize];
boolean first = true;
for (TTTState state : blockRoot.subtreeStates()) {
for (int i = 0; i < alphabetSize; i++) {
TTTTransition trans = state.getTransition(i);
if (first) {
properties[i] = trans.getProperty();
lcas[i] = trans.getDTTarget();
} else {
if (!Objects.equals(properties[i], trans.getProperty())) {
return new Splitter<>(i);
}
lcas[i] = dtree.leastCommonAncestor(lcas[i], trans.getDTTarget());
}
}
first = false;
}
int shortestLen = Integer.MAX_VALUE;
AbstractBaseDTNode shortestLca = null;
int shortestLcaSym = -1;
for (int i = 0; i < alphabetSize; i++) {
AbstractBaseDTNode lca = lcas[i];
if (!lca.isTemp() && !lca.isLeaf()) {
int lcaLen = lca.getDiscriminator().length();
if (shortestLca == null || lcaLen < shortestLen) {
shortestLca = lca;
shortestLen = lcaLen;
shortestLcaSym = i;
}
}
}
if (shortestLca != null) {
return new Splitter<>(shortestLcaSym, shortestLca);
}
return null;
}
/**
* Creates a state in the hypothesis. This method cannot be used for the initial state, which has no incoming tree
* transition.
*
* @param transition
* the "parent" transition in the spanning tree
*
* @return the newly created state
*/
private TTTState createState(@Nonnull TTTTransition transition) {
return hypothesis.createState(transition);
}
/**
* Retrieves the target state of a given transition. This method works for both tree and non-tree transitions. If a
* non-tree transition points to a non-leaf node, it is updated accordingly before a result is obtained.
*
* @param trans
* the transition
*
* @return the target state of this transition (possibly after it having been updated)
*/
protected TTTState getAnyTarget(TTTTransition trans) {
if (trans.isTree()) {
return trans.getTreeTarget();
}
return trans.getNonTreeTarget().anySubtreeState();
}
/**
* Retrieves the state reached by the given sequence of symbols, starting from the initial state.
*
* @param suffix
* the sequence of symbols to process
*
* @return the state reached after processing the specified symbols
*/
private TTTState getAnyState(Iterable extends I> suffix) {
return getAnySuccessor(hypothesis.getInitialState(), suffix);
}
protected OutputInconsistency findOutputInconsistency() {
OutputInconsistency best = null;
for (TTTState state : hypothesis.getStates()) {
AbstractBaseDTNode node = state.getDTLeaf();
while (!node.isRoot()) {
D expectedOut = node.getParentOutcome();
node = node.getParent();
Word suffix = node.getDiscriminator();
if (best == null || suffix.length() < best.suffix.length()) {
D hypOut = computeHypothesisOutput(state, suffix);
if (!Objects.equals(hypOut, expectedOut)) {
best = new OutputInconsistency<>(state, suffix, expectedOut);
}
}
}
}
return best;
}
/**
* Finalize a discriminator. Given a block root and a {@link Splitter}, replace the discriminator at the block root
* by the one derived from the splitter, and update the discrimination tree accordingly.
*
* @param blockRoot
* the block root whose discriminator to finalize
* @param splitter
* the splitter to use for finalization
*/
private void finalizeDiscriminator(AbstractBaseDTNode blockRoot, Splitter splitter) {
assert blockRoot.isBlockRoot();
notifyPreFinalizeDiscriminator(blockRoot, splitter);
Word succDiscr = splitter.getDiscriminator().prepend(alphabet.getSymbol(splitter.symbolIdx));
if (!blockRoot.getDiscriminator().equals(succDiscr)) {
Word finalDiscriminator = prepareSplit(blockRoot, splitter);
Map> repChildren = createMap();
for (D label : blockRoot.getSplitData().getLabels()) {
repChildren.put(label, extractSubtree(blockRoot, label));
}
blockRoot.replaceChildren(repChildren);
blockRoot.setDiscriminator(finalDiscriminator);
}
declareFinal(blockRoot);
notifyPostFinalizeDiscriminator(blockRoot, splitter);
}
protected boolean allNodesFinal() {
Iterator> it = dtree.getRoot().subtreeNodesIterator();
while (it.hasNext()) {
AbstractBaseDTNode node = it.next();
assert !node.isTemp() : "Final node with discriminator " + node.getDiscriminator();
}
return true;
}
protected void declareFinal(AbstractBaseDTNode blockRoot) {
blockRoot.setTemp(false);
blockRoot.setSplitData(null);
blockRoot.removeFromBlockList();
for (AbstractBaseDTNode subtree : blockRoot.getChildren()) {
assert subtree.getSplitData() == null;
blockRoot.setChild(subtree.getParentOutcome(), subtree);
// Register as blocks, if they are non-trivial subtrees
if (subtree.isInner()) {
blockList.insertBlock(subtree);
}
}
openTransitions.insertAllIncoming(blockRoot.getIncoming());
}
/**
* Prepare a split operation on a block, by marking all the nodes and transitions in the subtree (and annotating
* them with {@link SplitData} objects).
*
* @param node
* the block root to be split
* @param splitter
* the splitter to use for splitting the block
*
* @return the discriminator to use for splitting
*/
private Word prepareSplit(AbstractBaseDTNode node, Splitter splitter) {
int symbolIdx = splitter.symbolIdx;
I symbol = alphabet.getSymbol(symbolIdx);
Word discriminator = splitter.getDiscriminator().prepend(symbol);
Deque> dfsStack = new ArrayDeque<>();
AbstractBaseDTNode succSeparator = splitter.succSeparator;
dfsStack.push(node);
assert node.getSplitData() == null;
while (!dfsStack.isEmpty()) {
AbstractBaseDTNode curr = dfsStack.pop();
assert curr.getSplitData() == null;
curr.setSplitData(new SplitData<>(IncomingList::new));
for (TTTTransition trans : curr.getIncoming()) {
D outcome = query(trans, discriminator);
curr.getSplitData().getIncoming(outcome).insertIncoming(trans);
markAndPropagate(curr, outcome);
}
if (curr.isInner()) {
for (AbstractBaseDTNode child : curr.getChildren()) {
dfsStack.push(child);
}
} else {
TTTState state = curr.getData();
assert state != null;
TTTTransition trans = state.getTransition(symbolIdx);
D outcome = predictSuccOutcome(trans, succSeparator);
assert outcome != null;
curr.getSplitData().setStateLabel(outcome);
markAndPropagate(curr, outcome);
}
}
return discriminator;
}
protected abstract D predictSuccOutcome(TTTTransition trans, AbstractBaseDTNode succSeparator);
/**
* Extract a (reduced) subtree containing all nodes with the given label from the subtree given by its root.
* "Reduced" here refers to the fact that the resulting subtree will contain no inner nodes with only one child.
*
* The tree returned by this method (represented by its root) will have as a parent node the root that was passed to
* this method.
*
* @param root
* the root of the subtree from which to extract
* @param label
* the label of the nodes to extract
*
* @return the extracted subtree
*/
private AbstractBaseDTNode extractSubtree(AbstractBaseDTNode root, D label) {
assert root.getSplitData() != null;
assert root.getSplitData().isMarked(label);
Deque> stack = new ArrayDeque<>();
AbstractBaseDTNode firstExtracted = createNewNode(root, label);
stack.push(new ExtractRecord<>(root, firstExtracted));
while (!stack.isEmpty()) {
ExtractRecord curr = stack.pop();
AbstractBaseDTNode original = curr.original;
AbstractBaseDTNode extracted = curr.extracted;
moveIncoming(extracted, original, label);
if (original.isLeaf()) {
if (Objects.equals(original.getSplitData().getStateLabel(), label)) {
link(extracted, original.getData());
} else {
createNewState(extracted);
}
extracted.updateIncoming();
} else {
List> markedChildren = new ArrayList<>();
for (AbstractBaseDTNode child : original.getChildren()) {
if (child.getSplitData().isMarked(label)) {
markedChildren.add(child);
}
}
if (markedChildren.size() > 1) {
Map> childMap = createMap();
for (AbstractBaseDTNode c : markedChildren) {
D childLabel = c.getParentOutcome();
AbstractBaseDTNode extractedChild = createNewNode(extracted, childLabel);
childMap.put(childLabel, extractedChild);
stack.push(new ExtractRecord<>(c, extractedChild));
}
extracted.setDiscriminator(original.getDiscriminator());
extracted.replaceChildren(childMap);
extracted.updateIncoming();
extracted.setTemp(true);
} else if (markedChildren.size() == 1) {
stack.push(new ExtractRecord<>(markedChildren.get(0), extracted));
} else { // markedChildren.isEmpty()
createNewState(extracted);
extracted.updateIncoming();
}
}
assert extracted.getSplitData() == null;
}
return firstExtracted;
}
protected Map createMap() {
return new HashMap<>();
}
/**
* Create a new state during extraction on-the-fly. This is required if a node in the DT has an incoming transition
* with a certain label, but in its subtree there are no leaves with this label as their state label.
*
* @param newNode
* the extracted node
*/
private void createNewState(AbstractBaseDTNode newNode) {
TTTTransition newTreeTrans = newNode.getIncoming().choose();
assert newTreeTrans != null;
TTTState newState = createState(newTreeTrans);
link(newNode, newState);
initializeState(newState);
}
protected abstract D computeHypothesisOutput(TTTState state, Word suffix);
public AbstractTTTHypothesis getHypothesisDS() {
return hypothesis;
}
protected void closeTransitions() {
UnorderedCollection> newStateNodes = new UnorderedCollection<>();
do {
newStateNodes.addAll(closeTransitions(openTransitions, false));
if (!newStateNodes.isEmpty()) {
addNewStates(newStateNodes);
}
} while (!openTransitions.isEmpty());
}
/**
* Ensures that the specified transitions point to a leaf-node. If a transition is a tree transition, this method
* has no effect.
*
* The provided transList is consumed in this process.
*
* If a transition needs sifting, the reached leaf node will be collected in the returned collection.
*
* @param transList
* the list of transitions
*
* @return a collection containing the reached leaves of transitions that needed sifting
*/
private List> closeTransitions(IncomingList transList, boolean hard) {
final List> transToSift = new ArrayList<>(transList.size());
TTTTransition t;
while ((t = transList.poll()) != null) {
if (!t.isTree()) {
transToSift.add(t);
}
}
if (transToSift.isEmpty()) {
return Collections.emptyList();
}
final Iterator> leavesIter = updateDTTargets(transToSift, hard).iterator();
final List> result = new ArrayList<>(transToSift.size());
for (final TTTTransition transition : transToSift) {
final AbstractBaseDTNode node = leavesIter.next();
if (node.isLeaf() && node.getData() == null && transition.getNextElement() == null) {
result.add(node);
}
}
assert !leavesIter.hasNext();
return result;
}
private void addNewStates(UnorderedCollection> newStateNodes) {
AbstractBaseDTNode minTransNode = null;
TTTTransition minTrans = null;
int minAsLen = Integer.MAX_VALUE;
ElementReference minTransNodeRef = null;
for (ElementReference ref : newStateNodes.references()) {
AbstractBaseDTNode newStateNode = newStateNodes.get(ref);
for (TTTTransition trans : newStateNode.getIncoming()) {
Word as = trans.getAccessSequence();
int asLen = as.length();
if (asLen < minAsLen) {
minTransNode = newStateNode;
minTrans = trans;
minAsLen = asLen;
minTransNodeRef = ref;
}
}
}
assert minTransNode != null;
newStateNodes.remove(minTransNodeRef);
TTTState state = makeTree(minTrans);
link(minTransNode, state);
initializeState(state);
}
protected TTTState makeTree(TTTTransition trans) {
assert !trans.isTree();
AbstractBaseDTNode node = trans.nonTreeTarget;
assert node.isLeaf();
TTTState state = createState(trans);
trans.removeFromList();
link(node, state);
initializeState(state);
return state;
}
/**
* Updates the transition to point to either a leaf in the discrimination tree, or---if the {@code hard} parameter
* is set to {@code false}---to a block root.
*
* @param transition
* the transition
* @param hard
* whether to consider leaves as sufficient targets only
*
* @return the new target node of the transition
*/
private AbstractBaseDTNode updateDTTarget(TTTTransition transition, boolean hard) {
if (transition.isTree()) {
return transition.getTreeTarget().dtLeaf;
}
AbstractBaseDTNode dt = transition.getNonTreeTarget();
dt = dtree.sift(dt, transition.getAccessSequence(), hard);
transition.setNonTreeTarget(dt);
return dt;
}
/**
* Bulk version of {@link #updateDTTarget(TTTTransition, boolean)}.
*/
private List> updateDTTargets(List> transitions, boolean hard) {
final List> nodes = new ArrayList<>(transitions.size());
final List> prefixes = new ArrayList<>(transitions.size());
for (final TTTTransition t : transitions) {
if (!t.isTree()) {
AbstractBaseDTNode dt = t.getNonTreeTarget();
nodes.add(dt);
prefixes.add(t.getAccessSequence());
}
}
final Iterator> leavesIter = dtree.sift(nodes, prefixes, hard).iterator();
final List> result = new ArrayList<>(transitions.size());
for (final TTTTransition t : transitions) {
if (t.isTree()) {
result.add(t.getTreeTarget().dtLeaf);
} else {
AbstractBaseDTNode leaf = leavesIter.next();
t.setNonTreeTarget(leaf);
result.add(leaf);
}
}
assert !leavesIter.hasNext();
return result;
}
/**
* Performs a membership query.
*
* @param prefix
* the prefix part of the query
* @param suffix
* the suffix part of the query
*
* @return the output
*/
protected D query(Word prefix, Word suffix) {
return oracle.answerQuery(prefix, suffix);
}
/**
* Performs a membership query, using an access sequence as its prefix.
*
* @param accessSeqProvider
* the object from which to obtain the access sequence
* @param suffix
* the suffix part of the query
*
* @return the output
*/
protected D query(AccessSequenceProvider accessSeqProvider, Word suffix) {
return query(accessSeqProvider.getAccessSequence(), suffix);
}
/**
* Returns the discrimination tree.
*
* @return the discrimination tree
*/
public BaseTTTDiscriminationTree getDiscriminationTree() {
return dtree;
}
protected final AbstractBaseDTNode.SplitResult split(AbstractBaseDTNode node,
Word discriminator,
D oldOutput,
D newOutput) {
return node.split(discriminator, oldOutput, newOutput);
}
private void notifyPreFinalizeDiscriminator(AbstractBaseDTNode blockRoot, Splitter splitter) {
for (TTTEventListener listener : eventListeners()) {
listener.preFinalizeDiscriminator(blockRoot, splitter);
}
}
private void notifyPostFinalizeDiscriminator(AbstractBaseDTNode blockRoot, Splitter splitter) {
for (TTTEventListener listener : eventListeners()) {
listener.postFinalizeDiscriminator(blockRoot, splitter);
}
}
private void notifyPreSplit(TTTTransition transition, Word tempDiscriminator) {
for (TTTEventListener listener : eventListeners()) {
listener.preSplit(transition, tempDiscriminator);
}
}
private void notifyPostSplit(TTTTransition transition, Word tempDiscriminator) {
for (TTTEventListener listener : eventListeners()) {
listener.postSplit(transition, tempDiscriminator);
}
}
private Iterable> eventListeners() {
return eventListeners;
}
public void addEventListener(TTTEventListener listener) {
eventListeners.add(listener);
}
public void removeEventListener(TTTEventListener listener) {
eventListeners.remove(listener);
}
@Override
public void addAlphabetSymbol(I symbol) throws GrowingAlphabetNotSupportedException {
if (!this.alphabet.containsSymbol(symbol)) {
Alphabets.toGrowingAlphabetOrThrowException(this.alphabet).addSymbol(symbol);
}
this.hypothesis.addAlphabetSymbol(symbol);
// check if we already have information about the symbol (then the transition is defined) so we don't post
// redundant queries
if (this.hypothesis.getInitialState() != null &&
this.hypothesis.getSuccessor(this.hypothesis.getInitialState(), symbol) == null) {
final int newSymbolIdx = this.alphabet.getSymbolIndex(symbol);
for (final TTTState s : this.hypothesis.getStates()) {
final TTTTransition trans = createTransition(s, symbol);
trans.setNonTreeTarget(dtree.getRoot());
s.setTransition(newSymbolIdx, trans);
openTransitions.insertIncoming(trans);
}
this.closeTransitions();
}
}
protected abstract AbstractBaseDTNode createNewNode(AbstractBaseDTNode parent, D parentOutput);
@Override
public TTTLearnerState suspend() {
return new TTTLearnerState<>(hypothesis, dtree);
}
@Override
public void resume(final TTTLearnerState state) {
this.hypothesis = state.getHypothesis();
this.hypothesis.setAlphabet(alphabet);
this.dtree = state.getDiscriminationTree();
this.dtree.setOracle(oracle);
}
public static final class BuilderDefaults {
private BuilderDefaults() {
// prevent instantiation
}
public static AcexAnalyzer analyzer() {
return AcexAnalyzers.BINARY_SEARCH_BWD;
}
}
/**
* Data structure for representing a splitter.
*
* A splitter is represented by an input symbol, and a DT node that separates the successors (wrt. the input symbol)
* of the original states. From this, a discriminator can be obtained by prepending the input symbol to the
* discriminator that labels the separating successor.
*
* Note: as the discriminator finalization is applied to the root of a block and affects all nodes, there is
* no need to store references to the source states from which this splitter was obtained.
*
* @param
* input symbol type
*
* @author Malte Isberner
*/
public static final class Splitter {
public final int symbolIdx;
public final AbstractBaseDTNode succSeparator;
public Splitter(int symbolIdx) {
this.symbolIdx = symbolIdx;
this.succSeparator = null;
}
public Splitter(int symbolIdx, AbstractBaseDTNode succSeparator) {
assert !succSeparator.isTemp() && succSeparator.isInner();
this.symbolIdx = symbolIdx;
this.succSeparator = succSeparator;
}
public Word getDiscriminator() {
return (succSeparator != null) ? succSeparator.getDiscriminator() : Word.epsilon();
}
public int getDiscriminatorLength() {
return (succSeparator != null) ? succSeparator.getDiscriminator().length() : 0;
}
}
/**
* A global splitter. In addition to the information stored in a (local) {@link Splitter}, this class also stores
* the block the local splitter applies to.
*
* @param
* input symbol type
*
* @author Malte Isberner
*/
private static final class GlobalSplitter {
public final Splitter localSplitter;
public final AbstractBaseDTNode blockRoot;
GlobalSplitter(AbstractBaseDTNode blockRoot, Splitter localSplitter) {
this.blockRoot = blockRoot;
this.localSplitter = localSplitter;
}
}
/**
* Data structure required during an extract operation. The latter basically works by copying nodes that are
* required in the extracted subtree, and this data structure is required to associate original nodes with their
* extracted copies.
*
* @param
* input symbol type
*
* @author Malte Isberner
*/
private static final class ExtractRecord {
public final AbstractBaseDTNode original;
public final AbstractBaseDTNode extracted;
ExtractRecord(AbstractBaseDTNode original, AbstractBaseDTNode extracted) {
this.original = original;
this.extracted = extracted;
}
}
}