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

LearnLib / learnlib / 6433387082

06 Oct 2023 03:10PM UTC coverage: 92.296% (-0.007%) from 92.303%
6433387082

push

github

mtf90
update Falk's developer id

11573 of 12539 relevant lines covered (92.3%)

1.67 hits per line

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

98.39
/algorithms/active/adt/src/main/java/de/learnlib/algorithms/adt/learner/ADTLearner.java
1
/* Copyright (C) 2013-2023 TU Dortmund
2
 * This file is part of LearnLib, http://www.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.algorithms.adt.learner;
17

18
import java.util.ArrayDeque;
19
import java.util.ArrayList;
20
import java.util.Collections;
21
import java.util.Iterator;
22
import java.util.LinkedHashMap;
23
import java.util.LinkedHashSet;
24
import java.util.List;
25
import java.util.Map;
26
import java.util.Optional;
27
import java.util.Queue;
28
import java.util.Set;
29
import java.util.stream.Collectors;
30

31
import com.github.misberner.buildergen.annotations.GenerateBuilder;
32
import de.learnlib.algorithms.adt.adt.ADT;
33
import de.learnlib.algorithms.adt.adt.ADT.LCAInfo;
34
import de.learnlib.algorithms.adt.adt.ADTLeafNode;
35
import de.learnlib.algorithms.adt.adt.ADTNode;
36
import de.learnlib.algorithms.adt.adt.ADTResetNode;
37
import de.learnlib.algorithms.adt.api.ADTExtender;
38
import de.learnlib.algorithms.adt.api.LeafSplitter;
39
import de.learnlib.algorithms.adt.api.PartialTransitionAnalyzer;
40
import de.learnlib.algorithms.adt.api.SubtreeReplacer;
41
import de.learnlib.algorithms.adt.automaton.ADTHypothesis;
42
import de.learnlib.algorithms.adt.automaton.ADTState;
43
import de.learnlib.algorithms.adt.automaton.ADTTransition;
44
import de.learnlib.algorithms.adt.config.ADTExtenders;
45
import de.learnlib.algorithms.adt.config.LeafSplitters;
46
import de.learnlib.algorithms.adt.config.SubtreeReplacers;
47
import de.learnlib.algorithms.adt.model.ExtensionResult;
48
import de.learnlib.algorithms.adt.model.ObservationTree;
49
import de.learnlib.algorithms.adt.model.ReplacementResult;
50
import de.learnlib.algorithms.adt.util.ADTUtil;
51
import de.learnlib.algorithms.adt.util.SQOOTBridge;
52
import de.learnlib.api.Resumable;
53
import de.learnlib.api.algorithm.LearningAlgorithm;
54
import de.learnlib.api.oracle.SymbolQueryOracle;
55
import de.learnlib.api.query.DefaultQuery;
56
import de.learnlib.counterexamples.LocalSuffixFinders;
57
import de.learnlib.util.MQUtil;
58
import net.automatalib.SupportsGrowingAlphabet;
59
import net.automatalib.automata.transducers.MealyMachine;
60
import net.automatalib.commons.util.Pair;
61
import net.automatalib.words.Alphabet;
62
import net.automatalib.words.Word;
63
import net.automatalib.words.WordBuilder;
64
import net.automatalib.words.impl.Alphabets;
65
import org.slf4j.Logger;
66
import org.slf4j.LoggerFactory;
67

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

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

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

95
    public ADTLearner(Alphabet<I> alphabet,
96
                      SymbolQueryOracle<I, O> oracle,
97
                      LeafSplitter leafSplitter,
98
                      ADTExtender adtExtender,
99
                      SubtreeReplacer subtreeReplacer) {
100
        this(alphabet, oracle, leafSplitter, adtExtender, subtreeReplacer, true);
2✔
101
    }
2✔
102

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

111
        this.alphabet = alphabet;
2✔
112
        this.observationTree = new ObservationTree<>(this.alphabet);
2✔
113
        this.oracle = new SQOOTBridge<>(this.observationTree, oracle, useObservationTree);
2✔
114

115
        this.leafSplitter = leafSplitter;
2✔
116
        this.adtExtender = adtExtender;
2✔
117
        this.subtreeReplacer = subtreeReplacer;
2✔
118

119
        this.hypothesis = new ADTHypothesis<>(this.alphabet);
2✔
120
        this.openTransitions = new ArrayDeque<>();
2✔
121
        this.openCounterExamples = new ArrayDeque<>();
2✔
122
        this.allCounterExamples = new LinkedHashSet<>();
2✔
123
        this.adt = new ADT<>();
2✔
124
    }
2✔
125

126
    @Override
127
    public void startLearning() {
128

129
        final ADTState<I, O> initialState = this.hypothesis.addInitialState();
2✔
130
        initialState.setAccessSequence(Word.epsilon());
2✔
131
        this.observationTree.initialize(initialState);
2✔
132
        this.oracle.initialize();
2✔
133
        this.adt.initialize(initialState);
2✔
134

135
        for (I i : this.alphabet) {
2✔
136
            this.openTransitions.add(this.hypothesis.createOpenTransition(initialState, i, this.adt.getRoot()));
2✔
137
        }
2✔
138

139
        this.closeTransitions();
2✔
140
    }
2✔
141

142
    @Override
143
    public boolean refineHypothesis(DefaultQuery<I, Word<O>> ce) {
144

145
        if (!MQUtil.isCounterexample(ce, this.hypothesis)) {
2✔
146
            return false;
2✔
147
        }
148

149
        this.evaluateSubtreeReplacement();
2✔
150

151
        this.openCounterExamples.add(ce);
2✔
152

153
        while (!this.openCounterExamples.isEmpty()) {
2✔
154

155
            // normal refinement step
156
            while (!this.openCounterExamples.isEmpty()) {
2✔
157

158
                final DefaultQuery<I, Word<O>> currentCE = this.openCounterExamples.poll();
2✔
159
                this.allCounterExamples.add(currentCE);
2✔
160

161
                while (this.refineHypothesisInternal(currentCE)) {}
2✔
162
            }
2✔
163

164
            // subtree replacements may reactivate old CEs
165
            for (DefaultQuery<I, Word<O>> oldCE : this.allCounterExamples) {
2✔
166
                if (!this.hypothesis.computeOutput(oldCE.getInput()).equals(oldCE.getOutput())) {
2✔
167
                    this.openCounterExamples.add(oldCE);
2✔
168
                }
169
            }
2✔
170

171
            ADTUtil.collectLeaves(this.adt.getRoot()).forEach(this::ensureConsistency);
2✔
172
        }
173

174
        return true;
2✔
175
    }
176

177
    public boolean refineHypothesisInternal(DefaultQuery<I, Word<O>> ceQuery) {
178

179
        if (!MQUtil.isCounterexample(ceQuery, this.hypothesis)) {
2✔
180
            return false;
2✔
181
        }
182

183
        // Determine a counterexample decomposition (u, a, v)
184
        final int suffixIdx = LocalSuffixFinders.RIVEST_SCHAPIRE.findSuffixIndex(ceQuery,
2✔
185
                                                                                 this.hypothesis,
186
                                                                                 this.hypothesis,
187
                                                                                 this.oracle);
188

189
        if (suffixIdx == -1) {
2✔
190
            throw new IllegalStateException();
×
191
        }
192

193
        final Word<I> ceInput = ceQuery.getInput();
2✔
194

195
        final Word<I> u = ceInput.prefix(suffixIdx - 1);
2✔
196
        final Word<I> ua = ceInput.prefix(suffixIdx);
2✔
197
        final I a = ceInput.getSymbol(suffixIdx - 1);
2✔
198
        final Word<I> v = ceInput.subWord(suffixIdx);
2✔
199

200
        final ADTState<I, O> uState = this.hypothesis.getState(u);
2✔
201
        final ADTState<I, O> uaState = this.hypothesis.getState(ua);
2✔
202

203
        assert uState != null && uaState != null;
2✔
204

205
        final Word<I> uAccessSequence = uState.getAccessSequence();
2✔
206
        final Word<I> uaAccessSequence = uaState.getAccessSequence();
2✔
207
        final Word<I> uAccessSequenceWithA = uAccessSequence.append(a);
2✔
208

209
        final ADTState<I, O> newState = this.hypothesis.addState();
2✔
210
        newState.setAccessSequence(uAccessSequenceWithA);
2✔
211
        final ADTTransition<I, O> oldTrans = this.hypothesis.getTransition(uState, a);
2✔
212

213
        assert oldTrans != null;
2✔
214

215
        oldTrans.setTarget(newState);
2✔
216
        oldTrans.setIsSpanningTreeEdge(true);
2✔
217

218
        final Set<ADTNode<ADTState<I, O>, I, O>> finalNodes = ADTUtil.collectLeaves(this.adt.getRoot());
2✔
219
        final ADTNode<ADTState<I, O>, I, O> nodeToSplit = finalNodes.stream()
2✔
220
                                                                    .filter(n -> uaState.equals(n.getHypothesisState()))
2✔
221
                                                                    .findFirst()
2✔
222
                                                                    .orElseThrow(IllegalStateException::new);
2✔
223

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 Optional<Word<I>> extension = this.observationTree.findSeparatingWord(uaState, newState, previousTrace);
2✔
232

233
        if (extension.isPresent()) {
2✔
234
            final Word<I> completeSplitter = previousTrace.concat(extension.get());
2✔
235
            final Word<O> oldOutput = this.observationTree.trace(uaState, completeSplitter);
2✔
236
            final Word<O> newOutput = this.observationTree.trace(newState, completeSplitter);
2✔
237

238
            newNode = this.adt.extendLeaf(nodeToSplit, completeSplitter, oldOutput, newOutput, this.leafSplitter);
2✔
239
        } else {
2✔
240
            // directly insert into observation tree, because we use it for finding a splitter
241
            this.observationTree.addTrace(uaState, v, this.oracle.answerQuery(uaAccessSequence, v));
2✔
242
            this.observationTree.addTrace(newState, v, this.oracle.answerQuery(uAccessSequenceWithA, v));
2✔
243

244
            // in doubt, we will always find v
245
            final Word<I> otSepWord = this.observationTree.findSeparatingWord(uaState, newState);
2✔
246
            final Word<I> splitter;
247

248
            if (otSepWord.length() < v.length()) {
2✔
249
                splitter = otSepWord;
2✔
250
            } else {
251
                splitter = v;
2✔
252
            }
253

254
            final Word<O> oldOutput = this.observationTree.trace(uaState, splitter);
2✔
255
            final Word<O> newOutput = this.observationTree.trace(newState, splitter);
2✔
256

257
            newNode = this.adt.splitLeaf(nodeToSplit, splitter, oldOutput, newOutput, this.leafSplitter);
2✔
258
        }
259
        newNode.setHypothesisState(newState);
2✔
260

261
        final ADTNode<ADTState<I, O>, I, O> temporarySplitter = ADTUtil.getStartOfADS(nodeToSplit);
2✔
262
        final List<ADTTransition<I, O>> newTransitions = alphabet.stream()
2✔
263
                                                                 .map(i -> this.hypothesis.createOpenTransition(newState,
2✔
264
                                                                                                                i,
265
                                                                                                                this.adt.getRoot()))
2✔
266
                                                                 .collect(Collectors.toList());
2✔
267
        final List<ADTTransition<I, O>> transitionsToRefine = nodeToSplit.getHypothesisState()
2✔
268
                                                                         .getIncomingTransitions()
2✔
269
                                                                         .stream()
2✔
270
                                                                         .filter(x -> !x.isSpanningTreeEdge())
2✔
271
                                                                         .collect(Collectors.toList());
2✔
272

273
        transitionsToRefine.forEach(x -> {
2✔
274
            x.setTarget(null);
2✔
275
            x.setSiftNode(temporarySplitter);
2✔
276
        });
2✔
277

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

280
        transitionsToRefine.stream().filter(ADTTransition::needsSifting).forEach(x -> {
2✔
281
            x.setSiftNode(finalizedSplitter);
2✔
282
            this.openTransitions.add(x);
2✔
283
        });
2✔
284
        newTransitions.stream().filter(ADTTransition::needsSifting).forEach(this.openTransitions::add);
2✔
285

286
        this.closeTransitions();
2✔
287
        return true;
2✔
288
    }
289

290
    @Override
291
    public MealyMachine<?, I, ?, O> getHypothesisModel() {
292
        return this.hypothesis;
2✔
293
    }
294

295
    /**
296
     * Close all pending open transitions.
297
     */
298
    private void closeTransitions() {
299
        while (!this.openTransitions.isEmpty()) {
2✔
300
            this.closeTransition(this.openTransitions.poll());
2✔
301
        }
302
    }
2✔
303

304
    /**
305
     * Close the given transitions by means of sifting the associated long prefix through the ADT.
306
     *
307
     * @param transition
308
     *         the transition to close
309
     */
310
    private void closeTransition(ADTTransition<I, O> transition) {
311

312
        if (!transition.needsSifting()) {
2✔
313
            return;
2✔
314
        }
315

316
        final Word<I> accessSequence = transition.getSource().getAccessSequence();
2✔
317
        final I symbol = transition.getInput();
2✔
318

319
        this.oracle.reset();
2✔
320
        for (I i : accessSequence) {
2✔
321
            this.oracle.query(i);
2✔
322
        }
2✔
323

324
        transition.setOutput(this.oracle.query(symbol));
2✔
325

326
        final Word<I> longPrefix = accessSequence.append(symbol);
2✔
327
        final ADTNode<ADTState<I, O>, I, O> finalNode =
2✔
328
                this.adt.sift(this.oracle, longPrefix, transition.getSiftNode());
2✔
329

330
        assert ADTUtil.isLeafNode(finalNode);
2✔
331

332
        final ADTState<I, O> targetState;
333

334
        // new state discovered while sifting
335
        if (finalNode.getHypothesisState() == null) {
2✔
336
            targetState = this.hypothesis.addState();
2✔
337
            targetState.setAccessSequence(longPrefix);
2✔
338

339
            finalNode.setHypothesisState(targetState);
2✔
340
            transition.setIsSpanningTreeEdge(true);
2✔
341

342
            this.observationTree.addState(targetState, longPrefix, transition.getOutput());
2✔
343

344
            for (I i : this.alphabet) {
2✔
345
                this.openTransitions.add(this.hypothesis.createOpenTransition(targetState, i, this.adt.getRoot()));
2✔
346
            }
2✔
347
        } else {
348
            targetState = finalNode.getHypothesisState();
2✔
349
        }
350

351
        transition.setTarget(targetState);
2✔
352
    }
2✔
353

354
    @Override
355
    public void closeTransition(ADTState<I, O> state, I input) {
356

357
        final ADTTransition<I, O> transition = this.hypothesis.getTransition(state, input);
2✔
358
        assert transition != null;
2✔
359

360
        if (transition.needsSifting()) {
2✔
361
            final ADTNode<ADTState<I, O>, I, O> ads = transition.getSiftNode();
2✔
362
            final int oldNumberOfFinalStates = ADTUtil.collectLeaves(ads).size();
2✔
363

364
            this.closeTransition(transition);
2✔
365

366
            final int newNumberOfFinalStates = ADTUtil.collectLeaves(ads).size();
2✔
367

368
            if (oldNumberOfFinalStates < newNumberOfFinalStates) {
2✔
369
                throw PartialTransitionAnalyzer.HYPOTHESIS_MODIFICATION_EXCEPTION;
2✔
370
            }
371
        }
372
    }
2✔
373

374
    @Override
375
    public boolean isTransitionDefined(ADTState<I, O> state, I input) {
376
        final ADTTransition<I, O> transition = this.hypothesis.getTransition(state, input);
2✔
377
        assert transition != null;
2✔
378
        return !transition.needsSifting();
2✔
379
    }
380

381
    @Override
382
    public void addAlphabetSymbol(I symbol) {
383

384
        if (!this.alphabet.containsSymbol(symbol)) {
2✔
385
            Alphabets.toGrowingAlphabetOrThrowException(this.alphabet).addSymbol(symbol);
2✔
386
        }
387

388
        this.hypothesis.addAlphabetSymbol(symbol);
2✔
389
        this.observationTree.getObservationTree().addAlphabetSymbol(symbol);
2✔
390

391
        // check if we already have information about the symbol (then the transition is defined) so we don't post
392
        // redundant queries
393
        if (this.hypothesis.getInitialState() != null &&
2✔
394
            this.hypothesis.getSuccessor(this.hypothesis.getInitialState(), symbol) == null) {
2✔
395
            for (ADTState<I, O> s : this.hypothesis.getStates()) {
2✔
396
                this.openTransitions.add(this.hypothesis.createOpenTransition(s, symbol, this.adt.getRoot()));
2✔
397
            }
2✔
398

399
            this.closeTransitions();
2✔
400
        }
401
    }
2✔
402

403
    @Override
404
    public ADTLearnerState<ADTState<I, O>, I, O> suspend() {
405
        return new ADTLearnerState<>(this.hypothesis, this.adt);
2✔
406
    }
407

408
    @Override
409
    public void resume(ADTLearnerState<ADTState<I, O>, I, O> state) {
410
        this.hypothesis = state.getHypothesis();
2✔
411
        this.adt = state.getAdt();
2✔
412

413
        final Alphabet<I> oldAlphabet = this.hypothesis.getInputAlphabet();
2✔
414
        if (!oldAlphabet.equals(this.alphabet)) {
2✔
415
            LOGGER.warn(
×
416
                    "The current alphabet '{}' differs from the resumed alphabet '{}'. Future behavior may be inconsistent",
417
                    this.alphabet,
418
                    oldAlphabet);
419
        }
420

421
        // startLearning has already been invoked
422
        if (this.hypothesis.size() > 0) {
2✔
423
            this.observationTree.initialize(this.hypothesis.getStates(),
2✔
424
                                            ADTState::getAccessSequence,
425
                                            this.hypothesis::computeOutput);
426
            this.oracle.initialize();
2✔
427
        }
428
    }
2✔
429

430
    /**
431
     * Ensure that the output behavior of a hypothesis state matches the observed output behavior recorded in the ADT.
432
     * Any differences in output behavior yields new counterexamples.
433
     *
434
     * @param leaf
435
     *         the leaf whose hypothesis state should be checked
436
     */
437
    private void ensureConsistency(ADTNode<ADTState<I, O>, I, O> leaf) {
438

439
        final ADTState<I, O> state = leaf.getHypothesisState();
2✔
440
        final Word<I> as = state.getAccessSequence();
2✔
441
        final Word<O> asOut = this.hypothesis.computeOutput(as);
2✔
442

443
        ADTNode<ADTState<I, O>, I, O> iter = leaf;
2✔
444

445
        while (iter != null) {
2✔
446
            final Pair<Word<I>, Word<O>> trace = ADTUtil.buildTraceForNode(iter);
2✔
447

448
            final Word<I> input = trace.getFirst();
2✔
449
            final Word<O> output = trace.getSecond();
2✔
450

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

453
            if (!hypOut.equals(output)) {
2✔
454
                this.openCounterExamples.add(new DefaultQuery<>(as.concat(input), asOut.concat(output)));
2✔
455
            }
456

457
            iter = ADTUtil.getStartOfADS(iter).getParent();
2✔
458
        }
2✔
459
    }
2✔
460

461
    /**
462
     * Ask the current {@link #adtExtender} for a potential extension.
463
     *
464
     * @param ads
465
     *         the temporary ADS based on the inferred distinguishing suffix
466
     *
467
     * @return a validated ADT that can be used to distinguish the states referenced in the given temporary ADS
468
     */
469
    private ADTNode<ADTState<I, O>, I, O> evaluateAdtExtension(ADTNode<ADTState<I, O>, I, O> ads) {
470

471
        final ExtensionResult<ADTState<I, O>, I, O> potentialExtension =
2✔
472
                this.adtExtender.computeExtension(this.hypothesis, this, ads);
2✔
473

474
        if (potentialExtension.isCounterExample()) {
2✔
475
            this.openCounterExamples.add(potentialExtension.getCounterExample());
2✔
476
            return ads;
2✔
477
        } else if (!potentialExtension.isReplacement()) {
2✔
478
            return ads;
2✔
479
        }
480

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

484
        assert this.validateADS(nodeToReplace, extension, Collections.emptySet());
2✔
485

486
        final ADTNode<ADTState<I, O>, I, O> replacement = this.verifyADS(nodeToReplace,
2✔
487
                                                                         extension,
488
                                                                         ADTUtil.collectLeaves(this.adt.getRoot()),
2✔
489
                                                                         Collections.emptySet());
2✔
490

491
        // verification may have introduced reset nodes
492
        final int oldCosts = ADTUtil.computeEffectiveResets(nodeToReplace);
2✔
493
        final int newCosts = ADTUtil.computeEffectiveResets(replacement);
2✔
494

495
        if (newCosts >= oldCosts) {
2✔
496
            return ads;
2✔
497
        }
498

499
        // replace
500
        this.adt.replaceNode(nodeToReplace, replacement);
2✔
501

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

504
        // update
505
        this.resiftAffectedTransitions(ADTUtil.collectLeaves(extension), finalizedADS);
2✔
506

507
        return finalizedADS;
2✔
508
    }
509

510
    /**
511
     * Ask the {@link #subtreeReplacer} for any replacements.
512
     */
513
    private void evaluateSubtreeReplacement() {
514

515
        if (this.hypothesis.size() == 1) {
2✔
516
            // skip replacement if only one node is discovered
517
            return;
2✔
518
        }
519

520
        final Set<ReplacementResult<ADTState<I, O>, I, O>> potentialReplacements =
2✔
521
                this.subtreeReplacer.computeReplacements(this.hypothesis, this.alphabet, this.adt);
2✔
522
        final List<ReplacementResult<ADTState<I, O>, I, O>> validReplacements =
2✔
523
                new ArrayList<>(potentialReplacements.size());
2✔
524
        final Set<ADTNode<ADTState<I, O>, I, O>> cachedLeaves =
2✔
525
                potentialReplacements.isEmpty() ? Collections.emptySet() : ADTUtil.collectLeaves(this.adt.getRoot());
2✔
526

527
        for (ReplacementResult<ADTState<I, O>, I, O> potentialReplacement : potentialReplacements) {
2✔
528
            final ADTNode<ADTState<I, O>, I, O> proposedReplacement = potentialReplacement.getReplacement();
2✔
529
            final ADTNode<ADTState<I, O>, I, O> nodeToReplace = potentialReplacement.getNodeToReplace();
2✔
530

531
            assert this.validateADS(nodeToReplace, proposedReplacement, potentialReplacement.getCutoutNodes());
2✔
532

533
            final ADTNode<ADTState<I, O>, I, O> replacement = this.verifyADS(nodeToReplace,
2✔
534
                                                                             proposedReplacement,
535
                                                                             cachedLeaves,
536
                                                                             potentialReplacement.getCutoutNodes());
2✔
537

538
            // verification may have introduced reset nodes
539
            final int oldCosts = ADTUtil.computeEffectiveResets(nodeToReplace);
2✔
540
            final int newCosts = ADTUtil.computeEffectiveResets(replacement);
2✔
541

542
            if (newCosts >= oldCosts) {
2✔
543
                continue;
2✔
544
            }
545

546
            validReplacements.add(new ReplacementResult<>(nodeToReplace, replacement));
2✔
547
        }
2✔
548

549
        for (ReplacementResult<ADTState<I, O>, I, O> potentialReplacement : validReplacements) {
2✔
550
            final ADTNode<ADTState<I, O>, I, O> replacement = potentialReplacement.getReplacement();
2✔
551
            final ADTNode<ADTState<I, O>, I, O> nodeToReplace = potentialReplacement.getNodeToReplace();
2✔
552

553
            this.adt.replaceNode(nodeToReplace, replacement);
2✔
554

555
            this.resiftAffectedTransitions(ADTUtil.collectLeaves(replacement), ADTUtil.getStartOfADS(replacement));
2✔
556
        }
2✔
557

558
        this.closeTransitions();
2✔
559
    }
2✔
560

561
    /**
562
     * Validate the well-definedness of an ADT replacement, i.e. both ADTs cover the same set of hypothesis states and
563
     * the output behavior described in the replacement matches the hypothesis output.
564
     *
565
     * @param oldADS
566
     *         the old ADT (subtree) to be replaced
567
     * @param newADS
568
     *         the new ADT (subtree)
569
     * @param cutout
570
     *         the set of states not covered by the new ADT
571
     *
572
     * @return {@code true} if the replacement is valid, {@code false} otherwise.
573
     */
574
    private boolean validateADS(ADTNode<ADTState<I, O>, I, O> oldADS,
575
                                ADTNode<ADTState<I, O>, I, O> newADS,
576
                                Set<ADTState<I, O>> cutout) {
577

578
        final Set<ADTNode<ADTState<I, O>, I, O>> oldNodes;
579

580
        if (ADTUtil.isResetNode(oldADS)) {
2✔
581
            oldNodes = ADTUtil.collectResetNodes(this.adt.getRoot());
2✔
582
        } else {
583
            oldNodes = ADTUtil.collectADSNodes(this.adt.getRoot());
2✔
584
        }
585

586
        if (!oldNodes.contains(oldADS)) {
2✔
587
            throw new IllegalArgumentException("Subtree to replace does not exist");
×
588
        }
589

590
        final Set<ADTNode<ADTState<I, O>, I, O>> newFinalNodes = ADTUtil.collectLeaves(newADS);
2✔
591

592
        final Set<ADTState<I, O>> oldFinalStates = ADTUtil.collectHypothesisStates(oldADS);
2✔
593
        final Set<ADTState<I, O>> newFinalStates =
2✔
594
                newFinalNodes.stream().map(ADTNode::getHypothesisState).collect(Collectors.toSet());
2✔
595
        newFinalStates.addAll(cutout);
2✔
596

597
        if (!oldFinalStates.equals(newFinalStates)) {
2✔
598
            throw new IllegalArgumentException("New ADS does not cover all old nodes");
×
599
        }
600

601
        final Word<I> parentInputTrace = ADTUtil.buildTraceForNode(oldADS).getFirst();
2✔
602
        final Map<ADTState<I, O>, Pair<Word<I>, Word<O>>> traces = newFinalNodes.stream()
2✔
603
                                                                                .collect(Collectors.toMap(ADTNode::getHypothesisState,
2✔
604
                                                                                                          ADTUtil::buildTraceForNode));
605

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

608
            final Word<I> accessSequence = entry.getKey().getAccessSequence();
2✔
609
            final Word<I> prefix = accessSequence.concat(parentInputTrace);
2✔
610
            final Word<I> input = entry.getValue().getFirst();
2✔
611
            final Word<O> output = entry.getValue().getSecond();
2✔
612

613
            if (!this.hypothesis.computeSuffixOutput(prefix, input).equals(output)) {
2✔
614
                throw new IllegalArgumentException("Output of new ADS does not match hypothesis");
×
615
            }
616
        }
2✔
617

618
        return true;
2✔
619
    }
620

621
    /**
622
     * Verify the proposed ADT replacement by checking the actual behavior of the system under learning. During the
623
     * verification process, the system under learning may behave differently from what the ADT replacement suggests:
624
     * This means a counterexample is witnessed and added to the queue of counterexamples for later investigation.
625
     * Albeit observing diverging behavior, this method continues to trying to construct a valid ADT using the observed
626
     * output. If for two states, no distinguishing output can be observed, the states a separated by means of {@link
627
     * #resolveAmbiguities(ADTNode, ADTNode, ADTState, Set)}.
628
     *
629
     * @param nodeToReplace
630
     *         the old ADT (subtree) to be replaced
631
     * @param replacement
632
     *         the new ADT (subtree). Must have the form of an ADS, i.e. no reset nodes
633
     * @param cachedLeaves
634
     *         a set containing the leaves of the current tree, so they don't have to be re-fetched for every
635
     *         replacement verification
636
     * @param cutout
637
     *         the set of states not covered by the new ADT
638
     *
639
     * @return A verified ADT that correctly distinguishes the states covered by the original ADT
640
     */
641
    private ADTNode<ADTState<I, O>, I, O> verifyADS(ADTNode<ADTState<I, O>, I, O> nodeToReplace,
642
                                                    ADTNode<ADTState<I, O>, I, O> replacement,
643
                                                    Set<ADTNode<ADTState<I, O>, I, O>> cachedLeaves,
644
                                                    Set<ADTState<I, O>> cutout) {
645

646
        final Map<ADTState<I, O>, Pair<Word<I>, Word<O>>> traces = new LinkedHashMap<>();
2✔
647
        ADTUtil.collectLeaves(replacement)
2✔
648
               .forEach(x -> traces.put(x.getHypothesisState(), ADTUtil.buildTraceForNode(x)));
2✔
649

650
        final Pair<Word<I>, Word<O>> parentTrace = ADTUtil.buildTraceForNode(nodeToReplace);
2✔
651
        final Word<I> parentInput = parentTrace.getFirst();
2✔
652
        final Word<O> parentOutput = parentTrace.getSecond();
2✔
653

654
        ADTNode<ADTState<I, O>, I, O> result = null;
2✔
655

656
        // validate
657
        for (Map.Entry<ADTState<I, O>, Pair<Word<I>, Word<O>>> entry : traces.entrySet()) {
2✔
658
            final ADTState<I, O> state = entry.getKey();
2✔
659
            final Word<I> accessSequence = state.getAccessSequence();
2✔
660

661
            this.oracle.reset();
2✔
662
            accessSequence.forEach(this.oracle::query);
2✔
663
            parentInput.forEach(this.oracle::query);
2✔
664

665
            final Word<I> adsInput = entry.getValue().getFirst();
2✔
666
            final Word<O> adsOutput = entry.getValue().getSecond();
2✔
667

668
            final WordBuilder<I> inputWb = new WordBuilder<>(adsInput.size());
2✔
669
            final WordBuilder<O> outputWb = new WordBuilder<>(adsInput.size());
2✔
670

671
            final Iterator<I> inputIter = adsInput.iterator();
2✔
672
            final Iterator<O> outputIter = adsOutput.iterator();
2✔
673

674
            boolean equal = true;
2✔
675
            while (equal && inputIter.hasNext()) {
2✔
676
                final I in = inputIter.next();
2✔
677
                final O realOut = this.oracle.query(in);
2✔
678
                final O expectedOut = outputIter.next();
2✔
679

680
                inputWb.append(in);
2✔
681
                outputWb.append(realOut);
2✔
682

683
                if (!expectedOut.equals(realOut)) {
2✔
684
                    equal = false;
2✔
685
                }
686
            }
2✔
687

688
            final Word<I> traceInput = inputWb.toWord();
2✔
689
            final Word<O> traceOutput = outputWb.toWord();
2✔
690

691
            if (!equal) {
2✔
692
                this.openCounterExamples.add(new DefaultQuery<>(accessSequence.concat(parentInput, traceInput),
2✔
693
                                                                this.hypothesis.computeOutput(state.getAccessSequence())
2✔
694
                                                                               .concat(parentOutput, traceOutput)));
2✔
695
            }
696

697
            final ADTNode<ADTState<I, O>, I, O> trace = ADTUtil.buildADSFromObservation(traceInput, traceOutput, state);
2✔
698

699
            if (result == null) {
2✔
700
                result = trace;
2✔
701
            } else {
702
                if (!ADTUtil.mergeADS(result, trace)) {
2✔
703
                    this.resolveAmbiguities(nodeToReplace, result, state, cachedLeaves);
2✔
704
                }
705
            }
706
        }
2✔
707

708
        for (ADTState<I, O> s : cutout) {
2✔
709
            this.resolveAmbiguities(nodeToReplace, result, s, cachedLeaves);
2✔
710
        }
2✔
711

712
        return result;
2✔
713
    }
714

715
    /**
716
     * If two states show the same output behavior resolve this ambiguity by adding a reset node and add a new (sub) ADS
717
     * based on the lowest common ancestor in the existing ADT.
718
     *
719
     * @param nodeToReplace
720
     *         the old ADT (subtree) to be replaced
721
     * @param newADS
722
     *         the new ADT (subtree)
723
     * @param state
724
     *         the state which cannot be distinguished using the given replacement
725
     * @param cachedLeaves
726
     *         a set containing the leaves of the current tree, so they don't have to be re-fetched for every
727
     *         replacement verification
728
     */
729
    private void resolveAmbiguities(ADTNode<ADTState<I, O>, I, O> nodeToReplace,
730
                                    ADTNode<ADTState<I, O>, I, O> newADS,
731
                                    ADTState<I, O> state,
732
                                    Set<ADTNode<ADTState<I, O>, I, O>> cachedLeaves) {
733

734
        final Pair<Word<I>, Word<O>> parentTrace = ADTUtil.buildTraceForNode(nodeToReplace);
2✔
735
        final Word<I> parentInput = parentTrace.getFirst();
2✔
736
        final Word<I> effectiveAccessSequence = state.getAccessSequence().concat(parentInput);
2✔
737

738
        this.oracle.reset();
2✔
739
        effectiveAccessSequence.forEach(this.oracle::query);
2✔
740

741
        ADTNode<ADTState<I, O>, I, O> iter = newADS;
2✔
742
        while (!ADTUtil.isLeafNode(iter)) {
2✔
743

744
            if (ADTUtil.isResetNode(iter)) {
2✔
745
                this.oracle.reset();
2✔
746
                state.getAccessSequence().forEach(this.oracle::query);
2✔
747
                iter = iter.getChildren().values().iterator().next();
2✔
748
            } else {
749
                final O output = this.oracle.query(iter.getSymbol());
2✔
750
                final ADTNode<ADTState<I, O>, I, O> succ = iter.getChildren().get(output);
2✔
751

752
                if (succ == null) {
2✔
753
                    final ADTNode<ADTState<I, O>, I, O> newFinal = new ADTLeafNode<>(iter, state);
2✔
754
                    iter.getChildren().put(output, newFinal);
2✔
755
                    return;
2✔
756
                }
757

758
                iter = succ;
2✔
759
            }
2✔
760
        }
761

762
        ADTNode<ADTState<I, O>, I, O> oldReference = null, newReference = null;
2✔
763
        for (ADTNode<ADTState<I, O>, I, O> leaf : cachedLeaves) {
2✔
764
            final ADTState<I, O> hypState = leaf.getHypothesisState();
2✔
765
            assert hypState != null;
2✔
766

767
            if (hypState.equals(iter.getHypothesisState())) {
2✔
768
                oldReference = leaf;
2✔
769
            } else if (hypState.equals(state)) {
2✔
770
                newReference = leaf;
2✔
771
            }
772

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

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

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

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

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

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

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

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

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

817
            final List<ADTTransition<I, O>> transitionsToRefine = state.getHypothesisState()
2✔
818
                                                                       .getIncomingTransitions()
2✔
819
                                                                       .stream()
2✔
820
                                                                       .filter(x -> !x.isSpanningTreeEdge())
2✔
821
                                                                       .collect(Collectors.toList());
2✔
822

823
            for (ADTTransition<I, O> trans : transitionsToRefine) {
2✔
824
                trans.setTarget(null);
2✔
825
                trans.setSiftNode(finalizedADS);
2✔
826
                this.openTransitions.add(trans);
2✔
827
            }
2✔
828
        }
2✔
829
    }
2✔
830

831
    public ADT<ADTState<I, O>, I, O> getADT() {
832
        return adt;
2✔
833
    }
834

835
    public static final class BuilderDefaults {
836

837
        private BuilderDefaults() {
838
            // prevent instantiation
839
        }
840

841
        public static LeafSplitter leafSplitter() {
842
            return LeafSplitters.DEFAULT_SPLITTER;
2✔
843
        }
844

845
        public static ADTExtender adtExtender() {
846
            return ADTExtenders.EXTEND_BEST_EFFORT;
2✔
847
        }
848

849
        public static SubtreeReplacer subtreeReplacer() {
850
            return SubtreeReplacers.LEVELED_BEST_EFFORT;
2✔
851
        }
852

853
        public static boolean useObservationTree() {
854
            return true;
2✔
855
        }
856
    }
857
}
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

© 2025 Coveralls, Inc