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

LearnLib / learnlib / 6605620268

22 Oct 2023 06:53PM UTC coverage: 93.218% (+0.9%) from 92.296%
6605620268

push

github

mtf90
cleanup passive integration tests

11546 of 12386 relevant lines covered (93.22%)

1.68 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/algorithm/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.algorithm.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.algorithm.adt.adt.ADT;
33
import de.learnlib.algorithm.adt.adt.ADT.LCAInfo;
34
import de.learnlib.algorithm.adt.adt.ADTLeafNode;
35
import de.learnlib.algorithm.adt.adt.ADTNode;
36
import de.learnlib.algorithm.adt.adt.ADTResetNode;
37
import de.learnlib.algorithm.adt.api.ADTExtender;
38
import de.learnlib.algorithm.adt.api.LeafSplitter;
39
import de.learnlib.algorithm.adt.api.PartialTransitionAnalyzer;
40
import de.learnlib.algorithm.adt.api.SubtreeReplacer;
41
import de.learnlib.algorithm.adt.automaton.ADTHypothesis;
42
import de.learnlib.algorithm.adt.automaton.ADTState;
43
import de.learnlib.algorithm.adt.automaton.ADTTransition;
44
import de.learnlib.algorithm.adt.config.ADTExtenders;
45
import de.learnlib.algorithm.adt.config.LeafSplitters;
46
import de.learnlib.algorithm.adt.config.SubtreeReplacers;
47
import de.learnlib.algorithm.adt.model.ExtensionResult;
48
import de.learnlib.algorithm.adt.model.ObservationTree;
49
import de.learnlib.algorithm.adt.model.ReplacementResult;
50
import de.learnlib.algorithm.adt.util.ADTUtil;
51
import de.learnlib.algorithm.adt.util.SQOOTBridge;
52
import de.learnlib.api.Resumable;
53
import de.learnlib.api.algorithm.LearningAlgorithm;
54
import de.learnlib.api.logging.Category;
55
import de.learnlib.api.oracle.SymbolQueryOracle;
56
import de.learnlib.api.query.DefaultQuery;
57
import de.learnlib.counterexample.LocalSuffixFinders;
58
import de.learnlib.util.MQUtil;
59
import net.automatalib.SupportsGrowingAlphabet;
60
import net.automatalib.alphabet.Alphabet;
61
import net.automatalib.alphabet.impl.Alphabets;
62
import net.automatalib.automaton.transducer.MealyMachine;
63
import net.automatalib.common.util.Pair;
64
import net.automatalib.word.Word;
65
import net.automatalib.word.WordBuilder;
66
import org.slf4j.Logger;
67
import org.slf4j.LoggerFactory;
68

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

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

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

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

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

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

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

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

127
    @Override
128
    public void startLearning() {
129

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

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

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

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

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

150
        this.evaluateSubtreeReplacement();
2✔
151

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

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

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

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

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

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

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

175
        return true;
2✔
176
    }
177

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

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

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

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

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

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

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

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

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

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

214
        assert oldTrans != null;
2✔
215

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

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

225
        final ADTNode<ADTState<I, O>, I, O> newNode;
226

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

333
        final ADTState<I, O> targetState;
334

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

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

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

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

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

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

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

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

365
            this.closeTransition(transition);
2✔
366

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

508
        return finalizedADS;
2✔
509
    }
510

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

619
        return true;
2✔
620
    }
621

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

713
        return result;
2✔
714
    }
715

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

836
    public static final class BuilderDefaults {
837

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

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

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

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

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