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

LearnLib / learnlib / 12755372168

13 Jan 2025 08:13PM UTC coverage: 94.2% (-0.002%) from 94.202%
12755372168

push

github

mtf90
cleanup some ADT code

83 of 86 new or added lines in 10 files covered. (96.51%)

1 existing line in 1 file now uncovered.

11776 of 12501 relevant lines covered (94.2%)

1.71 hits per line

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

98.12
/algorithms/active/adt/src/main/java/de/learnlib/algorithm/adt/learner/ADTLearner.java
1
/* Copyright (C) 2013-2025 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.Resumable;
33
import de.learnlib.algorithm.LearningAlgorithm;
34
import de.learnlib.algorithm.adt.adt.ADT;
35
import de.learnlib.algorithm.adt.adt.ADT.LCAInfo;
36
import de.learnlib.algorithm.adt.adt.ADTLeafNode;
37
import de.learnlib.algorithm.adt.adt.ADTNode;
38
import de.learnlib.algorithm.adt.adt.ADTResetNode;
39
import de.learnlib.algorithm.adt.api.ADTExtender;
40
import de.learnlib.algorithm.adt.api.LeafSplitter;
41
import de.learnlib.algorithm.adt.api.PartialTransitionAnalyzer;
42
import de.learnlib.algorithm.adt.api.SubtreeReplacer;
43
import de.learnlib.algorithm.adt.automaton.ADTHypothesis;
44
import de.learnlib.algorithm.adt.automaton.ADTState;
45
import de.learnlib.algorithm.adt.automaton.ADTTransition;
46
import de.learnlib.algorithm.adt.config.ADTExtenders;
47
import de.learnlib.algorithm.adt.config.LeafSplitters;
48
import de.learnlib.algorithm.adt.config.SubtreeReplacers;
49
import de.learnlib.algorithm.adt.model.ExtensionResult;
50
import de.learnlib.algorithm.adt.model.ObservationTree;
51
import de.learnlib.algorithm.adt.model.ReplacementResult;
52
import de.learnlib.algorithm.adt.util.ADTUtil;
53
import de.learnlib.counterexample.LocalSuffixFinders;
54
import de.learnlib.logging.Category;
55
import de.learnlib.oracle.AdaptiveMembershipOracle;
56
import de.learnlib.oracle.MembershipOracle.MealyMembershipOracle;
57
import de.learnlib.query.DefaultQuery;
58
import de.learnlib.tooling.annotation.builder.GenerateBuilder;
59
import de.learnlib.util.MQUtil;
60
import net.automatalib.alphabet.Alphabet;
61
import net.automatalib.alphabet.SupportsGrowingAlphabet;
62
import net.automatalib.automaton.transducer.MealyMachine;
63
import net.automatalib.common.util.HashUtil;
64
import net.automatalib.common.util.Pair;
65
import net.automatalib.word.Word;
66
import org.checkerframework.checker.nullness.qual.NonNull;
67
import org.slf4j.Logger;
68
import org.slf4j.LoggerFactory;
69

70
/**
71
 * The main learning algorithm.
72
 *
73
 * @param <I>
74
 *         input symbol type
75
 * @param <O>
76
 *         output symbol type
77
 */
78
public class ADTLearner<I, O> implements LearningAlgorithm.MealyLearner<I, O>,
79
                                         PartialTransitionAnalyzer<ADTState<I, O>, I>,
80
                                         SupportsGrowingAlphabet<I>,
81
                                         Resumable<ADTLearnerState<ADTState<I, O>, I, O>> {
82

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

85
    private final Alphabet<I> alphabet;
86
    private final AdaptiveMembershipOracle<I, O> oracle;
87
    private final MealyMembershipOracle<I, O> mqo;
88
    private final LeafSplitter leafSplitter;
89
    private final ADTExtender adtExtender;
90
    private final SubtreeReplacer subtreeReplacer;
91
    private final Queue<ADTTransition<I, O>> openTransitions;
92
    private final Queue<DefaultQuery<I, Word<O>>> openCounterExamples;
93
    private final Set<DefaultQuery<I, Word<O>>> allCounterExamples;
94
    private final ObservationTree<ADTState<I, O>, I, O> observationTree;
95
    private ADTHypothesis<I, O> hypothesis;
96
    private ADT<ADTState<I, O>, I, O> adt;
97

98
    public ADTLearner(Alphabet<I> alphabet,
99
                      AdaptiveMembershipOracle<I, O> oracle,
100
                      LeafSplitter leafSplitter,
101
                      ADTExtender adtExtender,
102
                      SubtreeReplacer subtreeReplacer) {
103
        this(alphabet, oracle, leafSplitter, adtExtender, subtreeReplacer, true);
2✔
104
    }
2✔
105

106
    @GenerateBuilder(defaults = BuilderDefaults.class)
107
    public ADTLearner(Alphabet<I> alphabet,
108
                      AdaptiveMembershipOracle<I, O> oracle,
109
                      LeafSplitter leafSplitter,
110
                      ADTExtender adtExtender,
111
                      SubtreeReplacer subtreeReplacer,
112
                      boolean useObservationTree) {
2✔
113

114
        this.alphabet = alphabet;
2✔
115
        this.observationTree = new ObservationTree<>(this.alphabet, oracle, useObservationTree);
2✔
116
        this.oracle = this.observationTree;
2✔
117
        this.mqo = new Adaptive2MembershipWrapper<>(oracle);
2✔
118

119
        this.leafSplitter = leafSplitter;
2✔
120
        this.adtExtender = adtExtender;
2✔
121
        this.subtreeReplacer = subtreeReplacer;
2✔
122

123
        this.hypothesis = new ADTHypothesis<>(this.alphabet);
2✔
124
        this.openTransitions = new ArrayDeque<>();
2✔
125
        this.openCounterExamples = new ArrayDeque<>();
2✔
126
        this.allCounterExamples = new LinkedHashSet<>();
2✔
127
        this.adt = new ADT<>();
2✔
128
    }
2✔
129

130
    @Override
131
    public void startLearning() {
132

133
        final ADTState<I, O> initialState = this.hypothesis.addInitialState();
2✔
134
        initialState.setAccessSequence(Word.epsilon());
2✔
135
        this.observationTree.initialize(initialState);
2✔
136
        this.adt.initialize(initialState);
2✔
137

138
        for (I i : this.alphabet) {
2✔
139
            this.openTransitions.add(this.hypothesis.createOpenTransition(initialState, i, this.adt.getRoot()));
2✔
140
        }
2✔
141

142
        this.closeTransitions();
2✔
143
    }
2✔
144

145
    @Override
146
    public boolean refineHypothesis(DefaultQuery<I, Word<O>> ce) {
147

148
        if (!MQUtil.isCounterexample(ce, this.hypothesis)) {
2✔
149
            return false;
2✔
150
        }
151

152
        this.evaluateSubtreeReplacement();
2✔
153

154
        this.openCounterExamples.add(ce);
2✔
155

156
        while (!this.openCounterExamples.isEmpty()) {
2✔
157

158
            // normal refinement step
159
            while (!this.openCounterExamples.isEmpty()) {
2✔
160

161
                @SuppressWarnings("nullness")
162
                // false positive https://github.com/typetools/checker-framework/issues/399
163
                final @NonNull DefaultQuery<I, Word<O>> currentCE = this.openCounterExamples.poll();
2✔
164
                this.allCounterExamples.add(currentCE);
2✔
165

166
                while (this.refineHypothesisInternal(currentCE)) {
2✔
167
                    // refine exhaustively
168
                }
169
            }
2✔
170

171
            // subtree replacements may reactivate old CEs
172
            for (DefaultQuery<I, Word<O>> oldCE : this.allCounterExamples) {
2✔
173
                if (MQUtil.isCounterexample(oldCE, this.hypothesis)) {
2✔
174
                    this.openCounterExamples.add(oldCE);
2✔
175
                }
176
            }
2✔
177

178
            ADTUtil.collectLeaves(this.adt.getRoot()).forEach(this::ensureConsistency);
2✔
179
        }
180

181
        return true;
2✔
182
    }
183

184
    public boolean refineHypothesisInternal(DefaultQuery<I, Word<O>> ceQuery) {
185

186
        if (!MQUtil.isCounterexample(ceQuery, this.hypothesis)) {
2✔
187
            return false;
2✔
188
        }
189

190
        // Determine a counterexample decomposition (u, a, v)
191
        final int suffixIdx =
2✔
192
                LocalSuffixFinders.RIVEST_SCHAPIRE.findSuffixIndex(ceQuery, this.hypothesis, this.hypothesis, this.mqo);
2✔
193

194
        if (suffixIdx == -1) {
2✔
195
            throw new IllegalStateException();
×
196
        }
197

198
        final Word<I> ceInput = ceQuery.getInput();
2✔
199

200
        final Word<I> u = ceInput.prefix(suffixIdx - 1);
2✔
201
        final Word<I> ua = ceInput.prefix(suffixIdx);
2✔
202
        final I a = ceInput.getSymbol(suffixIdx - 1);
2✔
203
        final Word<I> v = ceInput.subWord(suffixIdx);
2✔
204

205
        final ADTState<I, O> uState = this.hypothesis.getState(u);
2✔
206
        final ADTState<I, O> uaState = this.hypothesis.getState(ua);
2✔
207

208
        assert uState != null && uaState != null;
2✔
209

210
        final Word<I> uAccessSequence = uState.getAccessSequence();
2✔
211
        final Word<I> uaAccessSequence = uaState.getAccessSequence();
2✔
212
        final Word<I> uAccessSequenceWithA = uAccessSequence.append(a);
2✔
213

214
        final ADTState<I, O> newState = this.hypothesis.addState();
2✔
215
        newState.setAccessSequence(uAccessSequenceWithA);
2✔
216
        final ADTTransition<I, O> oldTrans = this.hypothesis.getTransition(uState, a);
2✔
217

218
        assert oldTrans != null;
2✔
219

220
        oldTrans.setTarget(newState);
2✔
221
        oldTrans.setIsSpanningTreeEdge(true);
2✔
222

223
        final ADTNode<ADTState<I, O>, I, O> nodeToSplit = findNodeForState(uaState);
2✔
224
        final ADTNode<ADTState<I, O>, I, O> newNode;
225

226
        // directly insert into observation tree, because we use it for finding a splitter
227
        this.observationTree.addState(newState, newState.getAccessSequence(), oldTrans.getOutput());
2✔
228
        this.observationTree.addTrace(newState, nodeToSplit);
2✔
229

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

233
        if (extension == null) {
2✔
234
            // directly insert into observation tree, because we use it for finding a splitter
235
            this.observationTree.addTrace(uaState, v, this.mqo.answerQuery(uaAccessSequence, v));
2✔
236
            this.observationTree.addTrace(newState, v, this.mqo.answerQuery(uAccessSequenceWithA, v));
2✔
237

238
            // in doubt, we will always find v
239
            final Word<I> otSepWord = this.observationTree.findSeparatingWord(uaState, newState);
2✔
240
            final Word<I> splitter;
241
            assert otSepWord != null;
2✔
242

243
            if (otSepWord.length() < v.length()) {
2✔
244
                splitter = otSepWord;
2✔
245
            } else {
246
                splitter = v;
2✔
247
            }
248

249
            final Word<O> oldOutput = this.observationTree.trace(uaState, splitter);
2✔
250
            final Word<O> newOutput = this.observationTree.trace(newState, splitter);
2✔
251

252
            newNode = this.adt.splitLeaf(nodeToSplit, splitter, oldOutput, newOutput, this.leafSplitter);
2✔
253
        } else {
2✔
254
            final Word<I> completeSplitter = previousTrace.concat(extension);
2✔
255
            final Word<O> oldOutput = this.observationTree.trace(uaState, completeSplitter);
2✔
256
            final Word<O> newOutput = this.observationTree.trace(newState, completeSplitter);
2✔
257

258
            newNode = this.adt.extendLeaf(nodeToSplit, completeSplitter, oldOutput, newOutput, this.leafSplitter);
2✔
259
        }
260
        newNode.setState(newState);
2✔
261

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

265
        for (I i : alphabet) {
2✔
266
            newTransitions.add(this.hypothesis.createOpenTransition(newState, i, this.adt.getRoot()));
2✔
267
        }
2✔
268

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

271
        for (ADTTransition<I, O> x : transitionsToRefine) {
2✔
272
            x.setTarget(null);
2✔
273
            x.setSiftNode(temporarySplitter);
2✔
274
        }
2✔
275

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

278
        for (ADTTransition<I, O> t : transitionsToRefine) {
2✔
279
            if (t.needsSifting()) {
2✔
280
                t.setSiftNode(finalizedSplitter);
2✔
281
                this.openTransitions.add(t);
2✔
282
            }
283
        }
2✔
284

285
        for (ADTTransition<I, O> t : newTransitions) {
2✔
286
            if (t.needsSifting()) {
2✔
287
                this.openTransitions.add(t);
2✔
288
            }
289
        }
2✔
290

291
        this.closeTransitions();
2✔
292
        return true;
2✔
293
    }
294

295
    private ADTNode<ADTState<I, O>, I, O> findNodeForState(ADTState<I, O> state) {
296

297
        for (ADTNode<ADTState<I, O>, I, O> leaf : ADTUtil.collectLeaves(this.adt.getRoot())) {
2✔
298
            if (leaf.getState().equals(state)) {
2✔
299
                return leaf;
2✔
300
            }
301
        }
2✔
302

NEW
303
        throw new IllegalStateException("Cannot find leaf for state " + state);
×
304

305
    }
306

307
    @Override
308
    public MealyMachine<?, I, ?, O> getHypothesisModel() {
309
        return this.hypothesis;
2✔
310
    }
311

312
    /**
313
     * Close all pending open transitions.
314
     */
315
    private void closeTransitions() {
316
        while (!this.openTransitions.isEmpty()) {
2✔
317

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

320
            //create a query object for every transition
321
            for (ADTTransition<I, O> transition : this.openTransitions) {
2✔
322
                if (transition.needsSifting()) {
2✔
323
                    queries.add(new ADTAdaptiveQuery<>(transition, transition.getSiftNode()));
2✔
324
                }
325
            }
2✔
326

327
            this.openTransitions.clear();
2✔
328
            this.oracle.processQueries(queries);
2✔
329

330
            for (ADTAdaptiveQuery<I, O> query : queries) {
2✔
331
                processAnsweredQuery(query);
2✔
332
            }
2✔
333
        }
2✔
334
    }
2✔
335

336
    @Override
337
    public void closeTransition(ADTState<I, O> state, I input) {
338

339
        final ADTTransition<I, O> transition = this.hypothesis.getTransition(state, input);
2✔
340
        assert transition != null;
2✔
341

342
        if (transition.needsSifting()) {
2✔
343
            final ADTNode<ADTState<I, O>, I, O> ads = transition.getSiftNode();
2✔
344
            final int oldNumberOfFinalStates = ADTUtil.collectLeaves(ads).size();
2✔
345

346
            final ADTAdaptiveQuery<I, O> query = new ADTAdaptiveQuery<>(transition, transition.getSiftNode());
2✔
347
            this.oracle.processQueries(Collections.singleton(query));
2✔
348
            processAnsweredQuery(query);
2✔
349

350
            final int newNumberOfFinalStates = ADTUtil.collectLeaves(ads).size();
2✔
351

352
            if (oldNumberOfFinalStates < newNumberOfFinalStates) {
2✔
353
                throw PartialTransitionAnalyzer.HYPOTHESIS_MODIFICATION_EXCEPTION;
2✔
354
            }
355
        }
356
    }
2✔
357

358
    private void processAnsweredQuery(ADTAdaptiveQuery<I, O> query) {
359
        if (query.needsPostProcessing()) {
2✔
360
            final ADTNode<ADTState<I, O>, I, O> parent = query.getCurrentADTNode();
2✔
361
            final O out = query.getTempOut();
2✔
362
            final ADTNode<ADTState<I, O>, I, O> succ = parent.getChild(out);
2✔
363

364
            // first time we process the successor
365
            if (succ == null) {
2✔
366
                // add new state to the hypothesis and set the accessSequence
367
                final ADTState<I, O> newState = this.hypothesis.addState();
2✔
368
                final Word<I> longPrefix = query.getAccessSequence().append(query.getTransition().getInput());
2✔
369
                newState.setAccessSequence(longPrefix);
2✔
370

371
                // configure the transition
372
                final ADTTransition<I, O> transition = query.getTransition();
2✔
373
                transition.setTarget(newState);
2✔
374
                transition.setIsSpanningTreeEdge(true);
2✔
375

376
                // add new leaf node to ADT
377
                final ADTNode<ADTState<I, O>, I, O> result = new ADTLeafNode<>(parent, newState);
2✔
378
                parent.getChildren().put(out, result);
2✔
379

380
                // add the observations to the observation tree
381
                O transitionOutput = query.getTransition().getOutput();
2✔
382
                this.observationTree.addState(newState, longPrefix, transitionOutput);
2✔
383

384
                // query successors
385
                for (I i : this.alphabet) {
2✔
386
                    this.openTransitions.add(this.hypothesis.createOpenTransition(newState, i, this.adt.getRoot()));
2✔
387
                }
2✔
388
            } else {
2✔
389
                assert ADTUtil.isLeafNode(succ);
2✔
390
                // state has been created before, just update target
391
                query.getTransition().setTarget(succ.getState());
2✔
392
            }
393
        } else {
2✔
394
            // update target
395
            final ADTTransition<I, O> transition = query.getTransition();
2✔
396
            final ADTNode<ADTState<I, O>, I, O> adtNode = query.getCurrentADTNode();
2✔
397
            assert ADTUtil.isLeafNode(adtNode);
2✔
398
            transition.setTarget(adtNode.getState());
2✔
399
        }
400
    }
2✔
401

402
    @Override
403
    public boolean isTransitionDefined(ADTState<I, O> state, I input) {
404
        final ADTTransition<I, O> transition = this.hypothesis.getTransition(state, input);
2✔
405
        assert transition != null;
2✔
406
        return !transition.needsSifting();
2✔
407
    }
408

409
    @Override
410
    public void addAlphabetSymbol(I symbol) {
411

412
        if (!this.alphabet.containsSymbol(symbol)) {
2✔
413
            this.alphabet.asGrowingAlphabetOrThrowException().addSymbol(symbol);
2✔
414
        }
415

416
        this.hypothesis.addAlphabetSymbol(symbol);
2✔
417
        this.observationTree.addAlphabetSymbol(symbol);
2✔
418

419
        // check if we already have information about the symbol (then the transition is defined) so we don't post
420
        // redundant queries
421
        if (this.hypothesis.getInitialState() != null &&
2✔
422
            this.hypothesis.getSuccessor(this.hypothesis.getInitialState(), symbol) == null) {
2✔
423
            for (ADTState<I, O> s : this.hypothesis.getStates()) {
2✔
424
                this.openTransitions.add(this.hypothesis.createOpenTransition(s, symbol, this.adt.getRoot()));
2✔
425
            }
2✔
426

427
            this.closeTransitions();
2✔
428
        }
429
    }
2✔
430

431
    @Override
432
    public ADTLearnerState<ADTState<I, O>, I, O> suspend() {
433
        return new ADTLearnerState<>(this.hypothesis, this.adt);
2✔
434
    }
435

436
    @Override
437
    public void resume(ADTLearnerState<ADTState<I, O>, I, O> state) {
438
        this.hypothesis = state.getHypothesis();
2✔
439
        this.adt = state.getAdt();
2✔
440

441
        final Alphabet<I> oldAlphabet = this.hypothesis.getInputAlphabet();
2✔
442
        if (!oldAlphabet.equals(this.alphabet)) {
2✔
443
            LOGGER.warn(Category.DATASTRUCTURE,
×
444
                        "The current alphabet '{}' differs from the resumed alphabet '{}'. Future behavior may be inconsistent",
445
                        this.alphabet,
446
                        oldAlphabet);
447
        }
448

449
        // startLearning has already been invoked
450
        if (this.hypothesis.size() > 0) {
2✔
451
            this.observationTree.initialize(this.hypothesis.getStates(),
2✔
452
                                            ADTState::getAccessSequence,
453
                                            this.hypothesis::computeOutput);
2✔
454
        }
455
    }
2✔
456

457
    /**
458
     * Ensure that the output behavior of a hypothesis state matches the observed output behavior recorded in the ADT.
459
     * Any differences in output behavior yields new counterexamples.
460
     *
461
     * @param leaf
462
     *         the leaf whose hypothesis state should be checked
463
     */
464
    private void ensureConsistency(ADTNode<ADTState<I, O>, I, O> leaf) {
465

466
        final ADTState<I, O> state = leaf.getState();
2✔
467
        final Word<I> as = state.getAccessSequence();
2✔
468
        final Word<O> asOut = this.hypothesis.computeOutput(as);
2✔
469

470
        ADTNode<ADTState<I, O>, I, O> iter = leaf;
2✔
471

472
        while (iter != null) {
2✔
473
            final Pair<Word<I>, Word<O>> trace = ADTUtil.buildTraceForNode(iter);
2✔
474

475
            final Word<I> input = trace.getFirst();
2✔
476
            final Word<O> output = trace.getSecond();
2✔
477

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

480
            if (!hypOut.equals(output)) {
2✔
481
                this.openCounterExamples.add(new DefaultQuery<>(as.concat(input), asOut.concat(output)));
2✔
482
            }
483

484
            iter = ADTUtil.getStartOfADS(iter).getParent();
2✔
485
        }
2✔
486
    }
2✔
487

488
    /**
489
     * Ask the current {@link #adtExtender} for a potential extension.
490
     *
491
     * @param ads
492
     *         the temporary ADS based on the inferred distinguishing suffix
493
     *
494
     * @return a validated ADT that can be used to distinguish the states referenced in the given temporary ADS
495
     */
496
    private ADTNode<ADTState<I, O>, I, O> evaluateAdtExtension(ADTNode<ADTState<I, O>, I, O> ads) {
497

498
        final ExtensionResult<ADTState<I, O>, I, O> potentialExtension =
2✔
499
                this.adtExtender.computeExtension(this.hypothesis, this, ads);
2✔
500

501
        if (potentialExtension.isCounterExample()) {
2✔
502
            this.openCounterExamples.add(potentialExtension.getCounterExample());
2✔
503
            return ads;
2✔
504
        } else if (!potentialExtension.isReplacement()) {
2✔
505
            return ads;
2✔
506
        }
507

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

511
        assert extension != null && nodeToReplace != null &&
2✔
512
               this.validateADS(nodeToReplace, extension, Collections.emptySet());
2✔
513

514
        final ADTNode<ADTState<I, O>, I, O> replacement = this.verifyADS(nodeToReplace,
2✔
515
                                                                         extension,
516
                                                                         ADTUtil.collectLeaves(this.adt.getRoot()),
2✔
517
                                                                         Collections.emptySet());
2✔
518

519
        // verification may have introduced reset nodes
520
        final int oldCosts = ADTUtil.computeEffectiveResets(nodeToReplace);
2✔
521
        final int newCosts = ADTUtil.computeEffectiveResets(replacement);
2✔
522

523
        if (newCosts >= oldCosts) {
2✔
524
            return ads;
2✔
525
        }
526

527
        // replace
528
        this.adt.replaceNode(nodeToReplace, replacement);
2✔
529

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

532
        // update
533
        this.resiftAffectedTransitions(ADTUtil.collectLeaves(extension), finalizedADS);
2✔
534

535
        return finalizedADS;
2✔
536
    }
537

538
    /**
539
     * Ask the {@link #subtreeReplacer} for any replacements.
540
     */
541
    private void evaluateSubtreeReplacement() {
542

543
        if (this.hypothesis.size() == 1) {
2✔
544
            // skip replacement if only one node is discovered
545
            return;
2✔
546
        }
547

548
        final Set<ReplacementResult<ADTState<I, O>, I, O>> potentialReplacements =
2✔
549
                this.subtreeReplacer.computeReplacements(this.hypothesis, this.alphabet, this.adt);
2✔
550
        final List<ReplacementResult<ADTState<I, O>, I, O>> validReplacements =
2✔
551
                new ArrayList<>(potentialReplacements.size());
2✔
552
        final Set<ADTNode<ADTState<I, O>, I, O>> cachedLeaves =
553
                potentialReplacements.isEmpty() ? Collections.emptySet() : ADTUtil.collectLeaves(this.adt.getRoot());
2✔
554

555
        for (ReplacementResult<ADTState<I, O>, I, O> potentialReplacement : potentialReplacements) {
2✔
556
            final ADTNode<ADTState<I, O>, I, O> proposedReplacement = potentialReplacement.getReplacement();
2✔
557
            final ADTNode<ADTState<I, O>, I, O> nodeToReplace = potentialReplacement.getNodeToReplace();
2✔
558

559
            assert this.validateADS(nodeToReplace, proposedReplacement, potentialReplacement.getCutoutNodes());
2✔
560

561
            final ADTNode<ADTState<I, O>, I, O> replacement = this.verifyADS(nodeToReplace,
2✔
562
                                                                             proposedReplacement,
563
                                                                             cachedLeaves,
564
                                                                             potentialReplacement.getCutoutNodes());
2✔
565

566
            // verification may have introduced reset nodes
567
            final int oldCosts = ADTUtil.computeEffectiveResets(nodeToReplace);
2✔
568
            final int newCosts = ADTUtil.computeEffectiveResets(replacement);
2✔
569

570
            if (newCosts >= oldCosts) {
2✔
571
                continue;
2✔
572
            }
573

574
            validReplacements.add(new ReplacementResult<>(nodeToReplace, replacement));
2✔
575
        }
2✔
576

577
        for (ReplacementResult<ADTState<I, O>, I, O> potentialReplacement : validReplacements) {
2✔
578
            final ADTNode<ADTState<I, O>, I, O> replacement = potentialReplacement.getReplacement();
2✔
579
            final ADTNode<ADTState<I, O>, I, O> nodeToReplace = potentialReplacement.getNodeToReplace();
2✔
580

581
            this.adt.replaceNode(nodeToReplace, replacement);
2✔
582

583
            this.resiftAffectedTransitions(ADTUtil.collectLeaves(replacement), ADTUtil.getStartOfADS(replacement));
2✔
584
        }
2✔
585

586
        this.closeTransitions();
2✔
587
    }
2✔
588

589
    /**
590
     * Validate the well-definedness of an ADT replacement, i.e. both ADTs cover the same set of hypothesis states and
591
     * the output behavior described in the replacement matches the hypothesis output.
592
     *
593
     * @param oldADS
594
     *         the old ADT (subtree) to be replaced
595
     * @param newADS
596
     *         the new ADT (subtree)
597
     * @param cutout
598
     *         the set of states not covered by the new ADT
599
     *
600
     * @return {@code true} if the replacement is valid, {@code false} otherwise.
601
     */
602
    private boolean validateADS(ADTNode<ADTState<I, O>, I, O> oldADS,
603
                                ADTNode<ADTState<I, O>, I, O> newADS,
604
                                Set<ADTState<I, O>> cutout) {
605

606
        final Set<ADTNode<ADTState<I, O>, I, O>> oldNodes;
607

608
        if (ADTUtil.isResetNode(oldADS)) {
2✔
609
            oldNodes = ADTUtil.collectResetNodes(this.adt.getRoot());
2✔
610
        } else {
611
            oldNodes = ADTUtil.collectADSNodes(this.adt.getRoot(), true);
2✔
612
        }
613

614
        if (!oldNodes.contains(oldADS)) {
2✔
615
            throw new IllegalArgumentException("Subtree to replace does not exist");
×
616
        }
617

618
        final Set<ADTNode<ADTState<I, O>, I, O>> newFinalNodes = ADTUtil.collectLeaves(newADS);
2✔
619
        final Map<ADTState<I, O>, Pair<Word<I>, Word<O>>> traces =
2✔
620
                new HashMap<>(HashUtil.capacity(newFinalNodes.size()));
2✔
621

622
        for (ADTNode<ADTState<I, O>, I, O> n : newFinalNodes) {
2✔
623
            traces.put(n.getState(), ADTUtil.buildTraceForNode(n));
2✔
624
        }
2✔
625

626
        final Set<ADTState<I, O>> oldFinalStates = ADTUtil.collectHypothesisStates(oldADS);
2✔
627
        final Set<ADTState<I, O>> newFinalStates = new HashSet<>(traces.keySet());
2✔
628
        newFinalStates.addAll(cutout);
2✔
629

630
        if (!oldFinalStates.equals(newFinalStates)) {
2✔
631
            throw new IllegalArgumentException("New ADS does not cover all old nodes");
×
632
        }
633

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

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

638
            final Word<I> accessSequence = entry.getKey().getAccessSequence();
2✔
639
            final Word<I> prefix = accessSequence.concat(parentInputTrace);
2✔
640
            final Word<I> input = entry.getValue().getFirst();
2✔
641
            final Word<O> output = entry.getValue().getSecond();
2✔
642

643
            if (!this.hypothesis.computeSuffixOutput(prefix, input).equals(output)) {
2✔
644
                throw new IllegalArgumentException("Output of new ADS does not match hypothesis");
×
645
            }
646
        }
2✔
647

648
        return true;
2✔
649
    }
650

651
    /**
652
     * Verify the proposed ADT replacement by checking the actual behavior of the system under learning. During the
653
     * verification process, the system under learning may behave differently from what the ADT replacement suggests:
654
     * This means a counterexample is witnessed and added to the queue of counterexamples for later investigation.
655
     * Albeit observing diverging behavior, this method continues to trying to construct a valid ADT using the observed
656
     * output. If for two states, no distinguishing output can be observed, the states a separated by means of
657
     * {@link #resolveAmbiguities(ADTNode, ADTNode, ADTState, Set)}.
658
     *
659
     * @param nodeToReplace
660
     *         the old ADT (subtree) to be replaced
661
     * @param replacement
662
     *         the new ADT (subtree). Must have the form of an ADS, i.e. no reset nodes
663
     * @param cachedLeaves
664
     *         a set containing the leaves of the current tree, so they don't have to be re-fetched for every
665
     *         replacement verification
666
     * @param cutout
667
     *         the set of states not covered by the new ADT
668
     *
669
     * @return A verified ADT that correctly distinguishes the states covered by the original ADT
670
     */
671
    private ADTNode<ADTState<I, O>, I, O> verifyADS(ADTNode<ADTState<I, O>, I, O> nodeToReplace,
672
                                                    ADTNode<ADTState<I, O>, I, O> replacement,
673
                                                    Set<ADTNode<ADTState<I, O>, I, O>> cachedLeaves,
674
                                                    Set<ADTState<I, O>> cutout) {
675
        final Set<ADTNode<ADTState<I, O>, I, O>> leaves = ADTUtil.collectLeaves(replacement);
2✔
676
        final Map<ADTState<I, O>, Pair<Word<I>, Word<O>>> traces =
2✔
677
                new LinkedHashMap<>(HashUtil.capacity(leaves.size()));
2✔
678

679
        for (ADTNode<ADTState<I, O>, I, O> leaf : leaves) {
2✔
680
            traces.put(leaf.getState(), ADTUtil.buildTraceForNode(leaf));
2✔
681
        }
2✔
682

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

685
        ADTNode<ADTState<I, O>, I, O> result = null;
2✔
686

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

689
        for (Entry<ADTState<I, O>, Pair<Word<I>, Word<O>>> e : traces.entrySet()) {
2✔
690
            final ADTState<I, O> state = e.getKey();
2✔
691
            final Pair<Word<I>, Word<O>> ads = e.getValue();
2✔
692
            queries.add(new ADSVerificationQuery<>(state.getAccessSequence().concat(parentTrace.getFirst()),
2✔
693
                                                   ads.getFirst(),
2✔
694
                                                   ads.getSecond(),
2✔
695
                                                   state));
696
        }
2✔
697

698
        this.oracle.processQueries(queries);
2✔
699

700
        for (ADSVerificationQuery<I, O> query : queries) {
2✔
701
            final ADTNode<ADTState<I, O>, I, O> trace;
702
            final DefaultQuery<I, Word<O>> ce = query.getCounterexample();
2✔
703

704
            if (ce != null) {
2✔
705
                this.openCounterExamples.add(ce);
2✔
706
                trace = ADTUtil.buildADSFromObservation(ce.getSuffix(), ce.getOutput(), query.getState());
2✔
707
            } else {
708
                trace = ADTUtil.buildADSFromObservation(query.getSuffix(), query.getExpectedOutput(), query.getState());
2✔
709
            }
710

711
            if (result == null) {
2✔
712
                result = trace;
2✔
713
            } else {
714
                if (!ADTUtil.mergeADS(result, trace)) {
2✔
715
                    this.resolveAmbiguities(nodeToReplace, result, query.getState(), cachedLeaves);
2✔
716
                }
717
            }
718
        }
2✔
719

720
        for (ADTState<I, O> s : cutout) {
2✔
721
            this.resolveAmbiguities(nodeToReplace, result, s, cachedLeaves);
2✔
722
        }
2✔
723

724
        return result;
2✔
725
    }
726

727
    /**
728
     * If two states show the same output behavior resolve this ambiguity by adding a reset node and add a new (sub) ADS
729
     * based on the lowest common ancestor in the existing ADT.
730
     *
731
     * @param nodeToReplace
732
     *         the old ADT (subtree) to be replaced
733
     * @param newADS
734
     *         the new ADT (subtree)
735
     * @param state
736
     *         the state which cannot be distinguished using the given replacement
737
     * @param cachedLeaves
738
     *         a set containing the leaves of the current tree, so they don't have to be re-fetched for every
739
     *         replacement verification
740
     */
741
    private void resolveAmbiguities(ADTNode<ADTState<I, O>, I, O> nodeToReplace,
742
                                    ADTNode<ADTState<I, O>, I, O> newADS,
743
                                    ADTState<I, O> state,
744
                                    Set<ADTNode<ADTState<I, O>, I, O>> cachedLeaves) {
745

746
        final Pair<Word<I>, Word<O>> parentTrace = ADTUtil.buildTraceForNode(nodeToReplace);
2✔
747
        final ADSAmbiguityQuery<I, O> query =
2✔
748
                new ADSAmbiguityQuery<>(state.getAccessSequence(), parentTrace.getFirst(), newADS);
2✔
749

750
        this.oracle.processQuery(query);
2✔
751

752
        if (query.needsPostProcessing()) {
2✔
753
            final ADTNode<ADTState<I, O>, I, O> prev = query.getCurrentADTNode();
2✔
754
            final ADTNode<ADTState<I, O>, I, O> newFinal = new ADTLeafNode<>(prev, state);
2✔
755
            prev.getChildren().put(query.getTempOut(), newFinal);
2✔
756
            return;
2✔
757
        }
758

759
        final ADTNode<ADTState<I, O>, I, O> finalNode = query.getCurrentADTNode();
2✔
760
        ADTNode<ADTState<I, O>, I, O> oldReference = null, newReference = null;
2✔
761

762
        for (ADTNode<ADTState<I, O>, I, O> leaf : cachedLeaves) {
2✔
763
            final ADTState<I, O> hypState = leaf.getState();
2✔
764

765
            if (hypState.equals(finalNode.getState())) {
2✔
766
                oldReference = leaf;
2✔
767
            } else if (hypState.equals(state)) {
2✔
768
                newReference = leaf;
2✔
769
            }
770

771
            if (oldReference != null && newReference != null) {
2✔
772
                break;
2✔
773
            }
774
        }
2✔
775

776
        assert oldReference != null && newReference != null;
2✔
777
        final LCAInfo<ADTState<I, O>, I, O> lcaResult = this.adt.findLCA(oldReference, newReference);
2✔
778
        final ADTNode<ADTState<I, O>, I, O> lca = lcaResult.adtNode;
2✔
779
        final Pair<Word<I>, Word<O>> lcaTrace = ADTUtil.buildTraceForNode(lca);
2✔
780

781
        final Word<I> sepWord = lcaTrace.getFirst().append(lca.getSymbol());
2✔
782
        final Word<O> oldOutputTrace = lcaTrace.getSecond().append(lcaResult.firstOutput);
2✔
783
        final Word<O> newOutputTrace = lcaTrace.getSecond().append(lcaResult.secondOutput);
2✔
784

785
        final ADTNode<ADTState<I, O>, I, O> oldTrace =
2✔
786
                ADTUtil.buildADSFromObservation(sepWord, oldOutputTrace, finalNode.getState());
2✔
787
        final ADTNode<ADTState<I, O>, I, O> newTrace = ADTUtil.buildADSFromObservation(sepWord, newOutputTrace, state);
2✔
788

789
        if (!ADTUtil.mergeADS(oldTrace, newTrace)) {
2✔
790
            throw new IllegalStateException("Should never happen");
×
791
        }
792

793
        final ADTNode<ADTState<I, O>, I, O> reset = new ADTResetNode<>(oldTrace);
2✔
794
        final ADTNode<ADTState<I, O>, I, O> parent = finalNode.getParent();
2✔
795
        assert parent != null;
2✔
796
        final O parentOutput = ADTUtil.getOutputForSuccessor(parent, finalNode);
2✔
797

798
        parent.getChildren().put(parentOutput, reset);
2✔
799
        reset.setParent(parent);
2✔
800
        oldTrace.setParent(reset);
2✔
801
    }
2✔
802

803
    /**
804
     * Schedule all incoming transitions of the given states to be re-sifted against the given ADT (subtree).
805
     *
806
     * @param states
807
     *         A set of states, whose incoming transitions should be sifted
808
     * @param finalizedADS
809
     *         the ADT (subtree) to sift through
810
     */
811
    private void resiftAffectedTransitions(Set<ADTNode<ADTState<I, O>, I, O>> states,
812
                                           ADTNode<ADTState<I, O>, I, O> finalizedADS) {
813

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

816
            for (ADTTransition<I, O> trans : getIncomingNonSpanningTreeTransitions(state.getState())) {
2✔
817
                trans.setTarget(null);
2✔
818
                trans.setSiftNode(finalizedADS);
2✔
819
                this.openTransitions.add(trans);
2✔
820
            }
2✔
821
        }
2✔
822
    }
2✔
823

824
    private List<ADTTransition<I, O>> getIncomingNonSpanningTreeTransitions(ADTState<I, O> state) {
825
        final Set<ADTTransition<I, O>> transitions = state.getIncomingTransitions();
2✔
826
        final List<ADTTransition<I, O>> result = new ArrayList<>(transitions.size());
2✔
827

828
        for (ADTTransition<I, O> t : transitions) {
2✔
829
            if (!t.isSpanningTreeEdge()) {
2✔
830
                result.add(t);
2✔
831
            }
832
        }
2✔
833

834
        return result;
2✔
835
    }
836

837
    public ADT<ADTState<I, O>, I, O> getADT() {
838
        return adt;
2✔
839
    }
840

841
    static final class BuilderDefaults {
842

843
        private BuilderDefaults() {
844
            // prevent instantiation
845
        }
846

847
        public static LeafSplitter leafSplitter() {
848
            return LeafSplitters.DEFAULT_SPLITTER;
2✔
849
        }
850

851
        public static ADTExtender adtExtender() {
852
            return ADTExtenders.EXTEND_BEST_EFFORT;
2✔
853
        }
854

855
        public static SubtreeReplacer subtreeReplacer() {
856
            return SubtreeReplacers.LEVELED_BEST_EFFORT;
2✔
857
        }
858

859
        public static boolean useObservationTree() {
860
            return true;
2✔
861
        }
862
    }
863
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc