All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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> 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 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 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; } } }