• 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.2
/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/ADTLearner.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.adt.learner;
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.HashMap;
23
import java.util.HashSet;
24
import java.util.LinkedHashMap;
25
import java.util.LinkedHashSet;
26
import java.util.List;
27
import java.util.Map;
28
import java.util.Map.Entry;
29
import java.util.Queue;
30
import java.util.Set;
31

32
import de.learnlib.AccessSequenceTransformer;
33
import de.learnlib.LearnerStateTracker;
34
import de.learnlib.Resumable;
35
import de.learnlib.algorithm.LearningAlgorithm;
36
import de.learnlib.algorithm.adt.adt.ADT;
37
import de.learnlib.algorithm.adt.adt.ADT.LCAInfo;
38
import de.learnlib.algorithm.adt.adt.ADTLeafNode;
39
import de.learnlib.algorithm.adt.adt.ADTNode;
40
import de.learnlib.algorithm.adt.adt.ADTResetNode;
41
import de.learnlib.algorithm.adt.api.ADTExtender;
42
import de.learnlib.algorithm.adt.api.LeafSplitter;
43
import de.learnlib.algorithm.adt.api.PartialTransitionAnalyzer;
44
import de.learnlib.algorithm.adt.api.SubtreeReplacer;
45
import de.learnlib.algorithm.adt.automaton.ADTHypothesis;
46
import de.learnlib.algorithm.adt.automaton.ADTState;
47
import de.learnlib.algorithm.adt.automaton.ADTTransition;
48
import de.learnlib.algorithm.adt.config.ADTExtenders;
49
import de.learnlib.algorithm.adt.config.LeafSplitters;
50
import de.learnlib.algorithm.adt.config.SubtreeReplacers;
51
import de.learnlib.algorithm.adt.model.ExtensionResult;
52
import de.learnlib.algorithm.adt.model.ObservationTree;
53
import de.learnlib.algorithm.adt.model.ReplacementResult;
54
import de.learnlib.algorithm.adt.util.ADTUtil;
55
import de.learnlib.counterexample.LocalSuffixFinder;
56
import de.learnlib.counterexample.LocalSuffixFinders;
57
import de.learnlib.logging.Category;
58
import de.learnlib.oracle.AdaptiveMembershipOracle;
59
import de.learnlib.oracle.MembershipOracle.MealyMembershipOracle;
60
import de.learnlib.query.DefaultQuery;
61
import de.learnlib.tooling.annotation.builder.GenerateBuilder;
62
import de.learnlib.util.MQUtil;
63
import de.learnlib.util.mealy.Adaptive2MembershipWrapper;
64
import net.automatalib.alphabet.Alphabet;
65
import net.automatalib.alphabet.SupportsGrowingAlphabet;
66
import net.automatalib.automaton.transducer.MealyMachine;
67
import net.automatalib.common.util.HashUtil;
68
import net.automatalib.common.util.Pair;
69
import net.automatalib.word.Word;
70
import org.checkerframework.checker.nullness.qual.NonNull;
71
import org.slf4j.Logger;
72
import org.slf4j.LoggerFactory;
73

74
/**
75
 * The main learning algorithm.
76
 * <p>
77
 * <b>Implementation note:</b> this learner uses the {@link AccessSequenceTransformer} interface to provide access to
78
 * the representatives of the states of the current hypothesis model.
79
 *
80
 * @param <I>
81
 *         input symbol type
82
 * @param <O>
83
 *         output symbol type
84
 */
85
public class ADTLearner<I, O> implements LearningAlgorithm.MealyLearner<I, O>,
86
                                         PartialTransitionAnalyzer<ADTState<I, O>, I>,
87
                                         AccessSequenceTransformer<I>,
88
                                         SupportsGrowingAlphabet<I>,
89
                                         Resumable<ADTLearnerState<ADTState<I, O>, I, O>>,
90
                                         LearnerStateTracker {
91

92
    private static final Logger LOGGER = LoggerFactory.getLogger(ADTLearner.class);
2✔
93

94
    private final Alphabet<I> alphabet;
95
    private final AdaptiveMembershipOracle<I, O> oracle;
96
    private final MealyMembershipOracle<I, O> mqo;
97
    private final LeafSplitter leafSplitter;
98
    private final ADTExtender adtExtender;
99
    private final SubtreeReplacer subtreeReplacer;
100
    private final Queue<ADTTransition<I, O>> openTransitions;
101
    private final Queue<DefaultQuery<I, Word<O>>> openCounterExamples;
102
    private final Set<DefaultQuery<I, Word<O>>> allCounterExamples;
103
    private final ObservationTree<ADTState<I, O>, I, O> observationTree;
104
    private final LocalSuffixFinder<? super I, ? super Word<O>> suffixFinder;
105
    private ADTHypothesis<I, O> hypothesis;
106
    private ADT<ADTState<I, O>, I, O> adt;
107

108
    public ADTLearner(Alphabet<I> alphabet, AdaptiveMembershipOracle<I, O> oracle) {
109
        this(alphabet,
1✔
110
             oracle,
111
             BuilderDefaults.leafSplitter(),
1✔
112
             BuilderDefaults.adtExtender(),
1✔
113
             BuilderDefaults.subtreeReplacer());
1✔
114
    }
1✔
115

116
    public ADTLearner(Alphabet<I> alphabet,
117
                      AdaptiveMembershipOracle<I, O> oracle,
118
                      LeafSplitter leafSplitter,
119
                      ADTExtender adtExtender,
120
                      SubtreeReplacer subtreeReplacer) {
121
        this(alphabet,
2✔
122
             oracle,
123
             leafSplitter,
124
             adtExtender,
125
             subtreeReplacer,
126
             BuilderDefaults.useObservationTree(),
2✔
127
             BuilderDefaults.suffixFinder());
2✔
128
    }
2✔
129

130
    @GenerateBuilder(defaults = BuilderDefaults.class)
131
    public ADTLearner(Alphabet<I> alphabet,
132
                      AdaptiveMembershipOracle<I, O> oracle,
133
                      LeafSplitter leafSplitter,
134
                      ADTExtender adtExtender,
135
                      SubtreeReplacer subtreeReplacer,
136
                      boolean useObservationTree,
137
                      LocalSuffixFinder<? super I, ? super Word<O>> suffixFinder) {
2✔
138

139
        this.alphabet = alphabet;
2✔
140
        this.observationTree = new ObservationTree<>(this.alphabet, oracle, useObservationTree);
2✔
141
        this.oracle = this.observationTree;
2✔
142
        this.mqo = new Adaptive2MembershipWrapper<>(oracle);
2✔
143

144
        this.leafSplitter = leafSplitter;
2✔
145
        this.adtExtender = adtExtender;
2✔
146
        this.subtreeReplacer = subtreeReplacer;
2✔
147
        this.suffixFinder = suffixFinder;
2✔
148

149
        this.hypothesis = new ADTHypothesis<>(this.alphabet);
2✔
150
        this.openTransitions = new ArrayDeque<>();
2✔
151
        this.openCounterExamples = new ArrayDeque<>();
2✔
152
        this.allCounterExamples = new LinkedHashSet<>();
2✔
153
        this.adt = new ADT<>();
2✔
154
    }
2✔
155

156
    @Override
157
    public void startLearning() {
158
        requireLearningProcessNotStarted();
2✔
159

160
        final ADTState<I, O> initialState = this.hypothesis.addInitialState();
2✔
161
        initialState.setAccessSequence(Word.epsilon());
2✔
162
        this.observationTree.initialize(initialState);
2✔
163
        this.adt.initialize(initialState);
2✔
164

165
        for (I i : this.alphabet) {
2✔
166
            this.openTransitions.add(this.hypothesis.createOpenTransition(initialState, i, this.adt.getRoot()));
2✔
167
        }
2✔
168

169
        this.closeTransitions();
2✔
170
    }
2✔
171

172
    @Override
173
    public boolean refineHypothesis(DefaultQuery<I, Word<O>> ce) {
174
        requireLearningProcessStarted();
2✔
175

176
        if (!MQUtil.isCounterexample(ce, this.hypothesis)) {
2✔
177
            return false;
2✔
178
        }
179

180
        this.evaluateSubtreeReplacement();
2✔
181

182
        this.openCounterExamples.add(ce);
2✔
183

184
        while (!this.openCounterExamples.isEmpty()) {
2✔
185

186
            // normal refinement step
187
            while (!this.openCounterExamples.isEmpty()) {
2✔
188

189
                @SuppressWarnings("nullness")
190
                // false positive https://github.com/typetools/checker-framework/issues/399
191
                final @NonNull DefaultQuery<I, Word<O>> currentCE = this.openCounterExamples.poll();
2✔
192
                this.allCounterExamples.add(currentCE);
2✔
193

194
                while (this.refineHypothesisInternal(currentCE)) {
2✔
195
                    // refine exhaustively
196
                }
197
            }
2✔
198

199
            // subtree replacements may reactivate old CEs
200
            for (DefaultQuery<I, Word<O>> oldCE : this.allCounterExamples) {
2✔
201
                if (MQUtil.isCounterexample(oldCE, this.hypothesis)) {
2✔
202
                    this.openCounterExamples.add(oldCE);
2✔
203
                }
204
            }
2✔
205

206
            ADTUtil.collectLeaves(this.adt.getRoot()).forEach(this::ensureConsistency);
2✔
207
        }
208

209
        return true;
2✔
210
    }
211

212
    public boolean refineHypothesisInternal(DefaultQuery<I, Word<O>> ceQuery) {
213

214
        if (!MQUtil.isCounterexample(ceQuery, this.hypothesis)) {
2✔
215
            return false;
2✔
216
        }
217

218
        // Determine a counterexample decomposition (u, a, v)
219
        final int suffixIdx = suffixFinder.findSuffixIndex(ceQuery, this.hypothesis, this.hypothesis, this.mqo);
2✔
220

221
        if (suffixIdx == -1) {
2✔
222
            throw new IllegalStateException();
×
223
        }
224

225
        final Word<I> ceInput = ceQuery.getInput();
2✔
226

227
        final Word<I> u = ceInput.prefix(suffixIdx - 1);
2✔
228
        final Word<I> ua = ceInput.prefix(suffixIdx);
2✔
229
        final I a = ceInput.getSymbol(suffixIdx - 1);
2✔
230
        final Word<I> v = ceInput.subWord(suffixIdx);
2✔
231

232
        final ADTState<I, O> uState = this.hypothesis.getState(u);
2✔
233
        final ADTState<I, O> uaState = this.hypothesis.getState(ua);
2✔
234

235
        assert uState != null && uaState != null;
2✔
236

237
        final Word<I> uAccessSequence = uState.getAccessSequence();
2✔
238
        final Word<I> uaAccessSequence = uaState.getAccessSequence();
2✔
239
        final Word<I> uAccessSequenceWithA = uAccessSequence.append(a);
2✔
240

241
        final ADTState<I, O> newState = this.hypothesis.addState();
2✔
242
        newState.setAccessSequence(uAccessSequenceWithA);
2✔
243
        final ADTTransition<I, O> oldTrans = this.hypothesis.getTransition(uState, a);
2✔
244

245
        assert oldTrans != null;
2✔
246

247
        oldTrans.setTarget(newState);
2✔
248
        oldTrans.setIsSpanningTreeEdge(true);
2✔
249

250
        final ADTNode<ADTState<I, O>, I, O> nodeToSplit = findNodeForState(uaState);
2✔
251
        final ADTNode<ADTState<I, O>, I, O> newNode;
252

253
        // directly insert into observation tree, because we use it for finding a splitter
254
        this.observationTree.addState(newState, newState.getAccessSequence(), oldTrans.getOutput());
2✔
255
        this.observationTree.addTrace(newState, nodeToSplit);
2✔
256

257
        final Word<I> previousTrace = ADTUtil.buildTraceForNode(nodeToSplit).getFirst();
2✔
258
        final Word<I> extension = this.observationTree.findSeparatingWord(uaState, newState, previousTrace);
2✔
259

260
        if (extension == null) {
2✔
261
            // directly insert into observation tree, because we use it for finding a splitter
262
            this.observationTree.addTrace(uaState, v, this.mqo.answerQuery(uaAccessSequence, v));
2✔
263
            this.observationTree.addTrace(newState, v, this.mqo.answerQuery(uAccessSequenceWithA, v));
2✔
264

265
            // in doubt, we will always find v
266
            final Word<I> otSepWord = this.observationTree.findSeparatingWord(uaState, newState);
2✔
267
            final Word<I> splitter;
268
            assert otSepWord != null;
2✔
269

270
            if (otSepWord.length() < v.length()) {
2✔
271
                splitter = otSepWord;
2✔
272
            } else {
273
                splitter = v;
2✔
274
            }
275

276
            final Word<O> oldOutput = this.observationTree.trace(uaState, splitter);
2✔
277
            final Word<O> newOutput = this.observationTree.trace(newState, splitter);
2✔
278

279
            newNode = this.adt.splitLeaf(nodeToSplit, splitter, oldOutput, newOutput, this.leafSplitter);
2✔
280
        } else {
2✔
281
            final Word<I> completeSplitter = previousTrace.concat(extension);
2✔
282
            final Word<O> oldOutput = this.observationTree.trace(uaState, completeSplitter);
2✔
283
            final Word<O> newOutput = this.observationTree.trace(newState, completeSplitter);
2✔
284

285
            newNode = this.adt.extendLeaf(nodeToSplit, completeSplitter, oldOutput, newOutput, this.leafSplitter);
2✔
286
        }
287
        newNode.setState(newState);
2✔
288

289
        final ADTNode<ADTState<I, O>, I, O> temporarySplitter = ADTUtil.getStartOfADS(nodeToSplit);
2✔
290
        final List<ADTTransition<I, O>> newTransitions = new ArrayList<>(alphabet.size());
2✔
291

292
        for (I i : alphabet) {
2✔
293
            newTransitions.add(this.hypothesis.createOpenTransition(newState, i, this.adt.getRoot()));
2✔
294
        }
2✔
295

296
        final List<ADTTransition<I, O>> transitionsToRefine = getIncomingNonSpanningTreeTransitions(uaState);
2✔
297

298
        for (ADTTransition<I, O> x : transitionsToRefine) {
2✔
299
            x.setTarget(null);
2✔
300
            x.setSiftNode(temporarySplitter);
2✔
301
        }
2✔
302

303
        final ADTNode<ADTState<I, O>, I, O> finalizedSplitter = this.evaluateAdtExtension(temporarySplitter);
2✔
304

305
        for (ADTTransition<I, O> t : transitionsToRefine) {
2✔
306
            if (t.needsSifting()) {
2✔
307
                t.setSiftNode(finalizedSplitter);
2✔
308
                this.openTransitions.add(t);
2✔
309
            }
310
        }
2✔
311

312
        for (ADTTransition<I, O> t : newTransitions) {
2✔
313
            if (t.needsSifting()) {
2✔
314
                this.openTransitions.add(t);
2✔
315
            }
316
        }
2✔
317

318
        this.closeTransitions();
2✔
319
        return true;
2✔
320
    }
321

322
    private ADTNode<ADTState<I, O>, I, O> findNodeForState(ADTState<I, O> state) {
323

324
        for (ADTNode<ADTState<I, O>, I, O> leaf : ADTUtil.collectLeaves(this.adt.getRoot())) {
2✔
325
            if (leaf.getState().equals(state)) {
2✔
326
                return leaf;
2✔
327
            }
328
        }
2✔
329

330
        throw new IllegalStateException("Cannot find leaf for state " + state);
×
331

332
    }
333

334
    @Override
335
    public MealyMachine<?, I, ?, O> getHypothesisModel() {
336
        requireLearningProcessStarted();
2✔
337
        return this.hypothesis;
2✔
338
    }
339

340
    @Override
341
    public boolean hasLearningProcessStarted() {
342
        return !hypothesis.getStates().isEmpty();
2✔
343
    }
344

345
    /**
346
     * Close all pending open transitions.
347
     */
348
    private void closeTransitions() {
349
        while (!this.openTransitions.isEmpty()) {
2✔
350

351
            final Collection<ADTAdaptiveQuery<I, O>> queries = new ArrayList<>(this.openTransitions.size());
2✔
352

353
            //create a query object for every transition
354
            for (ADTTransition<I, O> transition : this.openTransitions) {
2✔
355
                if (transition.needsSifting()) {
2✔
356
                    queries.add(new ADTAdaptiveQuery<>(transition, transition.getSiftNode()));
2✔
357
                }
358
            }
2✔
359

360
            this.openTransitions.clear();
2✔
361
            this.oracle.processQueries(queries);
2✔
362

363
            for (ADTAdaptiveQuery<I, O> query : queries) {
2✔
364
                processAnsweredQuery(query);
2✔
365
            }
2✔
366
        }
2✔
367
    }
2✔
368

369
    @Override
370
    public void closeTransition(ADTState<I, O> state, I input) {
371

372
        final ADTTransition<I, O> transition = this.hypothesis.getTransition(state, input);
2✔
373
        assert transition != null;
2✔
374

375
        if (transition.needsSifting()) {
2✔
376
            final ADTNode<ADTState<I, O>, I, O> ads = transition.getSiftNode();
2✔
377
            final int oldNumberOfFinalStates = ADTUtil.collectLeaves(ads).size();
2✔
378

379
            final ADTAdaptiveQuery<I, O> query = new ADTAdaptiveQuery<>(transition, transition.getSiftNode());
2✔
380
            this.oracle.processQueries(Collections.singleton(query));
2✔
381
            processAnsweredQuery(query);
2✔
382

383
            final int newNumberOfFinalStates = ADTUtil.collectLeaves(ads).size();
2✔
384

385
            if (oldNumberOfFinalStates < newNumberOfFinalStates) {
2✔
386
                throw PartialTransitionAnalyzer.HYPOTHESIS_MODIFICATION_EXCEPTION;
2✔
387
            }
388
        }
389
    }
2✔
390

391
    private void processAnsweredQuery(ADTAdaptiveQuery<I, O> query) {
392
        if (query.needsPostProcessing()) {
2✔
393
            final ADTNode<ADTState<I, O>, I, O> parent = query.getCurrentADTNode();
2✔
394
            final O out = query.getTempOut();
2✔
395
            final ADTNode<ADTState<I, O>, I, O> succ = parent.getChild(out);
2✔
396

397
            // first time we process the successor
398
            if (succ == null) {
2✔
399
                // add new state to the hypothesis and set the accessSequence
400
                final ADTState<I, O> newState = this.hypothesis.addState();
2✔
401
                final Word<I> longPrefix = query.getAccessSequence().append(query.getTransition().getInput());
2✔
402
                newState.setAccessSequence(longPrefix);
2✔
403

404
                // configure the transition
405
                final ADTTransition<I, O> transition = query.getTransition();
2✔
406
                transition.setTarget(newState);
2✔
407
                transition.setIsSpanningTreeEdge(true);
2✔
408

409
                // add new leaf node to ADT
410
                final ADTNode<ADTState<I, O>, I, O> result = new ADTLeafNode<>(parent, newState);
2✔
411
                parent.getChildren().put(out, result);
2✔
412

413
                // add the observations to the observation tree
414
                O transitionOutput = query.getTransition().getOutput();
2✔
415
                this.observationTree.addState(newState, longPrefix, transitionOutput);
2✔
416

417
                // query successors
418
                for (I i : this.alphabet) {
2✔
419
                    this.openTransitions.add(this.hypothesis.createOpenTransition(newState, i, this.adt.getRoot()));
2✔
420
                }
2✔
421
            } else {
2✔
422
                assert ADTUtil.isLeafNode(succ);
2✔
423
                // state has been created before, just update target
424
                query.getTransition().setTarget(succ.getState());
2✔
425
            }
426
        } else {
2✔
427
            // update target
428
            final ADTTransition<I, O> transition = query.getTransition();
2✔
429
            final ADTNode<ADTState<I, O>, I, O> adtNode = query.getCurrentADTNode();
2✔
430
            assert ADTUtil.isLeafNode(adtNode);
2✔
431
            transition.setTarget(adtNode.getState());
2✔
432
        }
433
    }
2✔
434

435
    @Override
436
    public boolean isTransitionDefined(ADTState<I, O> state, I input) {
437
        final ADTTransition<I, O> transition = this.hypothesis.getTransition(state, input);
2✔
438
        assert transition != null;
2✔
439
        return !transition.needsSifting();
2✔
440
    }
441

442
    @Override
443
    public Word<I> transformAccessSequence(Word<I> word) {
444
        requireLearningProcessStarted();
2✔
445

446
        final ADTState<I, O> state = this.hypothesis.getState(word);
2✔
447
        assert state != null;
2✔
448
        return state.getAccessSequence();
2✔
449
    }
450

451
    @Override
452
    public void addAlphabetSymbol(I symbol) {
453

454
        if (!this.alphabet.containsSymbol(symbol)) {
2✔
455
            this.alphabet.asGrowingAlphabetOrThrowException().addSymbol(symbol);
2✔
456
        }
457

458
        this.hypothesis.addAlphabetSymbol(symbol);
2✔
459
        this.observationTree.addAlphabetSymbol(symbol);
2✔
460

461
        // check if we already have information about the symbol (then the transition is defined) so we don't post
462
        // redundant queries
463
        if (this.hypothesis.getInitialState() != null &&
2✔
464
            this.hypothesis.getSuccessor(this.hypothesis.getInitialState(), symbol) == null) {
2✔
465
            for (ADTState<I, O> s : this.hypothesis.getStates()) {
2✔
466
                this.openTransitions.add(this.hypothesis.createOpenTransition(s, symbol, this.adt.getRoot()));
2✔
467
            }
2✔
468

469
            this.closeTransitions();
2✔
470
        }
471
    }
2✔
472

473
    @Override
474
    public ADTLearnerState<ADTState<I, O>, I, O> suspend() {
475
        return new ADTLearnerState<>(this.hypothesis, this.adt);
2✔
476
    }
477

478
    @Override
479
    public void resume(ADTLearnerState<ADTState<I, O>, I, O> state) {
480
        this.hypothesis = state.getHypothesis();
2✔
481
        this.adt = state.getAdt();
2✔
482

483
        final Alphabet<I> oldAlphabet = this.hypothesis.getInputAlphabet();
2✔
484
        if (!oldAlphabet.equals(this.alphabet)) {
2✔
485
            LOGGER.warn(Category.DATASTRUCTURE,
×
486
                        "The current alphabet '{}' differs from the resumed alphabet '{}'. Future behavior may be inconsistent",
487
                        this.alphabet,
488
                        oldAlphabet);
489
        }
490

491
        // startLearning has already been invoked
492
        if (this.hypothesis.size() > 0) {
2✔
493
            this.observationTree.initialize(this.hypothesis.getStates(),
2✔
494
                                            ADTState::getAccessSequence,
495
                                            this.hypothesis::computeOutput);
2✔
496
        }
497
    }
2✔
498

499
    /**
500
     * Ensure that the output behavior of a hypothesis state matches the observed output behavior recorded in the ADT.
501
     * Any differences in output behavior yields new counterexamples.
502
     *
503
     * @param leaf
504
     *         the leaf whose hypothesis state should be checked
505
     */
506
    private void ensureConsistency(ADTNode<ADTState<I, O>, I, O> leaf) {
507

508
        final ADTState<I, O> state = leaf.getState();
2✔
509
        final Word<I> as = state.getAccessSequence();
2✔
510
        final Word<O> asOut = this.hypothesis.computeOutput(as);
2✔
511

512
        ADTNode<ADTState<I, O>, I, O> iter = leaf;
2✔
513

514
        while (iter != null) {
2✔
515
            final Pair<Word<I>, Word<O>> trace = ADTUtil.buildTraceForNode(iter);
2✔
516

517
            final Word<I> input = trace.getFirst();
2✔
518
            final Word<O> output = trace.getSecond();
2✔
519

520
            final Word<O> hypOut = this.hypothesis.computeStateOutput(state, input);
2✔
521

522
            if (!hypOut.equals(output)) {
2✔
523
                this.openCounterExamples.add(new DefaultQuery<>(as.concat(input), asOut.concat(output)));
2✔
524
            }
525

526
            iter = ADTUtil.getStartOfADS(iter).getParent();
2✔
527
        }
2✔
528
    }
2✔
529

530
    /**
531
     * Ask the current {@link #adtExtender} for a potential extension.
532
     *
533
     * @param ads
534
     *         the temporary ADS based on the inferred distinguishing suffix
535
     *
536
     * @return a validated ADT that can be used to distinguish the states referenced in the given temporary ADS
537
     */
538
    private ADTNode<ADTState<I, O>, I, O> evaluateAdtExtension(ADTNode<ADTState<I, O>, I, O> ads) {
539

540
        final ExtensionResult<ADTState<I, O>, I, O> potentialExtension =
2✔
541
                this.adtExtender.computeExtension(this.hypothesis, this, ads);
2✔
542

543
        if (potentialExtension.isCounterExample()) {
2✔
544
            this.openCounterExamples.add(potentialExtension.getCounterExample());
2✔
545
            return ads;
2✔
546
        } else if (!potentialExtension.isReplacement()) {
2✔
547
            return ads;
2✔
548
        }
549

550
        final ADTNode<ADTState<I, O>, I, O> extension = potentialExtension.getReplacement();
2✔
551
        final ADTNode<ADTState<I, O>, I, O> nodeToReplace = ads.getParent(); // reset node
2✔
552

553
        assert extension != null && nodeToReplace != null &&
2✔
554
               this.validateADS(nodeToReplace, extension, Collections.emptySet());
2✔
555

556
        final ADTNode<ADTState<I, O>, I, O> replacement = this.verifyADS(nodeToReplace,
2✔
557
                                                                         extension,
558
                                                                         ADTUtil.collectLeaves(this.adt.getRoot()),
2✔
559
                                                                         Collections.emptySet());
2✔
560

561
        // verification may have introduced reset nodes
562
        final int oldCosts = ADTUtil.computeEffectiveResets(nodeToReplace);
2✔
563
        final int newCosts = ADTUtil.computeEffectiveResets(replacement);
2✔
564

565
        if (newCosts >= oldCosts) {
2✔
566
            return ads;
2✔
567
        }
568

569
        // replace
570
        this.adt.replaceNode(nodeToReplace, replacement);
2✔
571

572
        final ADTNode<ADTState<I, O>, I, O> finalizedADS = ADTUtil.getStartOfADS(replacement);
2✔
573

574
        // update
575
        this.resiftAffectedTransitions(ADTUtil.collectLeaves(extension), finalizedADS);
2✔
576

577
        return finalizedADS;
2✔
578
    }
579

580
    /**
581
     * Ask the {@link #subtreeReplacer} for any replacements.
582
     */
583
    private void evaluateSubtreeReplacement() {
584

585
        if (this.hypothesis.size() == 1) {
2✔
586
            // skip replacement if only one node is discovered
587
            return;
2✔
588
        }
589

590
        final Set<ReplacementResult<ADTState<I, O>, I, O>> potentialReplacements =
2✔
591
                this.subtreeReplacer.computeReplacements(this.hypothesis, this.alphabet, this.adt);
2✔
592
        final List<ReplacementResult<ADTState<I, O>, I, O>> validReplacements =
2✔
593
                new ArrayList<>(potentialReplacements.size());
2✔
594
        final Set<ADTNode<ADTState<I, O>, I, O>> cachedLeaves =
595
                potentialReplacements.isEmpty() ? Collections.emptySet() : ADTUtil.collectLeaves(this.adt.getRoot());
2✔
596

597
        for (ReplacementResult<ADTState<I, O>, I, O> potentialReplacement : potentialReplacements) {
2✔
598
            final ADTNode<ADTState<I, O>, I, O> proposedReplacement = potentialReplacement.getReplacement();
2✔
599
            final ADTNode<ADTState<I, O>, I, O> nodeToReplace = potentialReplacement.getNodeToReplace();
2✔
600

601
            assert this.validateADS(nodeToReplace, proposedReplacement, potentialReplacement.getCutoutNodes());
2✔
602

603
            final ADTNode<ADTState<I, O>, I, O> replacement = this.verifyADS(nodeToReplace,
2✔
604
                                                                             proposedReplacement,
605
                                                                             cachedLeaves,
606
                                                                             potentialReplacement.getCutoutNodes());
2✔
607

608
            // verification may have introduced reset nodes
609
            final int oldCosts = ADTUtil.computeEffectiveResets(nodeToReplace);
2✔
610
            final int newCosts = ADTUtil.computeEffectiveResets(replacement);
2✔
611

612
            if (newCosts >= oldCosts) {
2✔
613
                continue;
2✔
614
            }
615

616
            validReplacements.add(new ReplacementResult<>(nodeToReplace, replacement));
2✔
617
        }
2✔
618

619
        for (ReplacementResult<ADTState<I, O>, I, O> potentialReplacement : validReplacements) {
2✔
620
            final ADTNode<ADTState<I, O>, I, O> replacement = potentialReplacement.getReplacement();
2✔
621
            final ADTNode<ADTState<I, O>, I, O> nodeToReplace = potentialReplacement.getNodeToReplace();
2✔
622

623
            this.adt.replaceNode(nodeToReplace, replacement);
2✔
624

625
            this.resiftAffectedTransitions(ADTUtil.collectLeaves(replacement), ADTUtil.getStartOfADS(replacement));
2✔
626
        }
2✔
627

628
        this.closeTransitions();
2✔
629
    }
2✔
630

631
    /**
632
     * Validate the well-definedness of an ADT replacement, i.e. both ADTs cover the same set of hypothesis states and
633
     * the output behavior described in the replacement matches the hypothesis output.
634
     *
635
     * @param oldADS
636
     *         the old ADT (subtree) to be replaced
637
     * @param newADS
638
     *         the new ADT (subtree)
639
     * @param cutout
640
     *         the set of states not covered by the new ADT
641
     *
642
     * @return {@code true} if the replacement is valid, {@code false} otherwise.
643
     */
644
    private boolean validateADS(ADTNode<ADTState<I, O>, I, O> oldADS,
645
                                ADTNode<ADTState<I, O>, I, O> newADS,
646
                                Set<ADTState<I, O>> cutout) {
647

648
        final Set<ADTNode<ADTState<I, O>, I, O>> oldNodes;
649

650
        if (ADTUtil.isResetNode(oldADS)) {
2✔
651
            oldNodes = ADTUtil.collectResetNodes(this.adt.getRoot());
2✔
652
        } else {
653
            oldNodes = ADTUtil.collectADSNodes(this.adt.getRoot(), true);
2✔
654
        }
655

656
        if (!oldNodes.contains(oldADS)) {
2✔
657
            throw new IllegalArgumentException("Subtree to replace does not exist");
×
658
        }
659

660
        final Set<ADTNode<ADTState<I, O>, I, O>> newFinalNodes = ADTUtil.collectLeaves(newADS);
2✔
661
        final Map<ADTState<I, O>, Pair<Word<I>, Word<O>>> traces =
2✔
662
                new HashMap<>(HashUtil.capacity(newFinalNodes.size()));
2✔
663

664
        for (ADTNode<ADTState<I, O>, I, O> n : newFinalNodes) {
2✔
665
            traces.put(n.getState(), ADTUtil.buildTraceForNode(n));
2✔
666
        }
2✔
667

668
        final Set<ADTState<I, O>> oldFinalStates = ADTUtil.collectHypothesisStates(oldADS);
2✔
669
        final Set<ADTState<I, O>> newFinalStates = new HashSet<>(traces.keySet());
2✔
670
        newFinalStates.addAll(cutout);
2✔
671

672
        if (!oldFinalStates.equals(newFinalStates)) {
2✔
673
            throw new IllegalArgumentException("New ADS does not cover all old nodes");
×
674
        }
675

676
        final Word<I> parentInputTrace = ADTUtil.buildTraceForNode(oldADS).getFirst();
2✔
677

678
        for (Map.Entry<ADTState<I, O>, Pair<Word<I>, Word<O>>> entry : traces.entrySet()) {
2✔
679

680
            final Word<I> accessSequence = entry.getKey().getAccessSequence();
2✔
681
            final Word<I> prefix = accessSequence.concat(parentInputTrace);
2✔
682
            final Word<I> input = entry.getValue().getFirst();
2✔
683
            final Word<O> output = entry.getValue().getSecond();
2✔
684

685
            if (!this.hypothesis.computeSuffixOutput(prefix, input).equals(output)) {
2✔
686
                throw new IllegalArgumentException("Output of new ADS does not match hypothesis");
×
687
            }
688
        }
2✔
689

690
        return true;
2✔
691
    }
692

693
    /**
694
     * Verify the proposed ADT replacement by checking the actual behavior of the system under learning. During the
695
     * verification process, the system under learning may behave differently from what the ADT replacement suggests:
696
     * This means a counterexample is witnessed and added to the queue of counterexamples for later investigation.
697
     * Albeit observing diverging behavior, this method continues to trying to construct a valid ADT using the observed
698
     * output. If for two states, no distinguishing output can be observed, the states a separated by means of
699
     * {@link #resolveAmbiguities(ADTNode, ADTNode, ADTState, Set)}.
700
     *
701
     * @param nodeToReplace
702
     *         the old ADT (subtree) to be replaced
703
     * @param replacement
704
     *         the new ADT (subtree). Must have the form of an ADS, i.e. no reset nodes
705
     * @param cachedLeaves
706
     *         a set containing the leaves of the current tree, so they don't have to be re-fetched for every
707
     *         replacement verification
708
     * @param cutout
709
     *         the set of states not covered by the new ADT
710
     *
711
     * @return A verified ADT that correctly distinguishes the states covered by the original ADT
712
     */
713
    private ADTNode<ADTState<I, O>, I, O> verifyADS(ADTNode<ADTState<I, O>, I, O> nodeToReplace,
714
                                                    ADTNode<ADTState<I, O>, I, O> replacement,
715
                                                    Set<ADTNode<ADTState<I, O>, I, O>> cachedLeaves,
716
                                                    Set<ADTState<I, O>> cutout) {
717
        final Set<ADTNode<ADTState<I, O>, I, O>> leaves = ADTUtil.collectLeaves(replacement);
2✔
718
        final Map<ADTState<I, O>, Pair<Word<I>, Word<O>>> traces =
2✔
719
                new LinkedHashMap<>(HashUtil.capacity(leaves.size()));
2✔
720

721
        for (ADTNode<ADTState<I, O>, I, O> leaf : leaves) {
2✔
722
            traces.put(leaf.getState(), ADTUtil.buildTraceForNode(leaf));
2✔
723
        }
2✔
724

725
        final Pair<Word<I>, Word<O>> parentTrace = ADTUtil.buildTraceForNode(nodeToReplace);
2✔
726

727
        ADTNode<ADTState<I, O>, I, O> result = null;
2✔
728

729
        final List<ADSVerificationQuery<I, O>> queries = new ArrayList<>(traces.size());
2✔
730

731
        for (Entry<ADTState<I, O>, Pair<Word<I>, Word<O>>> e : traces.entrySet()) {
2✔
732
            final ADTState<I, O> state = e.getKey();
2✔
733
            final Pair<Word<I>, Word<O>> ads = e.getValue();
2✔
734
            queries.add(new ADSVerificationQuery<>(state.getAccessSequence().concat(parentTrace.getFirst()),
2✔
735
                                                   ads.getFirst(),
2✔
736
                                                   ads.getSecond(),
2✔
737
                                                   state));
738
        }
2✔
739

740
        this.oracle.processQueries(queries);
2✔
741

742
        for (ADSVerificationQuery<I, O> query : queries) {
2✔
743
            final ADTNode<ADTState<I, O>, I, O> trace;
744
            final DefaultQuery<I, Word<O>> ce = query.getCounterexample();
2✔
745

746
            if (ce != null) {
2✔
747
                this.openCounterExamples.add(ce);
2✔
748
                trace = ADTUtil.buildADSFromObservation(ce.getSuffix(), ce.getOutput(), query.getState());
2✔
749
            } else {
750
                trace = ADTUtil.buildADSFromObservation(query.getSuffix(), query.getExpectedOutput(), query.getState());
2✔
751
            }
752

753
            if (result == null) {
2✔
754
                result = trace;
2✔
755
            } else {
756
                if (!ADTUtil.mergeADS(result, trace)) {
2✔
757
                    this.resolveAmbiguities(nodeToReplace, result, query.getState(), cachedLeaves);
2✔
758
                }
759
            }
760
        }
2✔
761

762
        for (ADTState<I, O> s : cutout) {
2✔
763
            this.resolveAmbiguities(nodeToReplace, result, s, cachedLeaves);
2✔
764
        }
2✔
765

766
        return result;
2✔
767
    }
768

769
    /**
770
     * If two states show the same output behavior resolve this ambiguity by adding a reset node and add a new (sub) ADS
771
     * based on the lowest common ancestor in the existing ADT.
772
     *
773
     * @param nodeToReplace
774
     *         the old ADT (subtree) to be replaced
775
     * @param newADS
776
     *         the new ADT (subtree)
777
     * @param state
778
     *         the state which cannot be distinguished using the given replacement
779
     * @param cachedLeaves
780
     *         a set containing the leaves of the current tree, so they don't have to be re-fetched for every
781
     *         replacement verification
782
     */
783
    private void resolveAmbiguities(ADTNode<ADTState<I, O>, I, O> nodeToReplace,
784
                                    ADTNode<ADTState<I, O>, I, O> newADS,
785
                                    ADTState<I, O> state,
786
                                    Set<ADTNode<ADTState<I, O>, I, O>> cachedLeaves) {
787

788
        final Pair<Word<I>, Word<O>> parentTrace = ADTUtil.buildTraceForNode(nodeToReplace);
2✔
789
        final ADSAmbiguityQuery<I, O> query =
2✔
790
                new ADSAmbiguityQuery<>(state.getAccessSequence(), parentTrace.getFirst(), newADS);
2✔
791

792
        this.oracle.processQuery(query);
2✔
793

794
        if (query.needsPostProcessing()) {
2✔
795
            final ADTNode<ADTState<I, O>, I, O> prev = query.getCurrentADTNode();
2✔
796
            final ADTNode<ADTState<I, O>, I, O> newFinal = new ADTLeafNode<>(prev, state);
2✔
797
            prev.getChildren().put(query.getTempOut(), newFinal);
2✔
798
            return;
2✔
799
        }
800

801
        final ADTNode<ADTState<I, O>, I, O> finalNode = query.getCurrentADTNode();
2✔
802
        ADTNode<ADTState<I, O>, I, O> oldReference = null, newReference = null;
2✔
803

804
        for (ADTNode<ADTState<I, O>, I, O> leaf : cachedLeaves) {
2✔
805
            final ADTState<I, O> hypState = leaf.getState();
2✔
806

807
            if (hypState.equals(finalNode.getState())) {
2✔
808
                oldReference = leaf;
2✔
809
            } else if (hypState.equals(state)) {
2✔
810
                newReference = leaf;
2✔
811
            }
812

813
            if (oldReference != null && newReference != null) {
2✔
814
                break;
2✔
815
            }
816
        }
2✔
817

818
        assert oldReference != null && newReference != null;
2✔
819
        final LCAInfo<ADTState<I, O>, I, O> lcaResult = this.adt.findLCA(oldReference, newReference);
2✔
820
        final ADTNode<ADTState<I, O>, I, O> lca = lcaResult.adtNode;
2✔
821
        final Pair<Word<I>, Word<O>> lcaTrace = ADTUtil.buildTraceForNode(lca);
2✔
822

823
        final Word<I> sepWord = lcaTrace.getFirst().append(lca.getSymbol());
2✔
824
        final Word<O> oldOutputTrace = lcaTrace.getSecond().append(lcaResult.firstOutput);
2✔
825
        final Word<O> newOutputTrace = lcaTrace.getSecond().append(lcaResult.secondOutput);
2✔
826

827
        final ADTNode<ADTState<I, O>, I, O> oldTrace =
2✔
828
                ADTUtil.buildADSFromObservation(sepWord, oldOutputTrace, finalNode.getState());
2✔
829
        final ADTNode<ADTState<I, O>, I, O> newTrace = ADTUtil.buildADSFromObservation(sepWord, newOutputTrace, state);
2✔
830

831
        if (!ADTUtil.mergeADS(oldTrace, newTrace)) {
2✔
832
            throw new IllegalStateException("Should never happen");
×
833
        }
834

835
        final ADTNode<ADTState<I, O>, I, O> reset = new ADTResetNode<>(oldTrace);
2✔
836
        final ADTNode<ADTState<I, O>, I, O> parent = finalNode.getParent();
2✔
837
        assert parent != null;
2✔
838
        final O parentOutput = ADTUtil.getOutputForSuccessor(parent, finalNode);
2✔
839

840
        parent.getChildren().put(parentOutput, reset);
2✔
841
        reset.setParent(parent);
2✔
842
        oldTrace.setParent(reset);
2✔
843
    }
2✔
844

845
    /**
846
     * Schedule all incoming transitions of the given states to be re-sifted against the given ADT (subtree).
847
     *
848
     * @param states
849
     *         A set of states, whose incoming transitions should be sifted
850
     * @param finalizedADS
851
     *         the ADT (subtree) to sift through
852
     */
853
    private void resiftAffectedTransitions(Set<ADTNode<ADTState<I, O>, I, O>> states,
854
                                           ADTNode<ADTState<I, O>, I, O> finalizedADS) {
855

856
        for (ADTNode<ADTState<I, O>, I, O> state : states) {
2✔
857

858
            for (ADTTransition<I, O> trans : getIncomingNonSpanningTreeTransitions(state.getState())) {
2✔
859
                trans.setTarget(null);
2✔
860
                trans.setSiftNode(finalizedADS);
2✔
861
                this.openTransitions.add(trans);
2✔
862
            }
2✔
863
        }
2✔
864
    }
2✔
865

866
    private List<ADTTransition<I, O>> getIncomingNonSpanningTreeTransitions(ADTState<I, O> state) {
867
        final Set<ADTTransition<I, O>> transitions = state.getIncomingTransitions();
2✔
868
        final List<ADTTransition<I, O>> result = new ArrayList<>(transitions.size());
2✔
869

870
        for (ADTTransition<I, O> t : transitions) {
2✔
871
            if (!t.isSpanningTreeEdge()) {
2✔
872
                result.add(t);
2✔
873
            }
874
        }
2✔
875

876
        return result;
2✔
877
    }
878

879
    public ADT<ADTState<I, O>, I, O> getADT() {
880
        return adt;
2✔
881
    }
882

883
    static final class BuilderDefaults {
884

885
        private BuilderDefaults() {
886
            // prevent instantiation
887
        }
888

889
        static LeafSplitter leafSplitter() {
890
            return LeafSplitters.DEFAULT_SPLITTER;
2✔
891
        }
892

893
        static ADTExtender adtExtender() {
894
            return ADTExtenders.EXTEND_BEST_EFFORT;
2✔
895
        }
896

897
        static SubtreeReplacer subtreeReplacer() {
898
            return SubtreeReplacers.LEVELED_BEST_EFFORT;
2✔
899
        }
900

901
        static boolean useObservationTree() {
902
            return true;
2✔
903
        }
904

905
        @SuppressWarnings("unchecked")
906
        static <I, D> LocalSuffixFinder<I, D> suffixFinder() {
907
            return (LocalSuffixFinder<I, D>) LocalSuffixFinders.RIVEST_SCHAPIRE;
2✔
908
        }
909
    }
910
}
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