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

LearnLib / learnlib / 13166566074

05 Feb 2025 09:05PM UTC coverage: 94.368% (+0.04%) from 94.324%
13166566074

push

github

mtf90
test api conformance

when provided with a non-counterexample, refineHypothesis should return false and not throw an exception.

12 of 12 new or added lines in 4 files covered. (100.0%)

19 existing lines in 4 files now uncovered.

12484 of 13229 relevant lines covered (94.37%)

1.72 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.LocalSuffixFinder;
54
import de.learnlib.counterexample.LocalSuffixFinders;
55
import de.learnlib.logging.Category;
56
import de.learnlib.oracle.AdaptiveMembershipOracle;
57
import de.learnlib.oracle.MembershipOracle.MealyMembershipOracle;
58
import de.learnlib.query.DefaultQuery;
59
import de.learnlib.tooling.annotation.builder.GenerateBuilder;
60
import de.learnlib.util.MQUtil;
61
import net.automatalib.alphabet.Alphabet;
62
import net.automatalib.alphabet.SupportsGrowingAlphabet;
63
import net.automatalib.automaton.transducer.MealyMachine;
64
import net.automatalib.common.util.HashUtil;
65
import net.automatalib.common.util.Pair;
66
import net.automatalib.word.Word;
67
import org.checkerframework.checker.nullness.qual.NonNull;
68
import org.slf4j.Logger;
69
import org.slf4j.LoggerFactory;
70

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

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

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

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

108
    @GenerateBuilder(defaults = BuilderDefaults.class)
109
    public ADTLearner(Alphabet<I> alphabet,
110
                      AdaptiveMembershipOracle<I, O> oracle,
111
                      LeafSplitter leafSplitter,
112
                      ADTExtender adtExtender,
113
                      SubtreeReplacer subtreeReplacer,
114
                      boolean useObservationTree,
115
                      LocalSuffixFinder<? super I, ? super Word<O>> suffixFinder) {
2✔
116

117
        this.alphabet = alphabet;
2✔
118
        this.observationTree = new ObservationTree<>(this.alphabet, oracle, useObservationTree);
2✔
119
        this.oracle = this.observationTree;
2✔
120
        this.mqo = new Adaptive2MembershipWrapper<>(oracle);
2✔
121

122
        this.leafSplitter = leafSplitter;
2✔
123
        this.adtExtender = adtExtender;
2✔
124
        this.subtreeReplacer = subtreeReplacer;
2✔
125
        this.suffixFinder = suffixFinder;
2✔
126

127
        this.hypothesis = new ADTHypothesis<>(this.alphabet);
2✔
128
        this.openTransitions = new ArrayDeque<>();
2✔
129
        this.openCounterExamples = new ArrayDeque<>();
2✔
130
        this.allCounterExamples = new LinkedHashSet<>();
2✔
131
        this.adt = new ADT<>();
2✔
132
    }
2✔
133

134
    @Override
135
    public void startLearning() {
136

137
        final ADTState<I, O> initialState = this.hypothesis.addInitialState();
2✔
138
        initialState.setAccessSequence(Word.epsilon());
2✔
139
        this.observationTree.initialize(initialState);
2✔
140
        this.adt.initialize(initialState);
2✔
141

142
        for (I i : this.alphabet) {
2✔
143
            this.openTransitions.add(this.hypothesis.createOpenTransition(initialState, i, this.adt.getRoot()));
2✔
144
        }
2✔
145

146
        this.closeTransitions();
2✔
147
    }
2✔
148

149
    @Override
150
    public boolean refineHypothesis(DefaultQuery<I, Word<O>> ce) {
151

152
        if (!MQUtil.isCounterexample(ce, this.hypothesis)) {
2✔
153
            return false;
2✔
154
        }
155

156
        this.evaluateSubtreeReplacement();
2✔
157

158
        this.openCounterExamples.add(ce);
2✔
159

160
        while (!this.openCounterExamples.isEmpty()) {
2✔
161

162
            // normal refinement step
163
            while (!this.openCounterExamples.isEmpty()) {
2✔
164

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

170
                while (this.refineHypothesisInternal(currentCE)) {
2✔
171
                    // refine exhaustively
172
                }
173
            }
2✔
174

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

182
            ADTUtil.collectLeaves(this.adt.getRoot()).forEach(this::ensureConsistency);
2✔
183
        }
184

185
        return true;
2✔
186
    }
187

188
    public boolean refineHypothesisInternal(DefaultQuery<I, Word<O>> ceQuery) {
189

190
        if (!MQUtil.isCounterexample(ceQuery, this.hypothesis)) {
2✔
191
            return false;
2✔
192
        }
193

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

197
        if (suffixIdx == -1) {
2✔
UNCOV
198
            throw new IllegalStateException();
×
199
        }
200

201
        final Word<I> ceInput = ceQuery.getInput();
2✔
202

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

208
        final ADTState<I, O> uState = this.hypothesis.getState(u);
2✔
209
        final ADTState<I, O> uaState = this.hypothesis.getState(ua);
2✔
210

211
        assert uState != null && uaState != null;
2✔
212

213
        final Word<I> uAccessSequence = uState.getAccessSequence();
2✔
214
        final Word<I> uaAccessSequence = uaState.getAccessSequence();
2✔
215
        final Word<I> uAccessSequenceWithA = uAccessSequence.append(a);
2✔
216

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

221
        assert oldTrans != null;
2✔
222

223
        oldTrans.setTarget(newState);
2✔
224
        oldTrans.setIsSpanningTreeEdge(true);
2✔
225

226
        final ADTNode<ADTState<I, O>, I, O> nodeToSplit = findNodeForState(uaState);
2✔
227
        final ADTNode<ADTState<I, O>, I, O> newNode;
228

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

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

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

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

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

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

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

261
            newNode = this.adt.extendLeaf(nodeToSplit, completeSplitter, oldOutput, newOutput, this.leafSplitter);
2✔
262
        }
263
        newNode.setState(newState);
2✔
264

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

268
        for (I i : alphabet) {
2✔
269
            newTransitions.add(this.hypothesis.createOpenTransition(newState, i, this.adt.getRoot()));
2✔
270
        }
2✔
271

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

274
        for (ADTTransition<I, O> x : transitionsToRefine) {
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
        for (ADTTransition<I, O> t : transitionsToRefine) {
2✔
282
            if (t.needsSifting()) {
2✔
283
                t.setSiftNode(finalizedSplitter);
2✔
284
                this.openTransitions.add(t);
2✔
285
            }
286
        }
2✔
287

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

294
        this.closeTransitions();
2✔
295
        return true;
2✔
296
    }
297

298
    private ADTNode<ADTState<I, O>, I, O> findNodeForState(ADTState<I, O> state) {
299

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

UNCOV
306
        throw new IllegalStateException("Cannot find leaf for state " + state);
×
307

308
    }
309

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

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

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

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

330
            this.openTransitions.clear();
2✔
331
            this.oracle.processQueries(queries);
2✔
332

333
            for (ADTAdaptiveQuery<I, O> query : queries) {
2✔
334
                processAnsweredQuery(query);
2✔
335
            }
2✔
336
        }
2✔
337
    }
2✔
338

339
    @Override
340
    public void closeTransition(ADTState<I, O> state, I input) {
341

342
        final ADTTransition<I, O> transition = this.hypothesis.getTransition(state, input);
2✔
343
        assert transition != null;
2✔
344

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

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

353
            final int newNumberOfFinalStates = ADTUtil.collectLeaves(ads).size();
2✔
354

355
            if (oldNumberOfFinalStates < newNumberOfFinalStates) {
2✔
356
                throw PartialTransitionAnalyzer.HYPOTHESIS_MODIFICATION_EXCEPTION;
2✔
357
            }
358
        }
359
    }
2✔
360

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

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

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

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

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

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

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

412
    @Override
413
    public void addAlphabetSymbol(I symbol) {
414

415
        if (!this.alphabet.containsSymbol(symbol)) {
2✔
416
            this.alphabet.asGrowingAlphabetOrThrowException().addSymbol(symbol);
2✔
417
        }
418

419
        this.hypothesis.addAlphabetSymbol(symbol);
2✔
420
        this.observationTree.addAlphabetSymbol(symbol);
2✔
421

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

430
            this.closeTransitions();
2✔
431
        }
432
    }
2✔
433

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

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

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

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

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

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

473
        ADTNode<ADTState<I, O>, I, O> iter = leaf;
2✔
474

475
        while (iter != null) {
2✔
476
            final Pair<Word<I>, Word<O>> trace = ADTUtil.buildTraceForNode(iter);
2✔
477

478
            final Word<I> input = trace.getFirst();
2✔
479
            final Word<O> output = trace.getSecond();
2✔
480

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

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

487
            iter = ADTUtil.getStartOfADS(iter).getParent();
2✔
488
        }
2✔
489
    }
2✔
490

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

501
        final ExtensionResult<ADTState<I, O>, I, O> potentialExtension =
2✔
502
                this.adtExtender.computeExtension(this.hypothesis, this, ads);
2✔
503

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

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

514
        assert extension != null && nodeToReplace != null &&
2✔
515
               this.validateADS(nodeToReplace, extension, Collections.emptySet());
2✔
516

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

522
        // verification may have introduced reset nodes
523
        final int oldCosts = ADTUtil.computeEffectiveResets(nodeToReplace);
2✔
524
        final int newCosts = ADTUtil.computeEffectiveResets(replacement);
2✔
525

526
        if (newCosts >= oldCosts) {
2✔
527
            return ads;
2✔
528
        }
529

530
        // replace
531
        this.adt.replaceNode(nodeToReplace, replacement);
2✔
532

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

535
        // update
536
        this.resiftAffectedTransitions(ADTUtil.collectLeaves(extension), finalizedADS);
2✔
537

538
        return finalizedADS;
2✔
539
    }
540

541
    /**
542
     * Ask the {@link #subtreeReplacer} for any replacements.
543
     */
544
    private void evaluateSubtreeReplacement() {
545

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

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

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

562
            assert this.validateADS(nodeToReplace, proposedReplacement, potentialReplacement.getCutoutNodes());
2✔
563

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

569
            // verification may have introduced reset nodes
570
            final int oldCosts = ADTUtil.computeEffectiveResets(nodeToReplace);
2✔
571
            final int newCosts = ADTUtil.computeEffectiveResets(replacement);
2✔
572

573
            if (newCosts >= oldCosts) {
2✔
574
                continue;
2✔
575
            }
576

577
            validReplacements.add(new ReplacementResult<>(nodeToReplace, replacement));
2✔
578
        }
2✔
579

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

584
            this.adt.replaceNode(nodeToReplace, replacement);
2✔
585

586
            this.resiftAffectedTransitions(ADTUtil.collectLeaves(replacement), ADTUtil.getStartOfADS(replacement));
2✔
587
        }
2✔
588

589
        this.closeTransitions();
2✔
590
    }
2✔
591

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

609
        final Set<ADTNode<ADTState<I, O>, I, O>> oldNodes;
610

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

617
        if (!oldNodes.contains(oldADS)) {
2✔
UNCOV
618
            throw new IllegalArgumentException("Subtree to replace does not exist");
×
619
        }
620

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

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

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

633
        if (!oldFinalStates.equals(newFinalStates)) {
2✔
UNCOV
634
            throw new IllegalArgumentException("New ADS does not cover all old nodes");
×
635
        }
636

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

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

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

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

651
        return true;
2✔
652
    }
653

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

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

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

688
        ADTNode<ADTState<I, O>, I, O> result = null;
2✔
689

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

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

701
        this.oracle.processQueries(queries);
2✔
702

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

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

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

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

727
        return result;
2✔
728
    }
729

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

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

753
        this.oracle.processQuery(query);
2✔
754

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

762
        final ADTNode<ADTState<I, O>, I, O> finalNode = query.getCurrentADTNode();
2✔
763
        ADTNode<ADTState<I, O>, I, O> oldReference = null, newReference = null;
2✔
764

765
        for (ADTNode<ADTState<I, O>, I, O> leaf : cachedLeaves) {
2✔
766
            final ADTState<I, O> hypState = leaf.getState();
2✔
767

768
            if (hypState.equals(finalNode.getState())) {
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
        assert oldReference != null && newReference != null;
2✔
780
        final LCAInfo<ADTState<I, O>, I, O> lcaResult = this.adt.findLCA(oldReference, newReference);
2✔
781
        final ADTNode<ADTState<I, O>, I, O> lca = lcaResult.adtNode;
2✔
782
        final Pair<Word<I>, Word<O>> lcaTrace = ADTUtil.buildTraceForNode(lca);
2✔
783

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

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

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

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

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

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

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

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

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

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

837
        return result;
2✔
838
    }
839

840
    public ADT<ADTState<I, O>, I, O> getADT() {
841
        return adt;
2✔
842
    }
843

844
    static final class BuilderDefaults {
845

846
        private BuilderDefaults() {
847
            // prevent instantiation
848
        }
849

850
        public static LeafSplitter leafSplitter() {
851
            return LeafSplitters.DEFAULT_SPLITTER;
2✔
852
        }
853

854
        public static ADTExtender adtExtender() {
855
            return ADTExtenders.EXTEND_BEST_EFFORT;
2✔
856
        }
857

858
        public static SubtreeReplacer subtreeReplacer() {
859
            return SubtreeReplacers.LEVELED_BEST_EFFORT;
2✔
860
        }
861

862
        public static boolean useObservationTree() {
863
            return true;
2✔
864
        }
865

866
        @SuppressWarnings("unchecked")
867
        public static <I, D> LocalSuffixFinder<I, D> suffixFinder() {
868
            return (LocalSuffixFinder<I, D>) LocalSuffixFinders.RIVEST_SCHAPIRE;
2✔
869
        }
870
    }
871
}
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