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

LearnLib / learnlib / 20198569605

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

Pull #153

github

web-flow
Merge 6a71fc929 into 879958926
Pull Request #153: Implementation for learning MMLTs, new model for collecting statistics

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

99.12
/algorithms/active/lstar/src/main/java/de/learnlib/algorithm/lstar/mmlt/ExtensibleLStarMMLT.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.Collections;
20
import java.util.HashMap;
21
import java.util.List;
22
import java.util.Map;
23
import java.util.Map.Entry;
24

25
import de.learnlib.acex.AcexAnalyzer;
26
import de.learnlib.acex.AcexAnalyzers;
27
import de.learnlib.algorithm.lstar.closing.ClosingStrategies;
28
import de.learnlib.algorithm.lstar.closing.ClosingStrategy;
29
import de.learnlib.algorithm.lstar.mmlt.cex.MMLTCounterexampleHandler;
30
import de.learnlib.algorithm.lstar.mmlt.cex.MMLTOutputInconsistency;
31
import de.learnlib.algorithm.lstar.mmlt.cex.results.CexAnalysisResult;
32
import de.learnlib.algorithm.lstar.mmlt.cex.results.FalseIgnoreResult;
33
import de.learnlib.algorithm.lstar.mmlt.cex.results.MissingDiscriminatorResult;
34
import de.learnlib.algorithm.lstar.mmlt.cex.results.MissingOneShotResult;
35
import de.learnlib.algorithm.lstar.mmlt.cex.results.MissingResetResult;
36
import de.learnlib.datastructure.observationtable.OTLearner;
37
import de.learnlib.datastructure.observationtable.ObservationTable;
38
import de.learnlib.datastructure.observationtable.Row;
39
import de.learnlib.filter.RefutableSymbolFilter;
40
import de.learnlib.filter.symbol.AcceptAllSymbolFilter;
41
import de.learnlib.oracle.TimedQueryOracle;
42
import de.learnlib.query.DefaultQuery;
43
import de.learnlib.query.Query;
44
import de.learnlib.statistic.Statistics;
45
import de.learnlib.statistic.StatisticsKey;
46
import de.learnlib.statistic.StatisticsService;
47
import de.learnlib.time.MMLTModelParams;
48
import de.learnlib.tooling.annotation.builder.GenerateBuilder;
49
import de.learnlib.util.mealy.MealyUtil;
50
import net.automatalib.alphabet.Alphabet;
51
import net.automatalib.alphabet.GrowingAlphabet;
52
import net.automatalib.alphabet.impl.GrowingMapAlphabet;
53
import net.automatalib.automaton.DeterministicAutomaton.FullIntAbstraction;
54
import net.automatalib.automaton.mmlt.MMLT;
55
import net.automatalib.automaton.mmlt.TimerInfo;
56
import net.automatalib.common.util.HashUtil;
57
import net.automatalib.common.util.collection.IterableUtil;
58
import net.automatalib.symbol.time.InputSymbol;
59
import net.automatalib.symbol.time.TimeStepSequence;
60
import net.automatalib.symbol.time.TimedInput;
61
import net.automatalib.symbol.time.TimedOutput;
62
import net.automatalib.word.Word;
63
import org.checkerframework.checker.nullness.qual.Nullable;
64
import org.slf4j.Logger;
65
import org.slf4j.LoggerFactory;
66

67
/**
68
 * An L*-based leaner for inferring {@link MMLT}s.
69
 *
70
 * @param <I>
71
 *         input symbol type (of non-delaying inputs)
72
 * @param <O>
73
 *         output symbol type
74
 */
75
public class ExtensibleLStarMMLT<I, O>
76
        implements OTLearner<MMLT<Integer, I, ?, O>, TimedInput<I>, Word<TimedOutput<O>>> {
77

78
    private static final Logger LOGGER = LoggerFactory.getLogger(ExtensibleLStarMMLT.class);
2✔
79
    private static final String STATISTICS_ID = "L*-MMLT";
80

81
    /**
82
     * The {@link StatisticsKey} this class uses for counting the number of refinements due to missing discriminators.
83
     */
84
    public static final StatisticsKey KEY_REF_DISCR =
2✔
85
            new StatisticsKey("lrn-ref-discr", "Inaccuracies: missing discriminators", STATISTICS_ID);
86

87
    /**
88
     * The {@link StatisticsKey} this class uses for counting the number of refinements due to missing resets.
89
     */
90
    public static final StatisticsKey KEY_REF_RESET =
2✔
91
            new StatisticsKey("lrn-ref-reset", "Inaccuracies: missing resets", STATISTICS_ID);
92

93
    /**
94
     * The {@link StatisticsKey} this class uses for counting the number of refinements due to missing one-shot timers.
95
     */
96
    public static final StatisticsKey KEY_REF_OS =
2✔
97
            new StatisticsKey("lrn-ref-os", "Inaccuracies: missing one-shot timers", STATISTICS_ID);
98

99
    /**
100
     * The {@link StatisticsKey} this class uses for counting the number of refinements due to false ignores by the
101
     * symbol filter.
102
     */
103
    public static final StatisticsKey KEY_REF_FI =
2✔
104
            new StatisticsKey("lrn-ref-fi", "Inaccuracies: false ignores", STATISTICS_ID);
105

106
    /**
107
     * The {@link StatisticsKey} this class uses for counting the number of counterexample analysis runs.
108
     */
109
    public static final StatisticsKey KEY_CEX_NUM =
2✔
110
            new StatisticsKey("lrn-cex-num", "Number of counterexample analyses", STATISTICS_ID);
111

112
    /**
113
     * The {@link StatisticsKey} this class uses for clocking the duration of the counterexample analysis runs.
114
     */
115
    public static final StatisticsKey KEY_CEX_DUR =
2✔
116
            new StatisticsKey("lrn-cex-dur", "Duration of counterexample analyses", STATISTICS_ID);
117

118
    private final StatisticsService statistics;
119
    private final ClosingStrategy<? super TimedInput<I>, ? super Word<TimedOutput<O>>> closingStrategy;
120

121
    private final TimedQueryOracle<I, O> timeOracle;
122
    private final RefutableSymbolFilter<TimedInput<I>, InputSymbol<I>> symbolFilter;
123

124
    private final MMLTHypDataContainer<I, O> hypData;
125

126
    // ============================
127

128
    private final List<Word<TimedInput<I>>> initialSuffixes;
129
    private final MMLTCounterexampleHandler<I, O> cexAnalyzer;
130

131
    /**
132
     * Instantiates a new learner.
133
     * <p>
134
     * This is a convenience constructor for
135
     * {@link #ExtensibleLStarMMLT(Alphabet, MMLTModelParams, TimedQueryOracle, List, ClosingStrategy,
136
     * RefutableSymbolFilter, AcexAnalyzer)} which uses
137
     * <ul>
138
     *     <li>{@link Collections#emptyList()} for {@code initialSuffixes},</li>
139
     *     <li>{@link ClosingStrategies#CLOSE_SHORTEST} for {@code closingStrategy},</li>
140
     *     <li>{@link AcceptAllSymbolFilter} for {@code symbolFilter}, and</li>
141
     *     <li>{@link AcexAnalyzers#BINARY_SEARCH_BWD} for {@code analyzer}.</li>
142
     * </ul>
143
     *
144
     * @param alphabet
145
     *         the alphabet (of non-delaying inputs)
146
     * @param modelParams
147
     *         the model parameters
148
     * @param timeOracle
149
     *         the query oracle for MMLTs
150
     */
151
    public ExtensibleLStarMMLT(Alphabet<I> alphabet,
152
                               MMLTModelParams<O> modelParams,
153
                               TimedQueryOracle<I, O> timeOracle) {
154
        this(alphabet,
2✔
155
             modelParams,
156
             timeOracle,
157
             Collections.emptyList(),
2✔
158
             ClosingStrategies.CLOSE_SHORTEST,
159
             new AcceptAllSymbolFilter<>(),
160
             AcexAnalyzers.BINARY_SEARCH_BWD);
161
    }
2✔
162

163
    /**
164
     * Instantiates a new learner.
165
     *
166
     * @param alphabet
167
     *         the alphabet (of non-delaying inputs)
168
     * @param modelParams
169
     *         the model parameters
170
     * @param timeOracle
171
     *         the query oracle for MMLTs
172
     * @param initialSuffixes
173
     *         the initial set of suffixes (may be empty)
174
     * @param closingStrategy
175
     *         the closing strategy for the observation table.
176
     * @param symbolFilter
177
     *         the symbol filter
178
     * @param analyzer
179
     *         the strategy for decomposing counterexamples.
180
     */
181
    @GenerateBuilder(defaults = BuilderDefaults.class)
182
    public ExtensibleLStarMMLT(Alphabet<I> alphabet,
183
                               MMLTModelParams<O> modelParams,
184
                               TimedQueryOracle<I, O> timeOracle,
185
                               List<Word<TimedInput<I>>> initialSuffixes,
186
                               ClosingStrategy<? super TimedInput<I>, ? super Word<TimedOutput<O>>> closingStrategy,
187
                               RefutableSymbolFilter<TimedInput<I>, InputSymbol<I>> symbolFilter,
188
                               AcexAnalyzer analyzer) {
2✔
189
        this.closingStrategy = closingStrategy;
2✔
190
        this.timeOracle = timeOracle;
2✔
191
        this.initialSuffixes = initialSuffixes;
2✔
192
        this.statistics = Statistics.getService();
2✔
193

194
        // Prepare hyp data:
195

196
        // Internally, the learner also stores TimeStepSequences in its alphabet:
197
        GrowingAlphabet<TimedInput<I>> internalAlphabet = new GrowingMapAlphabet<>();
2✔
198
        alphabet.forEach(s -> internalAlphabet.add(TimedInput.input(s)));
2✔
199

200
        // Init hypothesis data:
201
        this.hypData = new MMLTHypDataContainer<>(internalAlphabet,
2✔
202
                                                  modelParams,
203
                                                  new MMLTObservationTable<>(internalAlphabet,
204
                                                                             modelParams.maxTimerQueryWaitingTime(),
2✔
205
                                                                             symbolFilter,
206
                                                                             modelParams.silentOutput()));
2✔
207

208
        this.cexAnalyzer = new MMLTCounterexampleHandler<>(timeOracle, analyzer, symbolFilter);
2✔
209
        this.symbolFilter = symbolFilter;
2✔
210
    }
2✔
211

212
    /**
213
     * Heuristically chooses a new one-shot timer from the provided timers. Takes the timer with the highest initial
214
     * value that
215
     * <ul>
216
     *     <li>does not exceed {@code maxInitialValue} and</li>
217
     *     <li>has not timer with a lower initial value that times out at the same time.</li>
218
     * </ul>
219
     *
220
     * @param sortedTimers
221
     *         timers, sorted ascendingly by their initial value
222
     * @param maxInitialValue
223
     *         the maximum initial value to consider
224
     * @param <O>
225
     *         output type
226
     *
227
     * @return the index (in {@code sortedTimers}) of the new one-shot candidate
228
     */
229
    public static <O> int selectOneShotTimer(List<? extends TimerInfo<?, O>> sortedTimers, long maxInitialValue) {
230

231
        // Filter relevant timers:
232
        // Start at timer with the highest initial value.
233
        // Ignore all timers whose initial value exceeds the maximum value.
234
        // Also ignore timers whose timeout is the multiple of another timer's initial value.
235
        timers:
236
        for (int i = sortedTimers.size() - 1; i >= 0; i--) {
2✔
237
            TimerInfo<?, O> timer = sortedTimers.get(i);
2✔
238

239
            // could not have expired
240
            if (timer.initial() <= maxInitialValue) {
2✔
241

242
                // Ignore timers whose initial value is a multiple of another one.
243
                // When set to one-shot, these would expire at same time as periodic timer -> non-deterministic behavior!
244
                for (int j = 0; j < i; j++) {
2✔
245
                    TimerInfo<?, O> otherTimer = sortedTimers.get(j);
2✔
246
                    if (timer.initial() % otherTimer.initial() == 0) {
2✔
247
                        continue timers;
2✔
248
                    }
249
                }
250

251
                return i; // not a multiple and within time
2✔
252
            }
253
        }
254

NEW
255
        throw new IllegalStateException("Maximum initial value is too low; must include at least one timer.");
×
256
    }
257

258
    @Override
259
    public MMLT<Integer, I, ?, O> getHypothesisModel() {
260
        return getInternalHypothesisModel();
2✔
261
    }
262

263
    /**
264
     * Like {@link #getHypothesisModel()}, but returns an {@link MMLTHypothesis} object instead. This objects provides
265
     * additional functions that are just intended for the learner but not the teacher.
266
     *
267
     * @return the internal hypothesis
268
     */
269
    private MMLTHypothesis<I, O> getInternalHypothesisModel() {
270
        this.updateOutputs();
2✔
271
        return constructHypothesis(this.hypData);
2✔
272
    }
273

274
    private List<Row<TimedInput<I>>> selectClosingRows(List<List<Row<TimedInput<I>>>> unclosed) {
275
        return closingStrategy.selectClosingRows(unclosed, hypData.getTable(), timeOracle);
2✔
276
    }
277

278
    private void updateOutputs() {
279
        // Query output of newly-added transitions:
280
        MMLTObservationTable<I, O> table = this.hypData.getTable();
2✔
281
        List<OutputQuery<I, O>> queries = new ArrayList<>();
2✔
282

283
        for (Row<TimedInput<I>> row : IterableUtil.concat(table.getShortPrefixRows(), table.getLongPrefixRows())) {
2✔
284
            Word<TimedInput<I>> label = row.getLabel();
2✔
285

286
            if (label.isEmpty()) {
2✔
287
                continue; // initial state
2✔
288
            }
289

290
            if (this.hypData.getTransitionOutputMap().containsKey(label)) {
2✔
291
                continue; // already queried
2✔
292
            }
293

294
            Word<TimedInput<I>> prefix = label.prefix(-1);
2✔
295
            TimedInput<I> inputSym = label.lastSymbol();
2✔
296

297
            TimedOutput<O> output;
298
            if (inputSym instanceof TimeStepSequence<I> ws) {
2✔
299
                // Query timer output from table:
300
                TimerInfo<?, O> timerInfo = this.hypData.getTable().getTimerInfo(prefix, ws.timeSteps());
2✔
301
                assert timerInfo != null;
2✔
302
                O combinedOutput = this.hypData.getModelParams().outputCombiner().combineSymbols(timerInfo.outputs());
2✔
303
                output = new TimedOutput<>(combinedOutput);
2✔
304
                this.hypData.getTransitionOutputMap().put(label, output);
2✔
305
            } else {
2✔
306
                queries.add(new OutputQuery<>(label, prefix));
2✔
307
            }
308
        }
2✔
309

310
        if (!queries.isEmpty()) {
2✔
311
            timeOracle.processQueries(queries);
2✔
312

313
            for (OutputQuery<I, O> q : queries) {
2✔
314
                q.process(this.hypData.getTransitionOutputMap());
2✔
315
            }
2✔
316
        }
317
    }
2✔
318

319
    // ==========================
320

321
    @Override
322
    public void startLearning() {
323
        List<List<Row<TimedInput<I>>>> initialUnclosed =
2✔
324
                this.hypData.getTable().initialize(Collections.emptyList(), this.initialSuffixes, timeOracle);
2✔
325

326
        // Ensure that closed:
327
        this.completeConsistentTable(initialUnclosed);
2✔
328
    }
2✔
329

330
    @Override
331
    public boolean refineHypothesis(DefaultQuery<TimedInput<I>, Word<TimedOutput<O>>> ceQuery) {
332
        if (!refineHypothesisSingle(ceQuery)) {
2✔
333
            return false; // no valid CEX
2✔
334
        }
335
        while (refineHypothesisSingle(ceQuery)) {
2✔
336
            // analyze exhaustively
337
        }
338
        return true;
2✔
339
    }
340

341
    /**
342
     * Transforms the provided counterexample to an inconsistency object: First, checks if still a counterexample. If
343
     * so, cuts the cex after the first output deviation.
344
     *
345
     * @param ceQuery
346
     *         Counterexample
347
     * @param hypothesis
348
     *         Current hypothesis
349
     *
350
     * @return The resulting inconsistency, or null, if the counterexample is not a counterexample.
351
     */
352
    private @Nullable MMLTOutputInconsistency<I, O> toOutputInconsistency(DefaultQuery<TimedInput<I>, Word<TimedOutput<O>>> ceQuery,
353
                                                                          MMLTHypothesis<I, O> hypothesis) {
354
        // 1. Cut example after first deviation:
355
        DefaultQuery<TimedInput<I>, Word<TimedOutput<O>>> shortQuery =
2✔
356
                MealyUtil.shortenCounterExample(hypothesis.getSemantics(), ceQuery);
2✔
357
        if (shortQuery == null) {
2✔
358
            return null;
2✔
359
        }
360

361
        // 2. Calculate shortened hypothesis output:
362
        Word<TimedOutput<O>> shortHypOutput =
2✔
363
                hypothesis.getSemantics().computeSuffixOutput(shortQuery.getPrefix(), shortQuery.getSuffix());
2✔
364

365
        assert !shortHypOutput.equals(shortQuery.getOutput()) : "Deviation lost after shortening.";
2✔
366

367
        return new MMLTOutputInconsistency<>(shortQuery.getPrefix(),
2✔
368
                                             shortQuery.getSuffix(),
2✔
369
                                             shortQuery.getOutput(),
2✔
370
                                             shortHypOutput);
371
    }
372

373
    private boolean refineHypothesisSingle(DefaultQuery<TimedInput<I>, Word<TimedOutput<O>>> ceQuery) {
374
        // 1. Update hypothesis (may have changed since last refinement):
375
        MMLTHypothesis<I, O> hypothesis = this.getInternalHypothesisModel();
2✔
376

377
        // 2. Transform to output inconsistency:
378
        MMLTOutputInconsistency<I, O> outputIncons = this.toOutputInconsistency(ceQuery, hypothesis);
2✔
379
        if (outputIncons == null) {
2✔
380
            return false;
2✔
381
        }
382

383
        LOGGER.debug("Refining with inconsistency {}", outputIncons);
2✔
384

385
        // 3. Identify source of deviation:
386
        statistics.increaseCounter(KEY_CEX_NUM, this);
2✔
387
        statistics.startOrResumeClock(KEY_CEX_DUR, this);
2✔
388
        CexAnalysisResult<I, O> analysisResult = this.cexAnalyzer.analyzeInconsistency(outputIncons, hypothesis);
2✔
389
        statistics.pauseClock(KEY_CEX_DUR, this);
2✔
390

391
        // 4. Refine:
392
        if (analysisResult instanceof MissingDiscriminatorResult<I, O> locSplit) {
2✔
393
            statistics.increaseCounter(KEY_REF_DISCR, this);
2✔
394

395
            // Add new discriminator as suffix:
396
            assert !hypData.getTable().getSuffixes().contains(locSplit.getDiscriminator());
2✔
397
            List<Word<TimedInput<I>>> suffixes = Collections.singletonList(locSplit.getDiscriminator());
2✔
398
            List<List<Row<TimedInput<I>>>> unclosed = hypData.getTable().addSuffixes(suffixes, timeOracle);
2✔
399

400
            // Close transitions:
401
            this.completeConsistentTable(unclosed); // no consistency check for RS
2✔
402
        } else if (analysisResult instanceof MissingResetResult<I, O> noReset) {
2✔
403
            statistics.increaseCounter(KEY_REF_RESET, this);
2✔
404

405
            // Add missing reset:
406
            Word<TimedInput<I>> resetTrans = hypothesis.getPrefix(noReset.getLocation()).append(noReset.getInput());
2✔
407
            this.hypData.getTransitionResetSet().add(resetTrans);
2✔
408
        } else if (analysisResult instanceof MissingOneShotResult<I, O> noAperiodic) {
2✔
409
            statistics.increaseCounter(KEY_REF_OS, this);
2✔
410

411
            // Identify corresponding sp row:
412
            Word<TimedInput<I>> locPrefix = hypothesis.getPrefix(noAperiodic.getLocation());
2✔
413
            Row<TimedInput<I>> spRow = hypData.getTable().getRow(locPrefix);
2✔
414

415
            assert spRow != null && spRow.isShortPrefixRow();
2✔
416

417
            this.handleMissingTimeoutChange(spRow, noAperiodic.getTimeout());
2✔
418
        } else if (analysisResult instanceof FalseIgnoreResult<I, O> falseIgnore) {
2✔
419
            statistics.increaseCounter(KEY_REF_FI, this);
2✔
420

421
            // Identify corresponding sp row:
422
            Word<TimedInput<I>> locPrefix = hypothesis.getPrefix(falseIgnore.getLocation());
2✔
423
            Row<TimedInput<I>> spRow = hypData.getTable().getRow(locPrefix);
2✔
424

425
            assert spRow != null && spRow.isShortPrefixRow();
2✔
426

427
            // Update filter:
428
            this.symbolFilter.accept(locPrefix, falseIgnore.getSymbol());
2✔
429

430
            // Legalize symbol + close table:
431
            List<List<Row<TimedInput<I>>>> unclosed =
2✔
432
                    hypData.getTable().addOutgoingTransition(spRow, falseIgnore.getSymbol(), this.timeOracle);
2✔
433

434
            this.completeConsistentTable(unclosed);
2✔
435
        } else {
2✔
NEW
436
            throw new IllegalStateException("Unknown inconsistency type.");
×
437
        }
438

439
        return true;
2✔
440
    }
441

442
    private void handleMissingTimeoutChange(Row<TimedInput<I>> spRow, TimerInfo<?, O> timeout) {
443
        MMLTObservationTable<I, O> table = hypData.getTable();
2✔
444
        LocationTimerInfo<I, O> locationTimerInfo = table.getLocationTimerInfo(spRow);
2✔
445
        assert locationTimerInfo != null : "Location with missing one-shot timer must have timers.";
2✔
446

447
        // Only timer with highest initial value can be one-shot.
448
        // If location already has a one-shot timer, prefix of its timeout-transition might be core or fringe prefix.
449
        // If it is a fringe prefix, we need to remove it:
450
        TimerInfo<?, O> lastTimer = locationTimerInfo.getLastTimer();
2✔
451
        assert lastTimer != null;
2✔
452
        if (!lastTimer.periodic()) {
2✔
453
            Word<TimedInput<I>> lastTimerTransPrefix = spRow.getLabel().append(TimedInput.step(lastTimer.initial()));
2✔
454
            Row<TimedInput<I>> row = table.getRow(lastTimerTransPrefix);
2✔
455
            assert row != null;
2✔
456
            if (!row.isShortPrefixRow()) {
2✔
457
                // Last timer is one-shot + has fringe prefix:
458
                table.removeLpRow(lastTimerTransPrefix);
2✔
459
            }
460
        }
461

462
        // Prefix for timeout-transition of new one-shot timer:
463
        assert table.getRow(spRow.getLabel().append(TimedInput.step(timeout.initial()))) == null :
2✔
464
                "Timer already appears to be one-shot.";
465

466
        // Remove all timers with greater timeout (are now redundant):
467
        for (TimerInfo<?, O> t : new ArrayList<>(locationTimerInfo.getSortedTimers())) {
2✔
468
            if (t.initial() > timeout.initial()) {
2✔
469
                locationTimerInfo.removeTimer(t.name());
2✔
470
            }
471
        }
2✔
472

473
        // Change from periodic to one-shot:
474
        locationTimerInfo.setOneShotTimer(timeout.name());
2✔
475

476
        // Update fringe prefixes + close table:
477
        List<List<Row<TimedInput<I>>>> unclosed = table.addTimerTransition(spRow, timeout, this.timeOracle);
2✔
478
        this.completeConsistentTable(unclosed);
2✔
479
    }
2✔
480

481
    @Override
482
    public ObservationTable<TimedInput<I>, Word<TimedOutput<O>>> getObservationTable() {
483
        return this.hypData.getTable();
2✔
484
    }
485

486
    /**
487
     * Iteratively checks for unclosedness and inconsistencies in the table, and fixes any occurrences thereof. This
488
     * process is repeated until the observation table is both closed and consistent.
489
     * <p>
490
     * Simplified version for RS learner: assumes that OT is always consistent.
491
     *
492
     * @param unclosed
493
     *         the unclosed rows (equivalence classes) to start with.
494
     */
495
    private void completeConsistentTable(List<List<Row<TimedInput<I>>>> unclosed) {
496
        List<List<Row<TimedInput<I>>>> unclosedIter = unclosed;
2✔
497
        while (!unclosedIter.isEmpty()) {
2✔
498
            List<Row<TimedInput<I>>> closingRows = this.selectClosingRows(unclosedIter);
2✔
499

500
            // Add new states:
501
            unclosedIter = hypData.getTable().toShortPrefixes(closingRows, timeOracle);
2✔
502
        }
2✔
503

504
    }
2✔
505

506
    /**
507
     * Constructs a hypothesis MMLT from an observation table, inferred local resets, and inferred local timers.
508
     */
509
    private static <I, O> MMLTHypothesis<I, O> constructHypothesis(MMLTHypDataContainer<I, O> hypData) {
510

511
        final MMLTObservationTable<I, O> table = hypData.getTable();
2✔
512
        final MMLTModelParams<O> params = hypData.getModelParams();
2✔
513

514
        // 1. Create map that stores link between contentID and short-prefix row:
515
        final Map<Integer, Row<TimedInput<I>>> locationContentIdMap = new HashMap<>(); // contentId -> sp location
2✔
516
        for (Row<TimedInput<I>> spRow : table.getShortPrefixRows()) {
2✔
517
            // Multiple sp rows may have same contentID. Thus, assign each id only one location:
518
            locationContentIdMap.putIfAbsent(spRow.getRowContentId(), spRow);
2✔
519
        }
2✔
520

521
        // 2. Create untimed alphabet:
522
        GrowingMapAlphabet<I> alphabet = new GrowingMapAlphabet<>();
2✔
523
        for (TimedInput<I> symbol : hypData.getAlphabet()) {
2✔
524
            if (symbol instanceof InputSymbol<I> ndi) {
2✔
525
                alphabet.add(ndi.symbol());
2✔
526
            }
527
        }
2✔
528

529
        // 3. Prepare objects for automaton, timers and resets:
530
        int numLocations = table.numberOfShortPrefixRows();
2✔
531
        final Map<Integer, Integer> stateMap =
2✔
532
                new HashMap<>(HashUtil.capacity(numLocations)); // row content id -> state id
2✔
533
        final Map<Integer, Word<TimedInput<I>>> prefixMap =
2✔
534
                new HashMap<>(HashUtil.capacity(numLocations)); // state id -> location prefix
2✔
535
        MMLTHypothesis<I, O> hypothesis = new MMLTHypothesis<>(alphabet,
2✔
536
                                                               numLocations,
537
                                                               params.silentOutput(),
2✔
538
                                                               params.outputCombiner(),
2✔
539
                                                               prefixMap); // we pass the prefix map as reference so that we can fill it later
540

541
        // 4. Create one state per location:
542
        for (Row<TimedInput<I>> row : table.getShortPrefixRows()) {
2✔
543
            int newStateId = hypothesis.addState();
2✔
544
            stateMap.putIfAbsent(row.getRowContentId(), newStateId);
2✔
545
            prefixMap.put(newStateId, row.getLabel());
2✔
546

547
            if (row.getLabel().equals(Word.epsilon())) {
2✔
548
                hypothesis.setInitialState(newStateId);
2✔
549
            }
550
        }
2✔
551
        // Ensure initial location:
552
        assert hypothesis.getInitialState() != null : "Automaton must have an initial location.";
2✔
553

554
        // 5. Create outgoing transitions for non-delaying inputs:
555
        for (Entry<Integer, Integer> e : stateMap.entrySet()) {
2✔
556
            Integer rowContentId = e.getKey();
2✔
557
            Row<TimedInput<I>> spLocation = locationContentIdMap.get(rowContentId);
2✔
558

559
            assert spLocation != null;
2✔
560

561
            for (I symbol : alphabet) {
2✔
562
                int symIdx = hypData.getAlphabet().getSymbolIndex(TimedInput.input(symbol));
2✔
563

564
                TimedOutput<O> transOutput = hypData.getTransitionOutput(spLocation, symIdx);
2✔
565
                O output = params.silentOutput(); // silent by default
2✔
566
                if (transOutput != null) {
2✔
567
                    output = transOutput.symbol();
2✔
568
                }
569

570
                int successorId;
571
                if (spLocation.getSuccessor(symIdx) == null) {
2✔
572
                    successorId = spLocation.getRowContentId(); // not in local alphabet -> self-loop
2✔
573
                } else {
574
                    successorId = spLocation.getSuccessor(symIdx).getRowContentId();
2✔
575
                }
576

577
                // Add transition to automaton:
578
                int sourceLocId = e.getValue();
2✔
579
                int successorLocId = stateMap.getOrDefault(successorId, FullIntAbstraction.INVALID_STATE);
2✔
580
                hypothesis.addTransition(sourceLocId, symbol, successorLocId, output);
2✔
581

582
                // Check for local reset:
583
                Word<TimedInput<I>> targetTransition = spLocation.getLabel().append(TimedInput.input(symbol));
2✔
584
                if (hypData.getTransitionResetSet().contains(targetTransition) && sourceLocId == successorLocId) {
2✔
585
                    hypothesis.addLocalReset(sourceLocId, symbol);
2✔
586
                }
587
            }
2✔
588
        }
2✔
589

590
        // 6. Add timeout transitions:
591
        for (Entry<Integer, Integer> e : stateMap.entrySet()) {
2✔
592
            Integer rowContentId = e.getKey();
2✔
593
            Row<TimedInput<I>> spLocation = locationContentIdMap.get(rowContentId);
2✔
594

595
            assert spLocation != null;
2✔
596

597
            LocationTimerInfo<I, O> timerInfo = table.getLocationTimerInfo(spLocation);
2✔
598

599
            if (timerInfo != null) {
2✔
600
                for (TimerInfo<?, O> timer : timerInfo.getLocalTimers().values()) {
2✔
601
                    if (timer.periodic()) {
2✔
602
                        hypothesis.addPeriodicTimer(e.getValue(), timer.name(), timer.initial(), timer.outputs());
2✔
603
                    } else {
604
                        // One-shot: use successor from table
605
                        TimedInput<I> symbol = new TimeStepSequence<>(timer.initial());
2✔
606

607
                        int symIdx = hypData.getAlphabet().getSymbolIndex(symbol);
2✔
608
                        int successorId = spLocation.getSuccessor(symIdx).getRowContentId();
2✔
609

610
                        hypothesis.addOneShotTimer(e.getValue(),
2✔
611
                                                   timer.name(),
2✔
612
                                                   timer.initial(),
2✔
613
                                                   timer.outputs(),
2✔
614
                                                   stateMap.getOrDefault(successorId,
2✔
615
                                                                         FullIntAbstraction.INVALID_STATE));
2✔
616
                    }
617
                }
2✔
618
            }
619
        }
2✔
620

621
        return hypothesis;
2✔
622
    }
623

624
    private static final class OutputQuery<I, O> extends Query<TimedInput<I>, Word<TimedOutput<O>>> {
2✔
625

626
        private final Word<TimedInput<I>> label;
627
        private final Word<TimedInput<I>> prefix;
628
        private TimedOutput<O> output;
629

630
        private OutputQuery(Word<TimedInput<I>> label, Word<TimedInput<I>> prefix) {
2✔
631
            this.label = label;
2✔
632
            this.prefix = prefix;
2✔
633
        }
2✔
634

635
        @Override
636
        public void answer(Word<TimedOutput<O>> output) {
637
            assert output.size() == 1;
2✔
638
            this.output = output.firstSymbol();
2✔
639
        }
2✔
640

641
        @Override
642
        public Word<TimedInput<I>> getPrefix() {
643
            return prefix;
2✔
644
        }
645

646
        @Override
647
        public Word<TimedInput<I>> getSuffix() {
648
            return Word.fromLetter(label.lastSymbol());
2✔
649
        }
650

651
        /**
652
         * Processes the query result by mapping the given label to the (single) response.
653
         *
654
         * @param outputs
655
         *         the output map to write the mapping to
656
         */
657
        void process(Map<Word<TimedInput<I>>, TimedOutput<O>> outputs) {
658
            outputs.put(label, output);
2✔
659
        }
2✔
660
    }
661

662
    static final class BuilderDefaults {
663

664
        private BuilderDefaults() {
665
            // prevent instantiation
666
        }
667

668
        static <I> List<Word<TimedInput<I>>> initialSuffixes() {
669
            return Collections.emptyList();
2✔
670
        }
671

672
        static <I, O> ClosingStrategy<? super TimedInput<I>, ? super Word<TimedOutput<O>>> closingStrategy() {
673
            return ClosingStrategies.CLOSE_SHORTEST;
2✔
674
        }
675

676
        static <I> RefutableSymbolFilter<TimedInput<I>, InputSymbol<I>> symbolFilter() {
677
            return new AcceptAllSymbolFilter<>();
2✔
678
        }
679

680
        static AcexAnalyzer analyzer() {
681
            return AcexAnalyzers.BINARY_SEARCH_BWD;
2✔
682
        }
683
    }
684

685
}
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