• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LearnLib / learnlib / 31619759710

12 Aug 2026 04:27PM UTC coverage: 95.488% (+1.1%) from 94.368%
31619759710

push

github

mtf90
use new version scheme

15533 of 16267 relevant lines covered (95.49%)

1.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.69
/algorithms/active/ttt/src/main/java/de/learnlib/algorithm/ttt/base/AbstractTTTLearner.java
1
/* Copyright (C) 2013-2026 TU Dortmund University
2
 * This file is part of LearnLib <https://learnlib.de>.
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
package de.learnlib.algorithm.ttt.base;
17

18
import java.util.ArrayDeque;
19
import java.util.ArrayList;
20
import java.util.Collection;
21
import java.util.Collections;
22
import java.util.Deque;
23
import java.util.HashMap;
24
import java.util.HashSet;
25
import java.util.Iterator;
26
import java.util.List;
27
import java.util.Map;
28
import java.util.Objects;
29
import java.util.Set;
30

31
import de.learnlib.AccessSequenceTransformer;
32
import de.learnlib.LearnerStateTracker;
33
import de.learnlib.Resumable;
34
import de.learnlib.acex.AcexAnalyzer;
35
import de.learnlib.acex.AcexAnalyzers;
36
import de.learnlib.acex.OutInconsPrefixTransformAcex;
37
import de.learnlib.algorithm.LearningAlgorithm;
38
import de.learnlib.datastructure.discriminationtree.SplitData;
39
import de.learnlib.datastructure.list.IntrusiveList;
40
import de.learnlib.logging.Category;
41
import de.learnlib.oracle.MembershipOracle;
42
import de.learnlib.query.DefaultQuery;
43
import de.learnlib.query.Query;
44
import net.automatalib.alphabet.Alphabet;
45
import net.automatalib.alphabet.SupportsGrowingAlphabet;
46
import net.automatalib.common.smartcollection.ElementReference;
47
import net.automatalib.common.smartcollection.UnorderedCollection;
48
import net.automatalib.common.util.collection.CollectionUtil;
49
import net.automatalib.word.Word;
50
import org.checkerframework.checker.nullness.qual.Nullable;
51
import org.slf4j.Logger;
52
import org.slf4j.LoggerFactory;
53

54
/**
55
 * The TTT learning algorithm for generic automata.
56
 * <p>
57
 * <b>Implementation note:</b> this learner uses the {@link AccessSequenceTransformer} interface to provide access to
58
 * the representatives of the states of the current hypothesis model.
59
 *
60
 * @param <A>
61
 *         hypothesis automaton type
62
 * @param <I>
63
 *         input symbol type
64
 * @param <D>
65
 *         output domain type
66
 */
67
public abstract class AbstractTTTLearner<A, I, D> implements LearningAlgorithm<A, I, D>,
68
                                                             AccessSequenceTransformer<I>,
69
                                                             SupportsGrowingAlphabet<I>,
70
                                                             Resumable<TTTLearnerState<I, D>>,
71
                                                             LearnerStateTracker {
72

73
    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractTTTLearner.class);
2✔
74

75
    protected final Alphabet<I> alphabet;
76
    protected final MembershipOracle<I, D> oracle;
77
    protected final AcexAnalyzer analyzer;
78
    /**
79
     * Open transitions, i.e., transitions that possibly point to a non-leaf node in the discrimination tree.
80
     */
81
    protected final IntrusiveList<TTTTransition<I, D>> openTransitions = new IntrusiveList<>();
2✔
82
    /**
83
     * The blocks during a split operation. A block is a maximal subtree of the discrimination tree containing temporary
84
     * discriminators at its root.
85
     */
86
    protected final IntrusiveList<AbstractBaseDTNode<I, D>> blockList = new IntrusiveList<>();
2✔
87
    protected AbstractTTTHypothesis<?, I, D, ?> hypothesis;
88
    protected BaseTTTDiscriminationTree<I, D> dtree;
89

90
    protected AbstractTTTLearner(Alphabet<I> alphabet,
91
                                 MembershipOracle<I, D> oracle,
92
                                 AbstractTTTHypothesis<?, I, D, ?> hypothesis,
93
                                 BaseTTTDiscriminationTree<I, D> dtree,
94
                                 AcexAnalyzer analyzer) {
2✔
95
        this.alphabet = alphabet;
2✔
96
        this.hypothesis = hypothesis;
2✔
97
        this.oracle = oracle;
2✔
98
        this.dtree = dtree;
2✔
99
        this.analyzer = analyzer;
2✔
100
    }
2✔
101

102
    /**
103
     * Marks a node, and propagates the label up to all nodes on the path from the block root to this node.
104
     *
105
     * @param node
106
     *         the node to mark
107
     * @param label
108
     *         the label to mark the node with
109
     */
110
    private static <I, D> void markAndPropagate(AbstractBaseDTNode<I, D> node, D label) {
111
        AbstractBaseDTNode<I, D> curr = node;
2✔
112

113
        while (curr != null && curr.getSplitData() != null) {
2✔
114
            if (!curr.getSplitData().mark(label)) {
2✔
115
                return;
2✔
116
            }
117
            curr = curr.getParent();
2✔
118
        }
119
    }
2✔
120

121
    /**
122
     * Moves all transition from the "incoming" list (for a given label) of an old node to the "incoming" list of a new
123
     * node.
124
     *
125
     * @param newNode
126
     *         the new node
127
     * @param oldNode
128
     *         the old node
129
     * @param label
130
     *         the label to consider
131
     * @param <I>
132
     *         input symbol type
133
     * @param <D>
134
     *         output domain type
135
     */
136
    private static <I, D> void moveIncoming(AbstractBaseDTNode<I, D> newNode,
137
                                            AbstractBaseDTNode<I, D> oldNode,
138
                                            D label) {
139
        newNode.getIncoming().concat(oldNode.getSplitData().getIncoming(label));
2✔
140
    }
2✔
141

142
    /**
143
     * Establish the connection between a node in the discrimination tree and a state of the hypothesis.
144
     *
145
     * @param dtNode
146
     *         the node in the discrimination tree
147
     * @param state
148
     *         the state in the hypothesis
149
     * @param <I>
150
     *         input symbol type
151
     * @param <D>
152
     *         output domain type
153
     */
154
    protected static <I, D> void link(AbstractBaseDTNode<I, D> dtNode, TTTState<I, D> state) {
155
        assert dtNode.isLeaf();
2✔
156

157
        dtNode.setData(state);
2✔
158
        state.dtLeaf = dtNode;
2✔
159
    }
2✔
160

161
    /*
162
     * Private helper methods.
163
     */
164

165
    @Override
166
    public void startLearning() {
167
        requireLearningProcessNotStarted();
2✔
168

169
        TTTState<I, D> init = hypothesis.initialize();
2✔
170
        AbstractBaseDTNode<I, D> initNode = dtree.sift(init.getAccessSequence(), false);
2✔
171
        link(initNode, init);
2✔
172
        initializeState(init);
2✔
173

174
        closeTransitions();
2✔
175
    }
2✔
176

177
    @Override
178
    public boolean refineHypothesis(DefaultQuery<I, D> ceQuery) {
179
        requireLearningProcessStarted();
2✔
180

181
        if (!refineHypothesisSingle(ceQuery)) {
2✔
182
            return false;
2✔
183
        }
184

185
        while (refineHypothesisSingle(ceQuery)) {
2✔
186
            // refine exhaustively
187
        }
188

189
        return true;
2✔
190
    }
191

192
    @Override
193
    public boolean hasLearningProcessStarted() {
194
        return hypothesis.isInitialized();
2✔
195
    }
196

197
    /**
198
     * Initializes a state. Creates its outgoing transition objects, and adds them to the "open" list.
199
     *
200
     * @param state
201
     *         the state to initialize
202
     */
203
    protected void initializeState(TTTState<I, D> state) {
204
        TTTTransition<I, D> head = null;
2✔
205
        for (int i = 0; i < alphabet.size(); i++) {
2✔
206
            I sym = alphabet.getSymbol(i);
2✔
207
            TTTTransition<I, D> trans = createTransition(state, sym);
2✔
208
            trans.setNonTreeTarget(dtree.getRoot());
2✔
209
            state.setTransition(i, trans);
2✔
210
            openTransitions.add(trans);
2✔
211
            head = trans;
2✔
212
        }
213
        initTransitions(head, alphabet.size());
2✔
214
    }
2✔
215

216
    protected TTTTransition<I, D> createTransition(TTTState<I, D> state, I sym) {
217
        return new TTTTransition<>(state, sym);
2✔
218
    }
219

220
    /**
221
     * A post-processing hook for transitions created by {@link #createTransition(TTTState, Object)}, e.g., after
222
     * {@link #initializeState(TTTState)} or {@link #addAlphabetSymbol(Object)}. This is mainly useful for transition
223
     * output hypotheses that want to initialize the transition outputs in a bulk operation.
224
     *
225
     * @param head
226
     *         the head of (the list of) the created transitions
227
     * @param num
228
     *         the number of created transitions
229
     */
230
    protected void initTransitions(TTTTransition<I, D> head, int num) {}
2✔
231

232
    /**
233
     * Performs a single refinement of the hypothesis, i.e., without repeated counterexample evaluation. The parameter
234
     * and return value have the same significance as in {@link #refineHypothesis(DefaultQuery)}.
235
     *
236
     * @param ceQuery
237
     *         the counterexample (query) to be used for refinement
238
     *
239
     * @return {@code true} if the hypothesis was refined, {@code false} otherwise
240
     */
241
    protected boolean refineHypothesisSingle(DefaultQuery<I, D> ceQuery) {
242
        TTTState<I, D> state = getAnyState(ceQuery.getPrefix());
2✔
243
        D out = computeHypothesisOutput(state, ceQuery.getSuffix());
2✔
244

245
        if (Objects.equals(out, ceQuery.getOutput())) {
2✔
246
            return false;
2✔
247
        }
248

249
        OutputInconsistency<I, D> outIncons =
2✔
250
                new OutputInconsistency<>(state, ceQuery.getSuffix(), ceQuery.getOutput());
2✔
251

252
        do {
253
            splitState(outIncons);
2✔
254
            do {
255
                closeTransitions();
2✔
256
            } while (finalizeAny());
2✔
257

258
            outIncons = findOutputInconsistency();
2✔
259
        } while (outIncons != null);
2✔
260
        assert allNodesFinal();
2✔
261

262
        return true;
2✔
263
    }
264

265
    /**
266
     * Splits a state in the hypothesis, using a temporary discriminator. The state to be split is identified by an
267
     * incoming non-tree transition. This transition is subsequently turned into a spanning tree transition.
268
     *
269
     * @param transition
270
     *         the transition
271
     * @param tempDiscriminator
272
     *         the temporary discriminator
273
     */
274
    private void splitState(TTTTransition<I, D> transition, Word<I> tempDiscriminator, D oldOut, D newOut) {
275
        assert !transition.isTree();
2✔
276

277
        AbstractBaseDTNode<I, D> dtNode = transition.getNonTreeTarget();
2✔
278
        assert dtNode.isLeaf();
2✔
279
        TTTState<I, D> oldState = dtNode.getData();
2✔
280
        assert oldState != null;
2✔
281

282
        TTTState<I, D> newState = makeTree(transition);
2✔
283

284
        AbstractBaseDTNode<I, D>.SplitResult children = dtNode.split(tempDiscriminator, oldOut, newOut);
2✔
285
        dtNode.setTemp(true);
2✔
286

287
        link(children.nodeOld, oldState);
2✔
288
        link(children.nodeNew, newState);
2✔
289

290
        if (dtNode.getParent() == null || !dtNode.getParent().isTemp()) {
2✔
291
            blockList.add(dtNode);
2✔
292
        }
293
    }
2✔
294

295
    private void splitState(OutputInconsistency<I, D> outIncons) {
296

297
        OutInconsPrefixTransformAcex<I, D> acex = deriveAcex(outIncons);
2✔
298
        try {
299
            int breakpoint = analyzer.analyzeAbstractCounterexample(acex);
2✔
300
            assert !acex.testEffects(breakpoint, breakpoint + 1);
2✔
301

302
            Word<I> suffix = outIncons.suffix;
2✔
303

304
            TTTState<I, D> predState = getDeterministicState(outIncons.srcState, suffix.prefix(breakpoint));
2✔
305
            TTTState<I, D> succState = getDeterministicState(outIncons.srcState, suffix.prefix(breakpoint + 1));
2✔
306
            assert getDeterministicState(predState, Word.fromLetter(suffix.getSymbol(breakpoint))) == succState;
2✔
307

308
            I sym = suffix.getSymbol(breakpoint);
2✔
309
            Word<I> splitSuffix = suffix.subWord(breakpoint + 1);
2✔
310
            TTTTransition<I, D> trans = predState.getTransition(alphabet.getSymbolIndex(sym));
2✔
311
            assert !trans.isTree();
2✔
312
            D oldOut = acex.effect(breakpoint + 1);
2✔
313
            D newOut = succEffect(acex.effect(breakpoint));
2✔
314

315
            splitState(trans, splitSuffix, oldOut, newOut);
2✔
316
        } catch (HypothesisChangedException ignored) {
1✔
317
            // ignore
318
        }
2✔
319
    }
2✔
320

321
    protected OutInconsPrefixTransformAcex<I, D> deriveAcex(OutputInconsistency<I, D> outIncons) {
322
        TTTState<I, D> source = outIncons.srcState;
2✔
323
        Word<I> suffix = outIncons.suffix;
2✔
324

325
        OutInconsPrefixTransformAcex<I, D> acex = new OutInconsPrefixTransformAcex<>(suffix,
2✔
326
                                                                                     oracle,
327
                                                                                     w -> getDeterministicState(source,
2✔
328
                                                                                                                w).getAccessSequence());
2✔
329

330
        acex.setEffect(0, outIncons.targetOut);
2✔
331
        return acex;
2✔
332
    }
333

334
    protected abstract D succEffect(D effect);
335

336
    /**
337
     * Chooses a block root, and finalizes the corresponding discriminator.
338
     *
339
     * @return {@code true} if a splittable block root was found, {@code false} otherwise.
340
     */
341
    protected boolean finalizeAny() {
342
        GlobalSplitter<I, D> splitter = findSplitterGlobal();
2✔
343
        if (splitter != null) {
2✔
344
            finalizeDiscriminator(splitter.blockRoot, splitter.localSplitter);
2✔
345
            return true;
2✔
346
        }
347
        return false;
2✔
348
    }
349

350
    protected TTTState<I, D> getDeterministicState(TTTState<I, D> start, Word<I> word) {
351
        TTTState<I, D> lastSingleton = start;
2✔
352
        int lastSingletonIndex = 0;
2✔
353

354
        Set<TTTState<I, D>> states = Collections.singleton(start);
2✔
355
        int i = 1;
2✔
356
        for (I sym : word) {
2✔
357
            Set<TTTState<I, D>> nextStates = getNonDetSuccessors(states, sym);
2✔
358
            if (nextStates.size() == 1) {
2✔
359
                lastSingleton = nextStates.iterator().next();
2✔
360
                lastSingletonIndex = i;
2✔
361
            }
362
            states = nextStates;
2✔
363

364
            i++;
2✔
365
        }
2✔
366
        if (lastSingletonIndex == word.length()) {
2✔
367
            return lastSingleton;
2✔
368
        }
369

370
        TTTState<I, D> curr = lastSingleton;
2✔
371
        for (I sym : word.subWord(lastSingletonIndex)) {
2✔
372
            TTTTransition<I, D> trans = curr.getTransition(alphabet.getSymbolIndex(sym));
2✔
373
            curr = requireSuccessor(trans);
2✔
374
        }
2✔
375

376
        return curr;
2✔
377
    }
378

379
    protected Set<TTTState<I, D>> getNonDetSuccessors(Collection<? extends TTTState<I, D>> states, I sym) {
380
        Set<TTTState<I, D>> result = new HashSet<>();
2✔
381
        int symIdx = alphabet.getSymbolIndex(sym);
2✔
382
        for (TTTState<I, D> state : states) {
2✔
383
            TTTTransition<I, D> trans = state.getTransition(symIdx);
2✔
384
            if (trans.isTree()) {
2✔
385
                result.add(trans.getTreeTarget());
2✔
386
            } else {
387
                AbstractBaseDTNode<I, D> tgtNode = trans.getNonTreeTarget();
2✔
388
                CollectionUtil.add(result, tgtNode.subtreeStatesIterator());
2✔
389
            }
390
        }
2✔
391
        return result;
2✔
392
    }
393

394
    protected TTTState<I, D> getAnySuccessor(TTTState<I, D> state, I sym) {
395
        int symIdx = alphabet.getSymbolIndex(sym);
2✔
396
        TTTTransition<I, D> trans = state.getTransition(symIdx);
2✔
397
        if (trans.isTree()) {
2✔
398
            return trans.getTreeTarget();
2✔
399
        }
400
        return trans.getNonTreeTarget().subtreeStatesIterator().next();
2✔
401
    }
402

403
    protected TTTState<I, D> getAnySuccessor(TTTState<I, D> state, Iterable<? extends I> suffix) {
404
        TTTState<I, D> curr = state;
2✔
405
        for (I sym : suffix) {
2✔
406
            curr = getAnySuccessor(curr, sym);
2✔
407
        }
2✔
408
        return curr;
2✔
409
    }
410

411
    private TTTState<I, D> requireSuccessor(TTTTransition<I, D> trans) {
412
        if (trans.isTree()) {
2✔
413
            return trans.getTreeTarget();
2✔
414
        }
415
        AbstractBaseDTNode<I, D> newTgtNode = updateDTTarget(trans, true);
2✔
416
        if (newTgtNode.getData() == null) {
2✔
417
            makeTree(trans);
1✔
418
            closeTransitions();
1✔
419
            // FIXME: using exception handling for this is not very nice, but it appears there
420
            // is no quicker way to abort counterexample analysis
421
            throw new HypothesisChangedException();
1✔
422
        }
423
        return newTgtNode.getData();
2✔
424
    }
425

426
    /**
427
     * Determines a global splitter, i.e., a splitter for any block. This method may (but is not required to) employ
428
     * heuristics to obtain a splitter with a relatively short suffix length.
429
     *
430
     * @return a splitter for any of the blocks
431
     */
432
    private @Nullable GlobalSplitter<I, D> findSplitterGlobal() {
433
        // TODO: Make global option
434
        boolean optimizeGlobal = true;
2✔
435

436
        AbstractBaseDTNode<I, D> bestBlockRoot = null;
2✔
437

438
        Splitter<I, D> bestSplitter = null;
2✔
439

440
        for (AbstractBaseDTNode<I, D> blockRoot : blockList) {
2✔
441
            Splitter<I, D> splitter = findSplitter(blockRoot);
2✔
442

443
            if (splitter != null) {
2✔
444
                if (bestSplitter == null || splitter.getDiscriminatorLength() < bestSplitter.getDiscriminatorLength()) {
2✔
445
                    bestSplitter = splitter;
2✔
446
                    bestBlockRoot = blockRoot;
2✔
447
                }
448

449
                if (!optimizeGlobal) {
2✔
450
                    break;
×
451
                }
452
            }
453
        }
2✔
454

455
        if (bestSplitter == null) {
2✔
456
            return null;
2✔
457
        }
458

459
        return new GlobalSplitter<>(bestBlockRoot, bestSplitter);
2✔
460
    }
461

462
    /**
463
     * Determines a (local) splitter for a given block. This method may (but is not required to) employ heuristics to
464
     * obtain a splitter with a relatively short suffix.
465
     *
466
     * @param blockRoot
467
     *         the root of the block
468
     *
469
     * @return a splitter for this block, or {@code null} if no such splitter could be found.
470
     */
471
    private @Nullable Splitter<I, D> findSplitter(AbstractBaseDTNode<I, D> blockRoot) {
472
        int alphabetSize = alphabet.size();
2✔
473

474
        @Nullable Object[] properties = new Object[alphabetSize];
2✔
475
        @SuppressWarnings("unchecked")
476
        AbstractBaseDTNode<I, D>[] lcas = new AbstractBaseDTNode[alphabetSize];
2✔
477
        boolean first = true;
2✔
478

479
        for (TTTState<I, D> state : blockRoot.subtreeStates()) {
2✔
480
            for (int i = 0; i < alphabetSize; i++) {
2✔
481
                TTTTransition<I, D> trans = state.getTransition(i);
2✔
482
                if (first) {
2✔
483
                    properties[i] = trans.getProperty();
2✔
484
                    lcas[i] = trans.getDTTarget();
2✔
485
                } else {
486
                    if (!Objects.equals(properties[i], trans.getProperty())) {
2✔
487
                        return new Splitter<>(i);
2✔
488
                    }
489
                    lcas[i] = dtree.leastCommonAncestor(lcas[i], trans.getDTTarget());
2✔
490
                }
491
            }
492
            first = false;
2✔
493
        }
2✔
494

495
        int shortestLen = Integer.MAX_VALUE;
2✔
496
        AbstractBaseDTNode<I, D> shortestLca = null;
2✔
497
        int shortestLcaSym = -1;
2✔
498

499
        for (int i = 0; i < alphabetSize; i++) {
2✔
500
            AbstractBaseDTNode<I, D> lca = lcas[i];
2✔
501
            if (!lca.isTemp() && !lca.isLeaf()) {
2✔
502
                int lcaLen = lca.getDiscriminator().length();
2✔
503
                if (shortestLca == null || lcaLen < shortestLen) {
2✔
504
                    shortestLca = lca;
2✔
505
                    shortestLen = lcaLen;
2✔
506
                    shortestLcaSym = i;
2✔
507
                }
508
            }
509
        }
510

511
        if (shortestLca != null) {
2✔
512
            return new Splitter<>(shortestLcaSym, shortestLca);
2✔
513
        }
514
        return null;
2✔
515
    }
516

517
    /**
518
     * Creates a state in the hypothesis. This method cannot be used for the initial state, which has no incoming tree
519
     * transition.
520
     *
521
     * @param transition
522
     *         the "parent" transition in the spanning tree
523
     *
524
     * @return the newly created state
525
     */
526
    private TTTState<I, D> createState(TTTTransition<I, D> transition) {
527
        return hypothesis.createState(transition);
2✔
528
    }
529

530
    /**
531
     * Retrieves the target state of a given transition. This method works for both tree and non-tree transitions. If a
532
     * non-tree transition points to a non-leaf node, it is updated accordingly before a result is obtained.
533
     *
534
     * @param trans
535
     *         the transition
536
     *
537
     * @return the target state of this transition (possibly after it having been updated)
538
     */
539
    protected TTTState<I, D> getAnyTarget(TTTTransition<I, D> trans) {
540
        if (trans.isTree()) {
2✔
541
            return trans.getTreeTarget();
2✔
542
        }
543
        return trans.getNonTreeTarget().anySubtreeState();
2✔
544
    }
545

546
    /**
547
     * Retrieves the state reached by the given sequence of symbols, starting from the initial state.
548
     *
549
     * @param suffix
550
     *         the sequence of symbols to process
551
     *
552
     * @return the state reached after processing the specified symbols
553
     */
554
    private TTTState<I, D> getAnyState(Iterable<? extends I> suffix) {
555
        return getAnySuccessor(hypothesis.getInitialState(), suffix);
2✔
556
    }
557

558
    protected OutputInconsistency<I, D> findOutputInconsistency() {
559
        OutputInconsistency<I, D> best = null;
2✔
560

561
        for (TTTState<I, D> state : hypothesis.getStates()) {
2✔
562
            AbstractBaseDTNode<I, D> node = state.getDTLeaf();
2✔
563
            while (!node.isRoot()) {
2✔
564
                D expectedOut = node.getParentOutcome();
2✔
565
                node = node.getParent();
2✔
566
                Word<I> suffix = node.getDiscriminator();
2✔
567
                if (best == null || suffix.length() < best.suffix.length()) {
2✔
568
                    D hypOut = computeHypothesisOutput(state, suffix);
2✔
569
                    if (!Objects.equals(hypOut, expectedOut)) {
2✔
570
                        best = new OutputInconsistency<>(state, suffix, expectedOut);
2✔
571
                    }
572
                }
573
            }
2✔
574
        }
2✔
575
        return best;
2✔
576
    }
577

578
    /**
579
     * Finalize a discriminator. Given a block root and a {@link Splitter}, replace the discriminator at the block root
580
     * by the one derived from the splitter, and update the discrimination tree accordingly.
581
     *
582
     * @param blockRoot
583
     *         the block root whose discriminator to finalize
584
     * @param splitter
585
     *         the splitter to use for finalization
586
     */
587
    private void finalizeDiscriminator(AbstractBaseDTNode<I, D> blockRoot, Splitter<I, D> splitter) {
588
        assert blockRoot.isBlockRoot();
2✔
589

590
        Word<I> succDiscr = splitter.getDiscriminator().prepend(alphabet.getSymbol(splitter.symbolIdx));
2✔
591

592
        if (!blockRoot.getDiscriminator().equals(succDiscr)) {
2✔
593
            Word<I> finalDiscriminator = prepareSplit(blockRoot, splitter);
2✔
594
            Map<D, AbstractBaseDTNode<I, D>> repChildren = createMap();
2✔
595
            for (D label : blockRoot.getSplitData().getLabels()) {
2✔
596
                repChildren.put(label, extractSubtree(blockRoot, label));
2✔
597
            }
2✔
598
            blockRoot.replaceChildren(repChildren);
2✔
599

600
            blockRoot.setDiscriminator(finalDiscriminator);
2✔
601
        }
602

603
        declareFinal(blockRoot);
2✔
604
    }
2✔
605

606
    protected boolean allNodesFinal() {
607
        Iterator<AbstractBaseDTNode<I, D>> it = dtree.getRoot().subtreeNodesIterator();
2✔
608
        while (it.hasNext()) {
2✔
609
            AbstractBaseDTNode<I, D> node = it.next();
2✔
610
            assert !node.isTemp() : "Final node with discriminator " + node.getDiscriminator();
2✔
611
        }
2✔
612
        return true;
2✔
613
    }
614

615
    protected void declareFinal(AbstractBaseDTNode<I, D> blockRoot) {
616
        blockRoot.setTemp(false);
2✔
617
        blockRoot.setSplitData(null);
2✔
618

619
        blockRoot.removeFromList();
2✔
620

621
        for (AbstractBaseDTNode<I, D> subtree : blockRoot.getChildren()) {
2✔
622
            assert subtree.getSplitData() == null;
2✔
623
            blockRoot.setChild(subtree.getParentOutcome(), subtree);
2✔
624
            // Register as blocks, if they are non-trivial subtrees
625
            if (subtree.isInner()) {
2✔
626
                blockList.add(subtree);
2✔
627
            }
628
        }
2✔
629
        openTransitions.concat(blockRoot.getIncoming());
2✔
630
    }
2✔
631

632
    /**
633
     * Prepare a split operation on a block, by marking all the nodes and transitions in the subtree (and annotating
634
     * them with {@link SplitData} objects).
635
     *
636
     * @param node
637
     *         the block root to be split
638
     * @param splitter
639
     *         the splitter to use for splitting the block
640
     *
641
     * @return the discriminator to use for splitting
642
     */
643
    private Word<I> prepareSplit(AbstractBaseDTNode<I, D> node, Splitter<I, D> splitter) {
644
        int symbolIdx = splitter.symbolIdx;
2✔
645
        I symbol = alphabet.getSymbol(symbolIdx);
2✔
646
        Word<I> discriminator = splitter.getDiscriminator().prepend(symbol);
2✔
647

648
        Deque<AbstractBaseDTNode<I, D>> dfsStack = new ArrayDeque<>();
2✔
649
        List<SplitQuery<I, D>> queries = new ArrayList<>();
2✔
650

651
        AbstractBaseDTNode<I, D> succSeparator = splitter.succSeparator;
2✔
652

653
        dfsStack.push(node);
2✔
654
        assert node.getSplitData() == null;
2✔
655

656
        while (!dfsStack.isEmpty()) {
2✔
657
            AbstractBaseDTNode<I, D> curr = dfsStack.pop();
2✔
658
            assert curr.getSplitData() == null;
2✔
659

660
            curr.setSplitData(new SplitData<>(IntrusiveList::new));
2✔
661

662
            for (TTTTransition<I, D> trans : curr.getIncoming()) {
2✔
663
                queries.add(new SplitQuery<>(trans, discriminator));
2✔
664
            }
2✔
665

666
            if (!queries.isEmpty()) {
2✔
667
                oracle.processQueries(queries);
2✔
668

669
                for (SplitQuery<I, D> query : queries) {
2✔
670
                    curr.getSplitData().getIncoming(query.output).add(query.transition);
2✔
671
                    markAndPropagate(curr, query.output);
2✔
672
                }
2✔
673

674
                queries.clear();
2✔
675
            }
676

677
            if (curr.isInner()) {
2✔
678
                for (AbstractBaseDTNode<I, D> child : curr.getChildren()) {
2✔
679
                    dfsStack.push(child);
2✔
680
                }
2✔
681
            } else {
682
                TTTState<I, D> state = curr.getData();
2✔
683
                assert state != null;
2✔
684

685
                TTTTransition<I, D> trans = state.getTransition(symbolIdx);
2✔
686
                D outcome = predictSuccOutcome(trans, succSeparator);
2✔
687
                assert outcome != null;
2✔
688
                curr.getSplitData().setStateLabel(outcome);
2✔
689
                markAndPropagate(curr, outcome);
2✔
690
            }
691

692
        }
2✔
693

694
        return discriminator;
2✔
695
    }
696

697
    protected abstract D predictSuccOutcome(TTTTransition<I, D> trans, AbstractBaseDTNode<I, D> succSeparator);
698

699
    /**
700
     * Extract a (reduced) subtree containing all nodes with the given label from the subtree given by its root.
701
     * "Reduced" here refers to the fact that the resulting subtree will contain no inner nodes with only one child.
702
     * <p>
703
     * The tree returned by this method (represented by its root) will have as a parent node the root that was passed to
704
     * this method.
705
     *
706
     * @param root
707
     *         the root of the subtree from which to extract
708
     * @param label
709
     *         the label of the nodes to extract
710
     *
711
     * @return the extracted subtree
712
     */
713
    private AbstractBaseDTNode<I, D> extractSubtree(AbstractBaseDTNode<I, D> root, D label) {
714
        assert root.getSplitData() != null;
2✔
715
        assert root.getSplitData().isMarked(label);
2✔
716

717
        Deque<ExtractRecord<I, D>> stack = new ArrayDeque<>();
2✔
718

719
        AbstractBaseDTNode<I, D> firstExtracted = createNewNode(root, label);
2✔
720

721
        stack.push(new ExtractRecord<>(root, firstExtracted));
2✔
722
        while (!stack.isEmpty()) {
2✔
723
            ExtractRecord<I, D> curr = stack.pop();
2✔
724

725
            AbstractBaseDTNode<I, D> original = curr.original;
2✔
726
            AbstractBaseDTNode<I, D> extracted = curr.extracted;
2✔
727

728
            moveIncoming(extracted, original, label);
2✔
729

730
            if (original.isLeaf()) {
2✔
731
                if (Objects.equals(original.getSplitData().getStateLabel(), label)) {
2✔
732
                    link(extracted, original.getData());
2✔
733
                } else {
734
                    createNewState(extracted);
×
735
                }
736
                extracted.updateIncoming();
2✔
737
            } else {
738
                List<AbstractBaseDTNode<I, D>> markedChildren = new ArrayList<>();
2✔
739

740
                for (AbstractBaseDTNode<I, D> child : original.getChildren()) {
2✔
741
                    if (child.getSplitData().isMarked(label)) {
2✔
742
                        markedChildren.add(child);
2✔
743
                    }
744
                }
2✔
745

746
                if (markedChildren.size() > 1) {
2✔
747
                    Map<D, AbstractBaseDTNode<I, D>> childMap = createMap();
2✔
748
                    for (AbstractBaseDTNode<I, D> c : markedChildren) {
2✔
749
                        D childLabel = c.getParentOutcome();
2✔
750
                        AbstractBaseDTNode<I, D> extractedChild = createNewNode(extracted, childLabel);
2✔
751
                        childMap.put(childLabel, extractedChild);
2✔
752
                        stack.push(new ExtractRecord<>(c, extractedChild));
2✔
753
                    }
2✔
754
                    extracted.setDiscriminator(original.getDiscriminator());
2✔
755
                    extracted.replaceChildren(childMap);
2✔
756
                    extracted.updateIncoming();
2✔
757
                    extracted.setTemp(true);
2✔
758
                } else if (markedChildren.size() == 1) {
2✔
759
                    stack.push(new ExtractRecord<>(markedChildren.get(0), extracted));
2✔
760
                } else { // markedChildren.isEmpty()
761
                    createNewState(extracted);
2✔
762
                    extracted.updateIncoming();
2✔
763
                }
764
            }
765

766
            assert extracted.getSplitData() == null;
2✔
767
        }
2✔
768

769
        return firstExtracted;
2✔
770
    }
771

772
    protected <V> Map<D, V> createMap() {
773
        return new HashMap<>();
2✔
774
    }
775

776
    /**
777
     * Create a new state during extraction on-the-fly. This is required if a node in the DT has an incoming transition
778
     * with a certain label, but in its subtree there are no leaves with this label as their state label.
779
     *
780
     * @param newNode
781
     *         the extracted node
782
     */
783
    private void createNewState(AbstractBaseDTNode<I, D> newNode) {
784
        TTTTransition<I, D> newTreeTrans = newNode.getIncoming().choose();
2✔
785
        assert newTreeTrans != null;
2✔
786

787
        TTTState<I, D> newState = createState(newTreeTrans);
2✔
788
        link(newNode, newState);
2✔
789
        initializeState(newState);
2✔
790
    }
2✔
791

792
    protected abstract D computeHypothesisOutput(TTTState<I, D> state, Word<I> suffix);
793

794
    public AbstractTTTHypothesis<?, I, D, ?> getHypothesisDS() {
795
        requireLearningProcessStarted();
2✔
796
        return hypothesis;
2✔
797
    }
798

799
    protected void closeTransitions() {
800
        UnorderedCollection<AbstractBaseDTNode<I, D>> newStateNodes = new UnorderedCollection<>();
2✔
801

802
        do {
803
            newStateNodes.addAll(closeTransitions(openTransitions, false));
2✔
804
            if (!newStateNodes.isEmpty()) {
2✔
805
                addNewStates(newStateNodes);
2✔
806
            }
807
        } while (!openTransitions.isEmpty());
2✔
808
    }
2✔
809

810
    /**
811
     * Ensures that the specified transitions point to a leaf-node. If a transition is a tree transition, this method
812
     * has no effect.
813
     * <p>
814
     * The provided transList is consumed in this process.
815
     * <p>
816
     * If a transition needs sifting, the reached leaf node will be collected in the returned collection.
817
     *
818
     * @param transList
819
     *         the list of transitions
820
     *
821
     * @return a collection containing the reached leaves of transitions that needed sifting
822
     */
823
    private List<AbstractBaseDTNode<I, D>> closeTransitions(IntrusiveList<TTTTransition<I, D>> transList,
824
                                                            boolean hard) {
825

826
        final List<TTTTransition<I, D>> transToSift = new ArrayList<>(transList.size());
2✔
827

828
        TTTTransition<I, D> t;
829
        while ((t = transList.poll()) != null) {
2✔
830
            if (!t.isTree()) {
2✔
831
                transToSift.add(t);
2✔
832
            }
833
        }
834

835
        if (transToSift.isEmpty()) {
2✔
836
            return Collections.emptyList();
2✔
837
        }
838

839
        final Iterator<AbstractBaseDTNode<I, D>> leavesIter = updateDTTargets(transToSift, hard).iterator();
2✔
840
        final List<AbstractBaseDTNode<I, D>> result = new ArrayList<>(transToSift.size());
2✔
841

842
        for (TTTTransition<I, D> transition : transToSift) {
2✔
843
            final AbstractBaseDTNode<I, D> node = leavesIter.next();
2✔
844
            if (node.isLeaf() && node.getData() == null && transition.getNext() == null) {
2✔
845
                result.add(node);
2✔
846
            }
847
        }
2✔
848

849
        assert !leavesIter.hasNext();
2✔
850
        return result;
2✔
851
    }
852

853
    private void addNewStates(UnorderedCollection<AbstractBaseDTNode<I, D>> newStateNodes) {
854
        AbstractBaseDTNode<I, D> minTransNode = null;
2✔
855
        TTTTransition<I, D> minTrans = null;
2✔
856
        int minAsLen = Integer.MAX_VALUE;
2✔
857
        ElementReference minTransNodeRef = null;
2✔
858
        for (ElementReference ref : newStateNodes.references()) {
2✔
859
            AbstractBaseDTNode<I, D> newStateNode = newStateNodes.get(ref);
2✔
860
            for (TTTTransition<I, D> trans : newStateNode.getIncoming()) {
2✔
861
                Word<I> as = trans.getAccessSequence();
2✔
862
                int asLen = as.length();
2✔
863
                if (asLen < minAsLen) {
2✔
864
                    minTransNode = newStateNode;
2✔
865
                    minTrans = trans;
2✔
866
                    minAsLen = asLen;
2✔
867
                    minTransNodeRef = ref;
2✔
868
                }
869
            }
2✔
870
        }
2✔
871

872
        assert minTransNode != null;
2✔
873
        newStateNodes.remove(minTransNodeRef);
2✔
874
        TTTState<I, D> state = makeTree(minTrans);
2✔
875
        link(minTransNode, state);
2✔
876
        initializeState(state);
2✔
877
    }
2✔
878

879
    protected TTTState<I, D> makeTree(TTTTransition<I, D> trans) {
880
        assert !trans.isTree();
2✔
881
        AbstractBaseDTNode<I, D> node = trans.nonTreeTarget;
2✔
882
        assert node.isLeaf();
2✔
883
        TTTState<I, D> state = createState(trans);
2✔
884
        trans.removeFromList();
2✔
885
        link(node, state);
2✔
886
        initializeState(state);
2✔
887
        return state;
2✔
888
    }
889

890
    /**
891
     * Updates the transition to point to either a leaf in the discrimination tree, or---if the {@code hard} parameter
892
     * is set to {@code false}---to a block root.
893
     *
894
     * @param transition
895
     *         the transition
896
     * @param hard
897
     *         whether to consider leaves as sufficient targets only
898
     *
899
     * @return the new target node of the transition
900
     */
901
    private AbstractBaseDTNode<I, D> updateDTTarget(TTTTransition<I, D> transition, boolean hard) {
902
        if (transition.isTree()) {
2✔
903
            return transition.getTreeTarget().dtLeaf;
×
904
        }
905

906
        AbstractBaseDTNode<I, D> dt = transition.getNonTreeTarget();
2✔
907
        dt = dtree.sift(dt, transition.getAccessSequence(), hard);
2✔
908
        transition.setNonTreeTarget(dt);
2✔
909

910
        return dt;
2✔
911
    }
912

913
    /**
914
     * Bulk version of {@link #updateDTTarget(TTTTransition, boolean)}.
915
     */
916
    private List<AbstractBaseDTNode<I, D>> updateDTTargets(List<TTTTransition<I, D>> transitions, boolean hard) {
917

918
        final List<AbstractBaseDTNode<I, D>> nodes = new ArrayList<>(transitions.size());
2✔
919
        final List<Word<I>> prefixes = new ArrayList<>(transitions.size());
2✔
920

921
        for (TTTTransition<I, D> t : transitions) {
2✔
922
            if (!t.isTree()) {
2✔
923
                AbstractBaseDTNode<I, D> dt = t.getNonTreeTarget();
2✔
924

925
                nodes.add(dt);
2✔
926
                prefixes.add(t.getAccessSequence());
2✔
927
            }
928
        }
2✔
929

930
        final Iterator<AbstractBaseDTNode<I, D>> leavesIter = dtree.sift(nodes, prefixes, hard).iterator();
2✔
931
        final List<AbstractBaseDTNode<I, D>> result = new ArrayList<>(transitions.size());
2✔
932

933
        for (TTTTransition<I, D> t : transitions) {
2✔
934
            if (t.isTree()) {
2✔
935
                result.add(t.getTreeTarget().dtLeaf);
×
936
            } else {
937
                AbstractBaseDTNode<I, D> leaf = leavesIter.next();
2✔
938
                t.setNonTreeTarget(leaf);
2✔
939
                result.add(leaf);
2✔
940
            }
941
        }
2✔
942

943
        assert !leavesIter.hasNext();
2✔
944
        return result;
2✔
945
    }
946

947
    /**
948
     * Returns the discrimination tree.
949
     *
950
     * @return the discrimination tree
951
     */
952
    public BaseTTTDiscriminationTree<I, D> getDiscriminationTree() {
953
        return dtree;
2✔
954
    }
955

956
    @Override
957
    public Word<I> transformAccessSequence(Word<I> word) {
958
        requireLearningProcessStarted();
2✔
959
        final TTTState<I, D> s = hypothesis.getState(word);
2✔
960
        // we should only query defined paths
961
        assert s != null;
2✔
962
        return s.getAccessSequence();
2✔
963
    }
964

965
    @Override
966
    public void addAlphabetSymbol(I symbol) {
967

968
        if (!this.alphabet.containsSymbol(symbol)) {
2✔
969
            this.alphabet.asGrowingAlphabetOrThrowException().addSymbol(symbol);
2✔
970
        }
971

972
        this.hypothesis.addAlphabetSymbol(symbol);
2✔
973

974
        // check if we already have information about the symbol (then the transition is defined) so we don't post
975
        // redundant queries
976
        if (this.hypothesis.getInitialState() != null && this.hypothesis.getState(Word.fromLetter(symbol)) == null) {
2✔
977

978
            final int newSymbolIdx = this.alphabet.getSymbolIndex(symbol);
2✔
979
            TTTTransition<I, D> head = null;
2✔
980

981
            for (TTTState<I, D> s : this.hypothesis.getStates()) {
2✔
982
                final TTTTransition<I, D> trans = createTransition(s, symbol);
2✔
983
                trans.setNonTreeTarget(dtree.getRoot());
2✔
984
                s.setTransition(newSymbolIdx, trans);
2✔
985
                openTransitions.add(trans);
2✔
986
                head = trans;
2✔
987
            }
2✔
988

989
            this.initTransitions(head, this.hypothesis.size());
2✔
990
            this.closeTransitions();
2✔
991
        }
992
    }
2✔
993

994
    protected abstract AbstractBaseDTNode<I, D> createNewNode(AbstractBaseDTNode<I, D> parent, D parentOutput);
995

996
    @Override
997
    public TTTLearnerState<I, D> suspend() {
998
        return new TTTLearnerState<>(hypothesis, dtree);
2✔
999
    }
1000

1001
    @Override
1002
    public void resume(TTTLearnerState<I, D> state) {
1003
        this.hypothesis = state.getHypothesis();
2✔
1004
        this.dtree = state.getDiscriminationTree();
2✔
1005
        this.dtree.setOracle(oracle);
2✔
1006

1007
        final Alphabet<I> oldAlphabet = this.hypothesis.getInputAlphabet();
2✔
1008
        if (!oldAlphabet.equals(this.alphabet)) {
2✔
1009
            LOGGER.warn(Category.DATASTRUCTURE,
×
1010
                        "The current alphabet '{}' differs from the resumed alphabet '{}'. Future behavior may be inconsistent",
1011
                        this.alphabet,
1012
                        oldAlphabet);
1013
        }
1014
    }
2✔
1015

1016
    public static final class BuilderDefaults {
1017

1018
        private BuilderDefaults() {
1019
            // prevent instantiation
1020
        }
1021

1022
        public static AcexAnalyzer analyzer() {
1023
            return AcexAnalyzers.BINARY_SEARCH_BWD;
2✔
1024
        }
1025
    }
1026

1027
    /**
1028
     * Data structure for representing a splitter.
1029
     * <p>
1030
     * A splitter is represented by an input symbol, and a DT node that separates the successors (wrt. the input symbol)
1031
     * of the original states. From this, a discriminator can be obtained by prepending the input symbol to the
1032
     * discriminator that labels the separating successor.
1033
     * <p>
1034
     * <b>Note:</b> as the discriminator finalization is applied to the root of a block and affects all nodes, there is
1035
     * no need to store references to the source states from which this splitter was obtained.
1036
     *
1037
     * @param <I>
1038
     *         input symbol type
1039
     */
1040
    public static final class Splitter<I, D> {
2✔
1041

1042
        public final int symbolIdx;
1043
        public final AbstractBaseDTNode<I, D> succSeparator;
1044

1045
        public Splitter(int symbolIdx) {
2✔
1046
            this.symbolIdx = symbolIdx;
2✔
1047
            this.succSeparator = null;
2✔
1048
        }
2✔
1049

1050
        public Splitter(int symbolIdx, AbstractBaseDTNode<I, D> succSeparator) {
2✔
1051
            assert !succSeparator.isTemp() && succSeparator.isInner();
2✔
1052

1053
            this.symbolIdx = symbolIdx;
2✔
1054
            this.succSeparator = succSeparator;
2✔
1055
        }
2✔
1056

1057
        public Word<I> getDiscriminator() {
1058
            return (succSeparator != null) ? succSeparator.getDiscriminator() : Word.epsilon();
2✔
1059
        }
1060

1061
        public int getDiscriminatorLength() {
1062
            return (succSeparator != null) ? succSeparator.getDiscriminator().length() : 0;
×
1063
        }
1064
    }
1065

1066
    /**
1067
     * A global splitter. In addition to the information stored in a (local) {@link Splitter}, this class also stores
1068
     * the block the local splitter applies to.
1069
     *
1070
     * @param <I>
1071
     *         input symbol type
1072
     */
1073
    private static final class GlobalSplitter<I, D> {
1074

1075
        private final Splitter<I, D> localSplitter;
1076
        private final AbstractBaseDTNode<I, D> blockRoot;
1077

1078
        GlobalSplitter(AbstractBaseDTNode<I, D> blockRoot, Splitter<I, D> localSplitter) {
2✔
1079
            this.blockRoot = blockRoot;
2✔
1080
            this.localSplitter = localSplitter;
2✔
1081
        }
2✔
1082
    }
1083

1084
    /**
1085
     * Data structure required during an extract operation. The latter basically works by copying nodes that are
1086
     * required in the extracted subtree, and this data structure is required to associate original nodes with their
1087
     * extracted copies.
1088
     *
1089
     * @param <I>
1090
     *         input symbol type
1091
     */
1092
    private static final class ExtractRecord<I, D> {
1093

1094
        private final AbstractBaseDTNode<I, D> original;
1095
        private final AbstractBaseDTNode<I, D> extracted;
1096

1097
        ExtractRecord(AbstractBaseDTNode<I, D> original, AbstractBaseDTNode<I, D> extracted) {
2✔
1098
            this.original = original;
2✔
1099
            this.extracted = extracted;
2✔
1100
        }
2✔
1101
    }
1102

1103
    private static final class SplitQuery<I, D> extends Query<I, D> {
1104

1105
        private final TTTTransition<I, D> transition;
1106
        private final Word<I> discriminator;
1107
        private D output;
1108

1109
        SplitQuery(TTTTransition<I, D> transition, Word<I> discriminator) {
2✔
1110
            this.transition = transition;
2✔
1111
            this.discriminator = discriminator;
2✔
1112
        }
2✔
1113

1114
        @Override
1115
        public void answer(D output) {
1116
            this.output = output;
2✔
1117
        }
2✔
1118

1119
        @Override
1120
        public Word<I> getPrefix() {
1121
            return transition.getAccessSequence();
2✔
1122
        }
1123

1124
        @Override
1125
        public Word<I> getSuffix() {
1126
            return discriminator;
2✔
1127
        }
1128
    }
1129
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc