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

LearnLib / learnlib / 28964873450

08 Jul 2026 06:06PM UTC coverage: 95.178% (+0.05%) from 95.128%
28964873450

Pull #163

github

web-flow
Merge 98bdbddc6 into 525b3afa5
Pull Request #163: Enforce `LearningAlgorithm` contracts more rigorously

87 of 88 new or added lines in 33 files covered. (98.86%)

14724 of 15470 relevant lines covered (95.18%)

1.74 hits per line

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

97.2
/algorithms/active/dhc/src/main/java/de/learnlib/algorithm/dhc/mealy/MealyDHC.java
1
/* Copyright (C) 2013-2026 TU Dortmund University
2
 * This file is part of LearnLib <https://learnlib.de>.
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
package de.learnlib.algorithm.dhc.mealy;
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.Iterator;
24
import java.util.LinkedHashSet;
25
import java.util.List;
26
import java.util.Map;
27
import java.util.Queue;
28
import java.util.Set;
29

30
import de.learnlib.AccessSequenceTransformer;
31
import de.learnlib.LearnerStateTracker;
32
import de.learnlib.Resumable;
33
import de.learnlib.algorithm.GlobalSuffixLearner.GlobalSuffixLearnerMealy;
34
import de.learnlib.algorithm.LearningAlgorithm.MealyLearner;
35
import de.learnlib.counterexample.GlobalSuffixFinder;
36
import de.learnlib.counterexample.GlobalSuffixFinders;
37
import de.learnlib.oracle.MembershipOracle;
38
import de.learnlib.query.DefaultQuery;
39
import de.learnlib.tooling.annotation.builder.GenerateBuilder;
40
import net.automatalib.alphabet.Alphabet;
41
import net.automatalib.alphabet.SupportsGrowingAlphabet;
42
import net.automatalib.automaton.transducer.impl.CompactMealy;
43
import net.automatalib.common.util.HashUtil;
44
import net.automatalib.common.util.mapping.MapMapping;
45
import net.automatalib.common.util.mapping.MutableMapping;
46
import net.automatalib.word.Word;
47
import org.checkerframework.checker.nullness.qual.NonNull;
48
import org.checkerframework.checker.nullness.qual.Nullable;
49

50
/**
51
 * The DHC learner.
52
 * <p>
53
 * <b>Implementation note:</b> this learner uses the {@link AccessSequenceTransformer} interface to provide access
54
 * to the representatives of the states of the current hypothesis model.
55
 *
56
 * @param <I>
57
 *         input symbol type
58
 * @param <O>
59
 *         output symbol type
60
 */
61
public class MealyDHC<I, O> implements MealyLearner<I, O>,
2✔
62
                                       AccessSequenceTransformer<I>,
63
                                       GlobalSuffixLearnerMealy<I, O>,
64
                                       SupportsGrowingAlphabet<I>,
65
                                       Resumable<MealyDHCState<I, O>>,
66
                                       LearnerStateTracker {
67

68
    private final MembershipOracle<I, Word<O>> oracle;
69
    private final Alphabet<I> alphabet;
70
    private Set<Word<I>> splitters = new LinkedHashSet<>();
2✔
71
    private CompactMealy<I, O> hypothesis;
72
    private MutableMapping<Integer, QueueElement<I, O>> accessSequences;
73
    private final GlobalSuffixFinder<? super I, ? super Word<O>> suffixFinder;
74

75
    /**
76
     * Constructor, provided for backwards compatibility reasons.
77
     *
78
     * @param alphabet
79
     *         the learning alphabet
80
     * @param oracle
81
     *         the learning membership oracle
82
     */
83
    public MealyDHC(Alphabet<I> alphabet, MembershipOracle<I, Word<O>> oracle) {
84
        this(alphabet, oracle, GlobalSuffixFinders.RIVEST_SCHAPIRE, Collections.emptyList());
2✔
85
    }
2✔
86

87
    /**
88
     * Constructor.
89
     *
90
     * @param alphabet
91
     *         the learning alphabet
92
     * @param oracle
93
     *         the learning membership oracle
94
     * @param suffixFinder
95
     *         the {@link GlobalSuffixFinder suffix finder} to use for analyzing counterexamples
96
     * @param initialSplitters
97
     *         the initial set of splitters, {@code null} or an empty collection will result in the set of splitters
98
     *         being initialized as the set of alphabet symbols (interpreted as {@link Word}s)
99
     */
100
    @GenerateBuilder(defaults = BuilderDefaults.class)
101
    public MealyDHC(Alphabet<I> alphabet,
102
                    MembershipOracle<I, Word<O>> oracle,
103
                    GlobalSuffixFinder<? super I, ? super Word<O>> suffixFinder,
104
                    Collection<? extends Word<I>> initialSplitters) {
2✔
105
        this.alphabet = alphabet;
2✔
106
        this.oracle = oracle;
2✔
107
        this.suffixFinder = suffixFinder;
2✔
108
        // ensure that the first k splitters are the k alphabet symbols,
109
        // in correct order (this is required by scheduleSuccessors)
110
        for (I symbol : alphabet) {
2✔
111
            splitters.add(Word.fromLetter(symbol));
2✔
112
        }
2✔
113
        if (initialSplitters != null) {
2✔
114
            splitters.addAll(initialSplitters);
2✔
115
        }
116
    }
2✔
117

118
    @Override
119
    public Collection<Word<I>> getGlobalSuffixes() {
120
        return Collections.unmodifiableCollection(splitters);
×
121
    }
122

123
    @Override
124
    public boolean addGlobalSuffixes(Collection<? extends Word<I>> newGlobalSuffixes) {
NEW
125
        requireLearningProcessStarted();
×
126

127
        return addSuffixesUnchecked(newGlobalSuffixes);
×
128
    }
129

130
    protected boolean addSuffixesUnchecked(Collection<? extends Word<I>> newSuffixes) {
131
        int oldSize = hypothesis.size();
2✔
132

133
        splitters.addAll(newSuffixes);
2✔
134

135
        startLearningInternal();
2✔
136

137
        return hypothesis.size() != oldSize;
2✔
138
    }
139

140
    @Override
141
    public void startLearning() {
142
        requireLearningProcessNotStarted();
2✔
143
        startLearningInternal();
2✔
144
    }
2✔
145

146
    private void startLearningInternal() {
147
        // initialize structure to store state output signatures
148
        Map<List<Word<O>>, Integer> signatures = new HashMap<>();
2✔
149

150
        // set up new hypothesis machine
151
        hypothesis = new CompactMealy<>(alphabet);
2✔
152

153
        // initialize exploration queue
154
        Queue<QueueElement<I, O>> queue = new ArrayDeque<>();
2✔
155

156
        // initialize storage for access sequences
157
        accessSequences = hypothesis.createDynamicStateMapping();
2✔
158

159
        // first element to be explored represents the initial state with no predecessor
160
        queue.add(new QueueElement<>(null, null, null, null));
2✔
161

162
        while (!queue.isEmpty()) {
2✔
163
            // get element to be explored from queue
164
            @SuppressWarnings("nullness") // false positive https://github.com/typetools/checker-framework/issues/399
165
            @NonNull QueueElement<I, O> elem = queue.poll();
2✔
166

167
            // determine access sequence for state
168
            Word<I> access = assembleAccessSequence(elem);
2✔
169

170
            // assemble queries
171
            ArrayList<DefaultQuery<I, Word<O>>> queries = new ArrayList<>(splitters.size());
2✔
172
            for (Word<I> suffix : splitters) {
2✔
173
                queries.add(new DefaultQuery<>(access, suffix));
2✔
174
            }
2✔
175

176
            // retrieve answers
177
            oracle.processQueries(queries);
2✔
178

179
            // assemble output signature
180
            List<Word<O>> sig = new ArrayList<>(splitters.size());
2✔
181
            for (DefaultQuery<I, Word<O>> query : queries) {
2✔
182
                sig.add(query.getOutput());
2✔
183
            }
2✔
184

185
            Integer sibling = signatures.get(sig);
2✔
186

187
            if (sibling != null) {
2✔
188
                // this element does not possess a new output signature
189
                // create a transition from parent state to sibling
190
                hypothesis.addTransition(elem.parentState, elem.transIn, sibling, elem.transOut);
2✔
191
            } else {
192
                // this is actually an observably distinct state! Progress!
193
                // Create state and connect via transition to parent
194
                Integer state = elem.parentElement == null ? hypothesis.addInitialState() : hypothesis.addState();
2✔
195
                if (elem.parentElement != null) {
2✔
196
                    hypothesis.addTransition(elem.parentState, elem.transIn, state, elem.transOut);
2✔
197
                }
198
                signatures.put(sig, state);
2✔
199
                accessSequences.put(state, elem);
2✔
200

201
                scheduleSuccessors(elem, state, queue, sig);
2✔
202
            }
203
        }
2✔
204
    }
2✔
205

206
    private Word<I> assembleAccessSequence(QueueElement<I, O> elem) {
207
        List<I> word = new ArrayList<>(elem.depth);
2✔
208

209
        QueueElement<I, O> pre = elem.parentElement;
2✔
210
        I sym = elem.transIn;
2✔
211
        while (pre != null && sym != null) {
2✔
212
            word.add(sym);
2✔
213
            sym = pre.transIn;
2✔
214
            pre = pre.parentElement;
2✔
215
        }
216

217
        Collections.reverse(word);
2✔
218
        return Word.fromList(word);
2✔
219
    }
220

221
    private void scheduleSuccessors(QueueElement<I, O> elem,
222
                                    Integer state,
223
                                    Queue<QueueElement<I, O>> queue,
224
                                    List<Word<O>> sig) {
225
        for (int i = 0; i < alphabet.size(); ++i) {
2✔
226
            // retrieve I/O for transition
227
            I input = alphabet.getSymbol(i);
2✔
228
            O output = sig.get(i).getSymbol(0);
2✔
229

230
            // create successor element and schedule for exploration
231
            queue.add(new QueueElement<>(state, elem, input, output));
2✔
232
        }
233
    }
2✔
234

235
    @Override
236
    public boolean refineHypothesis(DefaultQuery<I, Word<O>> ceQuery) {
237
        requireLearningProcessStarted();
2✔
238

239
        if (hypothesis.computeSuffixOutput(ceQuery.getPrefix(), ceQuery.getSuffix()).equals(ceQuery.getOutput())) {
2✔
240
            return false;
2✔
241
        }
242

243
        Collection<Word<I>> ceSuffixes = suffixFinder.findSuffixes(ceQuery, this, hypothesis, oracle);
2✔
244

245
        return addSuffixesUnchecked(ceSuffixes);
2✔
246
    }
247

248
    @Override
249
    public CompactMealy<I, O> getHypothesisModel() {
250
        requireLearningProcessStarted();
2✔
251
        return hypothesis;
2✔
252
    }
253

254
    @Override
255
    public boolean hasLearningProcessStarted() {
256
        return hypothesis != null;
2✔
257
    }
258

259
    @Override
260
    public void addAlphabetSymbol(I symbol) {
261

262
        if (!this.alphabet.containsSymbol(symbol)) {
2✔
263
            this.alphabet.asGrowingAlphabetOrThrowException().addSymbol(symbol);
2✔
264
        }
265

266
        if (!this.splitters.contains(Word.fromLetter(symbol))) {
2✔
267
            final Iterator<Word<I>> splitterIterator = this.splitters.iterator();
2✔
268
            final LinkedHashSet<Word<I>> newSplitters =
2✔
269
                    new LinkedHashSet<>(HashUtil.capacity(this.splitters.size() + 1));
2✔
270

271
            // see initial initialization of the splitters
272
            for (int i = 0; i < this.alphabet.size() - 1; i++) {
2✔
273
                newSplitters.add(splitterIterator.next());
2✔
274
            }
275

276
            newSplitters.add(Word.fromLetter(symbol));
2✔
277

278
            while (splitterIterator.hasNext()) {
2✔
279
                newSplitters.add(splitterIterator.next());
1✔
280
            }
281

282
            this.splitters = newSplitters;
2✔
283

284
            if (hasLearningProcessStarted()) {
2✔
285
                this.startLearningInternal();
2✔
286
            }
287
        }
288
    }
2✔
289

290
    @Override
291
    public MealyDHCState<I, O> suspend() {
292
        return new MealyDHCState<>(splitters, hypothesis, accessSequences);
2✔
293
    }
294

295
    @Override
296
    public void resume(MealyDHCState<I, O> state) {
297
        this.splitters = state.getSplitters();
2✔
298
        this.accessSequences = new MapMapping<>(state.getAccessSequences());
2✔
299
        this.hypothesis = state.getHypothesis();
2✔
300
    }
2✔
301

302
    @Override
303
    public Word<I> transformAccessSequence(Word<I> word) {
304
        requireLearningProcessStarted();
2✔
305
        Integer state = hypothesis.getState(word);
2✔
306
        assert state != null;
2✔
307
        return assembleAccessSequence(accessSequences.get(state));
2✔
308
    }
309

310
    static final class BuilderDefaults {
311

312
        private BuilderDefaults() {
313
            // prevent instantiation
314
        }
315

316
        static <I, O> GlobalSuffixFinder<? super I, ? super Word<O>> suffixFinder() {
317
            return GlobalSuffixFinders.RIVEST_SCHAPIRE;
2✔
318
        }
319

320
        static <I> Collection<Word<I>> initialSplitters() {
321
            return Collections.emptyList();
2✔
322
        }
323
    }
324

325
    static final class QueueElement<I, O> {
326

327
        private final @Nullable Integer parentState;
328
        private final @Nullable QueueElement<I, O> parentElement;
329
        private final @Nullable I transIn;
330
        private final @Nullable O transOut;
331
        private final int depth;
332

333
        QueueElement(@Nullable Integer parentState,
334
                     @Nullable QueueElement<I, O> parentElement,
335
                     @Nullable I transIn,
336
                     @Nullable O transOut) {
2✔
337
            this.parentState = parentState;
2✔
338
            this.parentElement = parentElement;
2✔
339
            this.transIn = transIn;
2✔
340
            this.transOut = transOut;
2✔
341
            this.depth = (parentElement != null) ? parentElement.depth + 1 : 0;
2✔
342
        }
2✔
343
    }
344
}
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