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

LearnLib / learnlib / 20199022329

13 Dec 2025 10:51PM UTC coverage: 94.914% (+0.4%) from 94.471%
20199022329

push

github

web-flow
Implementation for learning MMLTs, new model for collecting statistics (#153)

* Made RowImpl public to enable access in custom observation table implementations.

* Made several methods of RowImpl public, to enable external access from observation tables that are defined in the learner module.

* Started with integration of MMLT.

* Extended EquivalenceOracle.java with LocalTimerMealyEquivalenceOracle.

* Created module for symbol filters; added tests for counterexample handling for MMLT learner.

* Functions for printing stats as JSON/YAML.

* Renamed statsContainer; exporting mmlt caches.

* Included symbol-filters module in dependency management.

* Renamed fast cache to cache for MMLTs; added integration tests for MMLTs; allow access to stats during testing of learner.

* Added more test models for the MMLT learner.

* Added more test models for the MMLT learner.

* Added cache consistency test for MMLT learning.

* Removed location type parameter from LocalTimerMealyEquivalenceOracle.

* Cache for MMLTs now inherits LearningCache

* Multiple EQ tests for MMLTs can now respect the provided list of inputs for counterexamples.

* Added tests for the cache; cleaned-up some files.

* Added tests for the MMLT cache consistency test.

* Made the MMLT SUL an interface with default methods.

* Made symbol filters more independent from MMLTs

* StatisticsSymbolFilter has stats container as constructor parameter.

* Moved several MMLT examples to test-support.

* Added an example for learning MMLTs; added module info for symbol filter module.

* Added an example for learning MMLTs; added module info for symbol filter module.

* Added more descriptions for included MMLT models

* More info on model params

* Updated reset search oracle to check if it can return a counterexample with the provided inputs.

* adjust to AutomataLib refactorings

* adjust to AutomataLib refactorings

* adjust to AutomataLib refactorings

* Using correct function to render MMLT i... (continued)

1823 of 1873 new or added lines in 77 files covered. (97.33%)

1 existing line in 1 file now uncovered.

14258 of 15022 relevant lines covered (94.91%)

1.73 hits per line

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

96.63
/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/MMLTObservationTable.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.lstar.mmlt;
17

18
import java.util.ArrayList;
19
import java.util.Collection;
20
import java.util.Collections;
21
import java.util.HashMap;
22
import java.util.HashSet;
23
import java.util.Iterator;
24
import java.util.LinkedHashMap;
25
import java.util.List;
26
import java.util.Map;
27
import java.util.Set;
28

29
import de.learnlib.datastructure.observationtable.ObservationTable;
30
import de.learnlib.datastructure.observationtable.Row;
31
import de.learnlib.datastructure.observationtable.RowImpl;
32
import de.learnlib.filter.FilterResponse;
33
import de.learnlib.filter.RefutableSymbolFilter;
34
import de.learnlib.oracle.TimedQueryOracle;
35
import de.learnlib.oracle.TimedQueryOracle.TimerQueryResult;
36
import de.learnlib.query.DefaultQuery;
37
import net.automatalib.alphabet.Alphabet;
38
import net.automatalib.automaton.mmlt.TimerInfo;
39
import net.automatalib.common.util.HashUtil;
40
import net.automatalib.common.util.collection.IterableUtil;
41
import net.automatalib.symbol.time.InputSymbol;
42
import net.automatalib.symbol.time.TimeStepSequence;
43
import net.automatalib.symbol.time.TimedInput;
44
import net.automatalib.symbol.time.TimedOutput;
45
import net.automatalib.word.Word;
46
import org.checkerframework.checker.nullness.qual.Nullable;
47
import org.slf4j.Logger;
48
import org.slf4j.LoggerFactory;
49

50
/**
51
 * The observation table used by the {@link ExtensibleLStarMMLT} learner.
52
 * <p>
53
 * Unlike an OT for standard Mealy learning, includes prefixes for the timeout transitions of one-shot timers. Intended
54
 * to be used with a symbol filter. The filter is queried before adding a new transition for a non-delaying input. If
55
 * the filter considers the transition to be a silent self-loop, the output of the transition is first verified. If it
56
 * is actually silent the learner considers the transition to be a silent self-loop. Consequently, it does not add a
57
 * transition for it. Transitions may be added later if an input was falsely ignored.
58
 * <p>
59
 * Assumes that all short prefixes lead to different locations (-> no need to make canonical)
60
 *
61
 * @param <I>
62
 *         input symbol type (of non-delaying inputs)
63
 * @param <O>
64
 *         output symbol type
65
 */
66
class MMLTObservationTable<I, O> implements ObservationTable<TimedInput<I>, Word<TimedOutput<O>>> {
67

68
    private static final Logger LOGGER = LoggerFactory.getLogger(MMLTObservationTable.class);
2✔
69
    private static final int NO_CONTENT = -1;
70

71
    private final RefutableSymbolFilter<TimedInput<I>, InputSymbol<I>> symbolFilter;
72

73
    private final Map<Word<TimedInput<I>>, LocationTimerInfo<I, O>> timerInfoMap; // prefix -> timer info
74

75
    private final Map<Word<TimedInput<I>>, RowImpl<TimedInput<I>>> shortPrefixRowMap; // label -> row info
76
    private final Map<Word<TimedInput<I>>, RowImpl<TimedInput<I>>> longPrefixRowMap; // label -> row info
77

78
    private final Map<Integer, List<Word<TimedOutput<O>>>> rowContentMap; // contentID -> row content
79

80
    private final List<Word<TimedInput<I>>> suffixes;
81
    private final Set<Word<TimedInput<I>>> suffixSet;
82

83
    private final Alphabet<TimedInput<I>> alphabet;
84
    private final long minTimerQueryWaitTime;
85
    private final TimedOutput<O> silentOutput; // used for symbol filtering
86

87
    MMLTObservationTable(Alphabet<TimedInput<I>> alphabet,
88
                         long minTimerQueryWaitTime,
89
                         RefutableSymbolFilter<TimedInput<I>, InputSymbol<I>> symbolFilter,
90
                         O silentOutput) {
2✔
91
        this.alphabet = alphabet;
2✔
92

93
        this.symbolFilter = symbolFilter;
2✔
94
        this.silentOutput = new TimedOutput<>(silentOutput);
2✔
95
        this.minTimerQueryWaitTime = minTimerQueryWaitTime;
2✔
96

97
        this.timerInfoMap = new HashMap<>();
2✔
98

99
        // use linked hashmaps for stable insertion-order
100
        this.shortPrefixRowMap = new LinkedHashMap<>();
2✔
101
        this.longPrefixRowMap = new LinkedHashMap<>();
2✔
102

103
        this.rowContentMap = new HashMap<>();
2✔
104
        this.suffixes = new ArrayList<>();
2✔
105
        this.suffixSet = new HashSet<>();
2✔
106
    }
2✔
107

108
    /**
109
     * Infers local timers for the provided location.
110
     *
111
     * @param location
112
     *         the source location
113
     */
114
    private void identifyLocalTimers(LocationTimerInfo<I, O> location, TimedQueryOracle<I, O> timeOracle) {
115
        TimerQueryResult<O> timerQueryResponse =
2✔
116
                timeOracle.queryTimers(location.getPrefix(), this.minTimerQueryWaitTime);
2✔
117
        List<TimerInfo<?, O>> timers = timerQueryResponse.timers();
2✔
118

119
        if (timerQueryResponse.aborted()) {
2✔
120
            int end = ExtensibleLStarMMLT.selectOneShotTimer(timers, Long.MAX_VALUE);
2✔
121
            timers.set(end, timers.get(end).asOneShot());
2✔
122
        }
123

124
        // Add timers up to one-shot:
125
        for (TimerInfo<?, O> timer : timerQueryResponse.timers()) {
2✔
126
            location.addTimer(timer);
2✔
127
            this.extendAlphabet(TimedInput.step(timer.initial()));
2✔
128
            if (!timer.periodic()) {
2✔
129
                break;
2✔
130
            }
131
        }
2✔
132
    }
2✔
133

134
    /**
135
     * Extends the global alphabet without adding new transitions.
136
     *
137
     * @param symbol
138
     *         the new alphabet symbol
139
     */
140
    private void extendAlphabet(TimeStepSequence<I> symbol) {
141
        if (!alphabet.containsSymbol(symbol)) {
2✔
142
            alphabet.asGrowingAlphabetOrThrowException().addSymbol(symbol);
2✔
143
        }
144

145
        for (RowImpl<TimedInput<I>> prefix : this.shortPrefixRowMap.values()) {
2✔
146
            prefix.ensureInputCapacity(alphabet.size());
2✔
147
        }
2✔
148
    }
2✔
149

150
    /**
151
     * Adds the initial location.
152
     *
153
     * @return the corresponding row in the observation table
154
     */
155
    private RowImpl<TimedInput<I>> addInitialLocation() {
156
        RowImpl<TimedInput<I>> newRow = new RowImpl<>(Word.epsilon(), 0, alphabet.size());
2✔
157
        newRow.makeShort(alphabet.size());
2✔
158
        this.shortPrefixRowMap.put(Word.epsilon(), newRow);
2✔
159

160
        return newRow;
2✔
161
    }
162

163
    /**
164
     * Adds a new location that belongs to the provided short-prefix row. Infers timers for this location and creates
165
     * outgoing transitions.
166
     *
167
     * @param newRow
168
     *         the newly-added short prefix row
169
     * @param timeOracle
170
     *         the time oracle
171
     */
172
    private void initLocation(RowImpl<TimedInput<I>> newRow, TimedQueryOracle<I, O> timeOracle) {
173
        LocationTimerInfo<I, O> timerInfo = new LocationTimerInfo<>(newRow.getLabel());
2✔
174
        this.identifyLocalTimers(timerInfo, timeOracle);
2✔
175

176
        if (timerInfo.getLastTimer() != null) { // location has timer
2✔
177
            this.timerInfoMap.put(newRow.getLabel(), timerInfo);
2✔
178
        }
179

180
        // Add outgoing transitions:
181
        List<RowImpl<TimedInput<I>>> transitions = this.createOutgoingTransitions(newRow, timeOracle);
2✔
182
        this.queryAllSuffixes(transitions, timeOracle);
2✔
183
    }
2✔
184

185
    /**
186
     * Creates transitions for the provided short-prefix row. Adds transitions for non-delaying inputs and a transition
187
     * for the one-shot timer of the location, if present.
188
     * <p>
189
     * If a symbol filter is provided, the filter is queried before adding a transition for a non-delaying input. If the
190
     * filter considers the input a silent self-loop, no transition is explicitly created for the input.
191
     *
192
     * @param spRow
193
     *         the short prefix row
194
     * @param timeOracle
195
     *         the time query oracle
196
     *
197
     * @return the new transitions
198
     */
199
    private List<RowImpl<TimedInput<I>>> createOutgoingTransitions(RowImpl<TimedInput<I>> spRow,
200
                                                                   TimedQueryOracle<I, O> timeOracle) {
201
        List<RowImpl<TimedInput<I>>> transitions = new ArrayList<>();
2✔
202

203
        Word<TimedInput<I>> sp = spRow.getLabel();
2✔
204

205
        // First, add transitions for non-delaying symbols:
206
        for (int i = 0; i < alphabet.size(); i++) {
2✔
207
            TimedInput<I> sym = alphabet.getSymbol(i);
2✔
208
            if (sym instanceof InputSymbol<I> in) {
2✔
209

210
                Word<TimedInput<I>> lp = sp.append(sym);
2✔
211
                assert !this.shortPrefixRowMap.containsKey(lp);
2✔
212

213
                RowImpl<TimedInput<I>> succRow = this.longPrefixRowMap.get(lp);
2✔
214
                if (succRow == null) {
2✔
215
                    // Query symbol filter before adding transition:
216
                    FilterResponse filterResponse = this.symbolFilter.query(sp, in);
2✔
217
                    if (filterResponse == FilterResponse.IGNORE) {
2✔
218
                        // Verify that output is silent:
219
                        Word<TimedOutput<O>> response = timeOracle.answerQuery(sp, Word.fromLetter(sym));
2✔
220
                        assert response.size() == 1;
2✔
221
                        if (!response.firstSymbol().equals(silentOutput)) {
2✔
222
                            // Not silent -> cannot be silent self-loop:
223
                            filterResponse = FilterResponse.ACCEPT;
2✔
224

225
                            // Update filter:
226
                            this.symbolFilter.accept(sp, in);
2✔
227
                        }
228
                    }
229

230
                    if (filterResponse == FilterResponse.ACCEPT) {
2✔
231
                        // Treat as usual:
232
                        succRow = this.createLpRow(lp);
2✔
233
                    }
234
                }
235

236
                if (succRow != null) {
2✔
237
                    spRow.setSuccessor(i, succRow);
2✔
238
                    transitions.add(succRow);
2✔
239
                }
240
            }
241

242
        }
243

244
        // Second, add one-shot timer transition (if any):
245
        LocationTimerInfo<I, O> locTimers = timerInfoMap.get(spRow.getLabel());
2✔
246
        if (locTimers != null) {
2✔
247
            TimerInfo<?, O> lastTimer = locTimers.getLastTimer();
2✔
248
            if (lastTimer != null && !lastTimer.periodic()) {
2✔
249
                TimedInput<I> waitSym = new TimeStepSequence<>(lastTimer.initial());
2✔
250
                Word<TimedInput<I>> lp = sp.append(waitSym);
2✔
251
                assert !this.shortPrefixRowMap.containsKey(lp);
2✔
252

253
                RowImpl<TimedInput<I>> succRow = this.longPrefixRowMap.get(lp);
2✔
254
                if (succRow == null) {
2✔
255
                    succRow = this.createLpRow(lp);
2✔
256
                }
257
                spRow.setSuccessor(this.alphabet.getSymbolIndex(waitSym), succRow);
2✔
258
                transitions.add(succRow);
2✔
259
            }
260
        }
261

262
        return transitions;
2✔
263
    }
264

265
    private RowImpl<TimedInput<I>> createLpRow(Word<TimedInput<I>> prefix) {
266
        RowImpl<TimedInput<I>> newRow = new RowImpl<>(prefix, 0);
2✔
267
        this.longPrefixRowMap.put(prefix, newRow);
2✔
268

269
        newRow.setLpIndex(0); // unused
2✔
270

271
        return newRow;
2✔
272
    }
273

274
    /**
275
     * Identify transitions that have not been closed, i.e., there is no state with the same suffix behavior. Also
276
     * removes unused content ids.
277
     *
278
     * @return the list of unclosed transition, in a deterministic order
279
     */
280
    List<List<Row<TimedInput<I>>>> findUnclosedTransitions() {
281
        // Identify contentIds for locations:
282
        Set<Integer> spContentIds = new HashSet<>(this.shortPrefixRowMap.size());
2✔
283

284
        for (RowImpl<TimedInput<I>> row : this.shortPrefixRowMap.values()) {
2✔
285
            spContentIds.add(row.getRowContentId());
2✔
286
        }
2✔
287

288
        // Identify ids that are not used by any SP and group them by their content id:
289
        Map<Integer, List<Row<TimedInput<I>>>> lpContentMap =
2✔
290
                new HashMap<>(HashUtil.capacity(this.longPrefixRowMap.size()));
2✔
291

292
        for (RowImpl<TimedInput<I>> row : this.longPrefixRowMap.values()) {
2✔
293
            int id = row.getRowContentId();
2✔
294

295
            if (!spContentIds.contains(id)) {
2✔
296
                lpContentMap.computeIfAbsent(id, k -> new ArrayList<>()).add(row);
2✔
297
            }
298
        }
2✔
299

300
        // Remove unused content ids:
301
        this.rowContentMap.keySet().removeIf(key -> !(spContentIds.contains(key) || lpContentMap.containsKey(key)));
2✔
302

303
        return new ArrayList<>(lpContentMap.values());
2✔
304
    }
305

306
    List<List<Row<TimedInput<I>>>> initialize(List<Word<TimedInput<I>>> initialShortPrefixes,
307
                                              List<Word<TimedInput<I>>> initialSuffixes,
308
                                              TimedQueryOracle<I, O> oracle) {
309

310
        assert this.shortPrefixRowMap.isEmpty() && this.longPrefixRowMap.isEmpty() && initialShortPrefixes.isEmpty();
2✔
311

312
        // Add initial suffixes:
313
        for (Word<TimedInput<I>> suffix : initialSuffixes) {
2✔
314
            if (suffixSet.add(suffix)) {
2✔
315
                suffixes.add(suffix);
2✔
316
            }
317
        }
2✔
318

319
        // 1. Create initial location:
320
        RowImpl<TimedInput<I>> newLoc = this.addInitialLocation();
2✔
321
        this.initLocation(newLoc, oracle);
2✔
322
        this.queryAllSuffixes(Collections.singleton(newLoc), oracle);
2✔
323

324
        // 2. Identify unclosed transitions:
325
        return this.findUnclosedTransitions();
2✔
326
    }
327

328
    private void queryAllSuffixes(Collection<RowImpl<TimedInput<I>>> rows, TimedQueryOracle<I, O> timedOracle) {
329

330
        int numSuffixes = this.suffixes.size();
2✔
331
        List<DefaultQuery<TimedInput<I>, Word<TimedOutput<O>>>> queries = new ArrayList<>(rows.size() * numSuffixes);
2✔
332

333
        for (RowImpl<TimedInput<I>> row : rows) {
2✔
334
            Word<TimedInput<I>> prefix = row.getLabel();
2✔
335

336
            for (Word<TimedInput<I>> suffix : this.suffixes) {
2✔
337
                queries.add(new DefaultQuery<>(prefix, suffix));
2✔
338
            }
2✔
339
        }
2✔
340

341
        timedOracle.processQueries(queries);
2✔
342
        Iterator<DefaultQuery<TimedInput<I>, Word<TimedOutput<O>>>> iter = queries.iterator();
2✔
343

344
        for (RowImpl<TimedInput<I>> row : rows) {
2✔
345
            List<Word<TimedOutput<O>>> outputs = new ArrayList<>(numSuffixes);
2✔
346
            fetchResults(iter, outputs, numSuffixes);
2✔
347

348
            this.processSuffixOutputs(row, outputs);
2✔
349
        }
2✔
350
    }
2✔
351

352
    private void processSuffixOutputs(RowImpl<TimedInput<I>> row, List<Word<TimedOutput<O>>> rowContents) {
353
        if (rowContents.isEmpty()) {
2✔
354
            row.setRowContentId(NO_CONTENT);
2✔
355
            return;
2✔
356
        }
357

358
        int contentId = rowContents.hashCode();
2✔
359
        this.rowContentMap.putIfAbsent(contentId, rowContents);
2✔
360
        row.setRowContentId(contentId);
2✔
361
    }
2✔
362

363
    private static <I, D> void fetchResults(Iterator<DefaultQuery<I, D>> queryIt, List<D> output, int numSuffixes) {
364
        for (int j = 0; j < numSuffixes; j++) {
2✔
365
            DefaultQuery<I, D> qry = queryIt.next();
2✔
366
            output.add(qry.getOutput());
2✔
367
        }
368
    }
2✔
369

370
    List<List<Row<TimedInput<I>>>> addSuffixes(Collection<? extends Word<TimedInput<I>>> newSuffixes,
371
                                               TimedQueryOracle<I, O> oracle) {
372
        // 1. Extend current suffixes + identify new suffixes:
373
        int numOld = this.suffixes.size();
2✔
374
        for (Word<TimedInput<I>> suffix : newSuffixes) {
2✔
375
            if (this.suffixSet.add(suffix)) {
2✔
376
                LOGGER.debug("Adding new suffix '{}'", suffix);
2✔
377
                this.suffixes.add(suffix);
2✔
378
            }
379
        }
2✔
380
        int numNew = this.suffixes.size();
2✔
381

382
        if (numOld == numNew) {
2✔
NEW
383
            return Collections.emptyList();
×
384
        }
385

386
        // 2. Update row content:
387
        int numNewSuffixes = numNew - numOld;
2✔
388
        List<DefaultQuery<TimedInput<I>, Word<TimedOutput<O>>>> queries =
2✔
389
                new ArrayList<>(numNewSuffixes * numberOfRows());
2✔
390
        Iterable<RowImpl<TimedInput<I>>> rows =
2✔
391
                IterableUtil.concat(shortPrefixRowMap.values(), longPrefixRowMap.values());
2✔
392
        List<Word<TimedInput<I>>> newSuffixList = this.suffixes.subList(numOld, numNew);
2✔
393

394
        for (RowImpl<TimedInput<I>> row : rows) {
2✔
395
            for (Word<TimedInput<I>> suffix : newSuffixList) {
2✔
396
                queries.add(new DefaultQuery<>(row.getLabel(), suffix));
2✔
397
            }
2✔
398
        }
2✔
399

400
        oracle.processQueries(queries);
2✔
401
        Iterator<DefaultQuery<TimedInput<I>, Word<TimedOutput<O>>>> iterator = queries.iterator();
2✔
402

403
        for (RowImpl<TimedInput<I>> row : rows) {
2✔
404
            List<Word<TimedOutput<O>>> updatedOutputs = new ArrayList<>(numNew);
2✔
405
            if (row.getRowContentId() != NO_CONTENT) {
2✔
406
                // Add existing suffix outputs:
407
                updatedOutputs.addAll(rowContents(row));
2✔
408
            }
409

410
            fetchResults(iterator, updatedOutputs, numNewSuffixes);
2✔
411
            this.processSuffixOutputs(row, updatedOutputs);
2✔
412
        }
2✔
413

414
        return this.findUnclosedTransitions();
2✔
415
    }
416

417
    List<List<Row<TimedInput<I>>>> toShortPrefixes(List<Row<TimedInput<I>>> lpRows, TimedQueryOracle<I, O> oracle) {
418
        for (Row<TimedInput<I>> row : lpRows) {
2✔
419
            LOGGER.debug("Adding new location with prefix '{}'", row.getLabel());
2✔
420

421
            final RowImpl<TimedInput<I>> lpRow = (RowImpl<TimedInput<I>>) row;
2✔
422

423
            // Delete from LP rows:
424
            this.longPrefixRowMap.remove(row.getLabel());
2✔
425

426
            // Add to SP rows:
427
            this.shortPrefixRowMap.put(row.getLabel(), lpRow);
2✔
428

429
            lpRow.makeShort(alphabet.size());
2✔
430

431
            this.initLocation(lpRow, oracle);
2✔
432
        }
2✔
433
        return this.findUnclosedTransitions();
2✔
434
    }
435

436
    @Override
437
    public Alphabet<TimedInput<I>> getInputAlphabet() {
438
        return this.alphabet;
2✔
439
    }
440

441
    @Override
442
    public Collection<Row<TimedInput<I>>> getShortPrefixRows() {
443
        return Collections.unmodifiableCollection(this.shortPrefixRowMap.values());
2✔
444
    }
445

446
    @Override
447
    public Collection<Row<TimedInput<I>>> getLongPrefixRows() {
448
        return Collections.unmodifiableCollection(this.longPrefixRowMap.values());
2✔
449
    }
450

451
    @Override
452
    public Row<TimedInput<I>> getRow(int idx) {
NEW
453
        throw new UnsupportedOperationException("Not supported. Use prefix to access rows instead.");
×
454
    }
455

456
    @Override
457
    public @Nullable Row<TimedInput<I>> getRow(Word<TimedInput<I>> prefix) {
458
        if (this.shortPrefixRowMap.containsKey(prefix)) {
2✔
459
            return this.shortPrefixRowMap.get(prefix);
2✔
460
        }
461
        if (this.longPrefixRowMap.containsKey(prefix)) {
2✔
462
            return this.longPrefixRowMap.get(prefix);
2✔
463
        }
464
        return null;
2✔
465
    }
466

467
    @Override
468
    public int numberOfDistinctRows() {
469
        return this.shortPrefixRowMap.size();
2✔
470
    }
471

472
    @Override
473
    public List<Word<TimedInput<I>>> getSuffixes() {
474
        return this.suffixes;
2✔
475
    }
476

477
    @Override
478
    public List<Word<TimedOutput<O>>> rowContents(Row<TimedInput<I>> row) {
479
        final List<Word<TimedOutput<O>>> content = this.rowContentMap.get(row.getRowContentId());
2✔
480
        if (content == null) {
2✔
481
            // OT may be empty if only single location with timers:
NEW
482
            assert this.suffixes.isEmpty();
×
NEW
483
            return Collections.emptyList();
×
484
        } else {
485
            return content;
2✔
486
        }
487
    }
488

489
    @Override
490
    public Word<TimedInput<I>> transformAccessSequence(Word<TimedInput<I>> word) {
NEW
491
        throw new UnsupportedOperationException("Not implemented.");
×
492
    }
493

494
    @Nullable TimerInfo<?, O> getTimerInfo(Word<TimedInput<I>> prefix, long initial) {
495
        LocationTimerInfo<I, O> info = this.timerInfoMap.get(prefix);
2✔
496
        if (info != null) {
2✔
497
            return info.getTimerInfo(initial);
2✔
498
        }
NEW
499
        return null;
×
500
    }
501

502
    @Nullable LocationTimerInfo<I, O> getLocationTimerInfo(Row<TimedInput<I>> sp) {
503
        return this.timerInfoMap.get(sp.getLabel());
2✔
504
    }
505

506
    /**
507
     * Adds an outgoing transition for the given symbol to the given location and subsequently tests for unclosed
508
     * transitions.
509
     * <p>
510
     * Raises an error if this transition already exists.
511
     *
512
     * @param spRow
513
     *         the source location
514
     * @param symbol
515
     *         the input symbol
516
     * @param timeOracle
517
     *         the oracle
518
     *
519
     * @return List of unclosed rows. Empty, if none.
520
     */
521
    List<List<Row<TimedInput<I>>>> addOutgoingTransition(Row<TimedInput<I>> spRow,
522
                                                         TimedInput<I> symbol,
523
                                                         TimedQueryOracle<I, O> timeOracle) {
524
        if (!this.alphabet.containsSymbol(symbol)) {
2✔
NEW
525
            throw new IllegalArgumentException("Unknown symbol.");
×
526
        }
527

528
        Word<TimedInput<I>> transitionPrefix = spRow.getLabel().append(symbol);
2✔
529

530
        // Add long-prefix row:
531
        assert this.getRow(transitionPrefix) == null :
2✔
532
                "Location already has an outgoing transition for the provided symbol";
533

534
        RowImpl<TimedInput<I>> succRow = this.createLpRow(transitionPrefix);
2✔
535

536
        // Set as successor:
537
        int symIdx = this.alphabet.getSymbolIndex(symbol);
2✔
538
        ((RowImpl<TimedInput<I>>) spRow).setSuccessor(symIdx, succRow);
2✔
539

540
        // Update suffixes:
541
        this.queryAllSuffixes(Collections.singleton(succRow), timeOracle);
2✔
542

543
        return this.findUnclosedTransitions();
2✔
544
    }
545

546
    List<List<Row<TimedInput<I>>>> addTimerTransition(Row<TimedInput<I>> spRow,
547
                                                      TimerInfo<?, O> timeout,
548
                                                      TimedQueryOracle<I, O> timeOracle) {
549
        return this.addOutgoingTransition(spRow, new TimeStepSequence<>(timeout.initial()), timeOracle);
2✔
550
    }
551

552
    /**
553
     * Removes a long prefix row. Should only be used when removing a transition of a former one-shot timer. When
554
     * turning a long into a short prefix, use toShortPrefix instead,
555
     *
556
     * @param prefix
557
     *         the row prefix
558
     */
559
    void removeLpRow(Word<TimedInput<I>> prefix) {
560
        assert this.longPrefixRowMap.containsKey(prefix) : "Attempting to remove lp row that does not exist.";
2✔
561

562
        // Remove lp row:
563
        this.longPrefixRowMap.remove(prefix);
2✔
564

565
        // Unset as successor:
566
        int symIdx = this.alphabet.getSymbolIndex(prefix.lastSymbol());
2✔
567
        RowImpl<TimedInput<I>> spRow = this.shortPrefixRowMap.get(prefix.prefix(-1));
2✔
568
        assert spRow != null;
2✔
569

570
        spRow.setSuccessor(symIdx, null);
2✔
571
    }
2✔
572
}
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