Skip to content

Commit 5192e3c

Browse files
committed
Llambda: implement the OTLearner interface
1 parent 110ed62 commit 5192e3c

15 files changed

Lines changed: 470 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1212
* Added the L<sup>s</sup> active learning algorithm for Mealy machines (thanks to [Wolffhardt Schwabe](https://github.com/stateMachinist)).
1313
* Added an `EarlyExitEQOracle` which for a given `AdaptiveMembershipOracle` and `TestWordGenerator` stops the evaluation of (potentially long) Mealy-based equivalence tests as soon as a mismatch with the hypothesis is detected, potentially improving the symbol performance of the given equivalence oracle.
1414
* Both lambda learners (`LLambda{DFA,Mealy}` and `TTTLambda{DFA,Mealy}`) now support the `Resumable` interface.
15+
* The `LLambda{DFA,Mealy}` learners now implement the `OTLearner` interface to export their internal knowledge via an `ObservationTable`.
1516

1617
### Changed
1718

algorithms/active/lambda/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ limitations under the License.
4040
<groupId>de.learnlib</groupId>
4141
<artifactId>learnlib-api</artifactId>
4242
</dependency>
43+
<dependency>
44+
<groupId>de.learnlib</groupId>
45+
<artifactId>learnlib-datastructures</artifactId>
46+
</dependency>
4347
<dependency>
4448
<groupId>de.learnlib</groupId>
4549
<artifactId>learnlib-util</artifactId>

algorithms/active/lambda/src/main/java/de/learnlib/algorithm/lambda/lstar/AbstractLLambda.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,18 @@
1919
import java.util.ArrayList;
2020
import java.util.Collection;
2121
import java.util.Deque;
22-
import java.util.HashMap;
2322
import java.util.HashSet;
23+
import java.util.LinkedHashMap;
2424
import java.util.List;
2525
import java.util.Map;
2626
import java.util.Map.Entry;
2727
import java.util.Objects;
2828
import java.util.Set;
2929

30+
import de.learnlib.AccessSequenceTransformer;
3031
import de.learnlib.Resumable;
31-
import de.learnlib.algorithm.LearningAlgorithm;
32+
import de.learnlib.datastructure.observationtable.OTLearner;
33+
import de.learnlib.datastructure.observationtable.ObservationTable;
3234
import de.learnlib.logging.Category;
3335
import de.learnlib.oracle.MembershipOracle;
3436
import de.learnlib.query.DefaultQuery;
@@ -41,7 +43,8 @@
4143
import org.slf4j.Logger;
4244
import org.slf4j.LoggerFactory;
4345

44-
abstract class AbstractLLambda<M extends SuffixOutput<I, D>, I, D> implements LearningAlgorithm<M, I, D>,
46+
abstract class AbstractLLambda<M extends SuffixOutput<I, D>, I, D> implements OTLearner<M, I, D>,
47+
AccessSequenceTransformer<I>,
4548
SupportsGrowingAlphabet<I>,
4649
Resumable<LLambdaState<I, D>>,
4750
FiniteRepresentation {
@@ -66,7 +69,7 @@ abstract class AbstractLLambda<M extends SuffixOutput<I, D>, I, D> implements Le
6669

6770
this.suffixes = new ArrayList<>(initialSuffixes);
6871
this.shortPrefixes = new HashSet<>();
69-
this.rows = new HashMap<>();
72+
this.rows = new LinkedHashMap<>();
7073
}
7174

7275
abstract int maxSearchIndex(int ceLength);
@@ -308,6 +311,11 @@ public void addAlphabetSymbol(I symbol) {
308311
}
309312
}
310313

314+
@Override
315+
public ObservationTable<I, D> getObservationTable() {
316+
return new OTView<>(alphabet, shortPrefixes, rows, suffixes, this);
317+
}
318+
311319
@Override
312320
public LLambdaState<I, D> suspend() {
313321
return new LLambdaState<>(alphabet, shortPrefixes, rows, suffixes);

algorithms/active/lambda/src/main/java/de/learnlib/algorithm/lambda/lstar/LLambdaDFA.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@
2020
import java.util.List;
2121
import java.util.Map;
2222

23-
import de.learnlib.algorithm.LearningAlgorithm.DFALearner;
23+
import de.learnlib.datastructure.observationtable.OTLearner.OTLearnerDFA;
2424
import de.learnlib.oracle.MembershipOracle;
2525
import net.automatalib.alphabet.Alphabet;
2626
import net.automatalib.automaton.fsa.DFA;
2727
import net.automatalib.automaton.fsa.impl.CompactDFA;
2828
import net.automatalib.common.util.mapping.MutableMapping;
2929
import net.automatalib.word.Word;
3030

31-
public class LLambdaDFA<I> extends AbstractLLambda<DFA<?, I>, I, Boolean> implements DFALearner<I> {
31+
public class LLambdaDFA<I> extends AbstractLLambda<DFA<?, I>, I, Boolean> implements OTLearnerDFA<I> {
3232

3333
private CompactDFA<I> hypothesis;
3434
private MutableMapping<Integer, List<Boolean>> hypStateMap;
@@ -100,6 +100,16 @@ public DFA<?, I> getHypothesisModel() {
100100
return hypothesis;
101101
}
102102

103+
@Override
104+
public Word<I> transformAccessSequence(Word<I> word) {
105+
final List<Boolean> row = rowForState(word);
106+
final List<Word<I>> shortPrefixes = super.getShortPrefixes(row);
107+
108+
assert shortPrefixes.size() == 1;
109+
110+
return shortPrefixes.get(0);
111+
}
112+
103113
@Override
104114
public void addAlphabetSymbol(I symbol) {
105115
if (this.hypothesis != null) {

algorithms/active/lambda/src/main/java/de/learnlib/algorithm/lambda/lstar/LLambdaMealy.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import java.util.Map;
2222
import java.util.Objects;
2323

24-
import de.learnlib.algorithm.LearningAlgorithm.MealyLearner;
24+
import de.learnlib.datastructure.observationtable.OTLearner.OTLearnerMealy;
2525
import de.learnlib.oracle.MembershipOracle;
2626
import net.automatalib.alphabet.Alphabet;
2727
import net.automatalib.automaton.transducer.MealyMachine;
@@ -30,7 +30,7 @@
3030
import net.automatalib.word.Word;
3131

3232
public class LLambdaMealy<I, O> extends AbstractLLambda<MealyMachine<?, I, ?, O>, I, Word<O>>
33-
implements MealyLearner<I, O> {
33+
implements OTLearnerMealy<I, O> {
3434

3535
private CompactMealy<I, O> hypothesis;
3636
private MutableMapping<Integer, List<Word<O>>> hypStateMap;
@@ -111,6 +111,16 @@ void automatonFromTable() {
111111
}
112112
}
113113

114+
@Override
115+
public Word<I> transformAccessSequence(Word<I> word) {
116+
final List<Word<O>> row = rowForState(word);
117+
final List<Word<I>> shortPrefixes = super.getShortPrefixes(row);
118+
119+
assert shortPrefixes.size() == 1;
120+
121+
return shortPrefixes.get(0);
122+
}
123+
114124
@Override
115125
public void addAlphabetSymbol(I symbol) {
116126
if (this.hypothesis != null) {
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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.lambda.lstar;
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.List;
23+
import java.util.Map;
24+
import java.util.Map.Entry;
25+
import java.util.Set;
26+
27+
import de.learnlib.AccessSequenceTransformer;
28+
import de.learnlib.datastructure.observationtable.ObservationTable;
29+
import de.learnlib.datastructure.observationtable.Row;
30+
import de.learnlib.datastructure.observationtable.RowImpl;
31+
import net.automatalib.alphabet.Alphabet;
32+
import net.automatalib.common.util.HashUtil;
33+
import net.automatalib.word.Word;
34+
import org.checkerframework.checker.nullness.qual.NonNull;
35+
36+
class OTView<I, D> implements ObservationTable<I, D> {
37+
38+
private final Alphabet<I> alphabet;
39+
private final List<List<D>> rowContents;
40+
private final List<Row<I>> shortPrefixes;
41+
private final List<Row<I>> longPrefixes;
42+
private final List<Word<I>> suffixes;
43+
private final AccessSequenceTransformer<I> asTransformer;
44+
45+
OTView(Alphabet<I> alphabet,
46+
Set<Word<I>> shortPrefixes,
47+
Map<Word<I>, List<D>> rows,
48+
List<Word<I>> suffixes,
49+
AccessSequenceTransformer<I> asTransformer) {
50+
final int numInputs = alphabet.size();
51+
final int numRows = rows.size();
52+
final int numSP = shortPrefixes.size();
53+
final int numLP = numRows - numSP;
54+
55+
List<List<D>> contents = new ArrayList<>(numRows);
56+
List<RowImpl<I>> shortPrefs = new ArrayList<>(numSP);
57+
List<RowImpl<I>> longPrefs = new ArrayList<>(numLP);
58+
59+
final Map<Word<I>, RowImpl<I>> wordRowMap = new HashMap<>(HashUtil.capacity(numRows));
60+
int idx = 0;
61+
for (Entry<Word<I>, List<D>> e : rows.entrySet()) {
62+
final Word<I> label = e.getKey();
63+
final RowImpl<I> r = new RowImpl<>(label, idx++);
64+
65+
contents.add(Collections.unmodifiableList(e.getValue()));
66+
wordRowMap.put(label, r);
67+
68+
if (shortPrefixes.contains(label)) {
69+
shortPrefs.add(r);
70+
r.makeShort(numInputs);
71+
} else {
72+
longPrefs.add(r);
73+
}
74+
}
75+
76+
for (RowImpl<I> sp : shortPrefs) {
77+
final Word<I> label = sp.getLabel();
78+
for (int i = 0; i < numInputs; i++) {
79+
@SuppressWarnings("nullness") // short prefixes + 1-letter extensions = long prefixes
80+
final @NonNull RowImpl<I> row = wordRowMap.get(label.append(alphabet.getSymbol(i)));
81+
sp.setSuccessor(i, row);
82+
}
83+
}
84+
85+
this.alphabet = alphabet;
86+
this.rowContents = Collections.unmodifiableList(contents);
87+
this.shortPrefixes = Collections.unmodifiableList(shortPrefs);
88+
this.longPrefixes = Collections.unmodifiableList(longPrefs);
89+
this.suffixes = Collections.unmodifiableList(suffixes);
90+
this.asTransformer = asTransformer;
91+
}
92+
93+
@Override
94+
public Alphabet<I> getInputAlphabet() {
95+
return alphabet;
96+
}
97+
98+
@Override
99+
public Collection<Row<I>> getShortPrefixRows() {
100+
return shortPrefixes;
101+
}
102+
103+
@Override
104+
public Collection<Row<I>> getLongPrefixRows() {
105+
return longPrefixes;
106+
}
107+
108+
@Override
109+
public Row<I> getRow(int idx) {
110+
final int numSP = shortPrefixes.size();
111+
if (idx < numSP) {
112+
return shortPrefixes.get(idx);
113+
} else {
114+
return longPrefixes.get(idx - numSP);
115+
}
116+
}
117+
118+
@Override
119+
public int numberOfDistinctRows() {
120+
return shortPrefixes.size() + longPrefixes.size();
121+
}
122+
123+
@Override
124+
public List<Word<I>> getSuffixes() {
125+
return this.suffixes;
126+
}
127+
128+
@Override
129+
public List<D> rowContents(Row<I> row) {
130+
return rowContents.get(row.getRowId());
131+
}
132+
133+
@Override
134+
public Word<I> transformAccessSequence(Word<I> word) {
135+
return this.asTransformer.transformAccessSequence(word);
136+
}
137+
}

algorithms/active/lambda/src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
requires de.learnlib.api;
3434
requires de.learnlib.common.util;
35+
requires de.learnlib.datastructure;
3536
requires net.automatalib.api;
3637
requires net.automatalib.common.util;
3738
requires net.automatalib.core;

algorithms/active/lambda/src/test/java/de/learnlib/algorithm/lambda/AbstractCounterexampleQueueTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
*/
3737
public abstract class AbstractCounterexampleQueueTest {
3838

39-
private static final CompactDFA<Character> DFA;
39+
public static final CompactDFA<Character> DFA;
4040

4141
static {
4242
final Alphabet<Character> alphabet = Alphabets.characters('a', 'b');
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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.lambda.lstar.dfa;
17+
18+
import java.io.IOException;
19+
import java.io.InputStream;
20+
import java.io.Reader;
21+
22+
import de.learnlib.algorithm.lambda.AbstractCounterexampleQueueTest;
23+
import de.learnlib.algorithm.lambda.lstar.LLambdaDFA;
24+
import de.learnlib.datastructure.observationtable.Row;
25+
import de.learnlib.datastructure.observationtable.writer.ObservationTableASCIIWriter;
26+
import de.learnlib.oracle.equivalence.DFASimulatorEQOracle;
27+
import de.learnlib.oracle.membership.DFASimulatorOracle;
28+
import de.learnlib.util.Experiment;
29+
import net.automatalib.common.util.IOUtil;
30+
import net.automatalib.word.Word;
31+
import org.testng.Assert;
32+
import org.testng.annotations.Test;
33+
34+
public class LLambdaDFAObservationTableTest {
35+
36+
@Test
37+
public void testOT() throws IOException {
38+
final var sul = AbstractCounterexampleQueueTest.DFA;
39+
final var alphabet = sul.getInputAlphabet();
40+
final var mqo = new DFASimulatorOracle<>(sul);
41+
final var eqo = new DFASimulatorEQOracle<>(sul);
42+
43+
final var learner = new LLambdaDFA<>(alphabet, mqo);
44+
45+
final var exp = new Experiment<>(learner, eqo, alphabet);
46+
47+
exp.run();
48+
49+
final var ot = learner.getObservationTable();
50+
51+
final int spRows = ot.numberOfShortPrefixRows();
52+
final int lpRows = ot.numberOfLongPrefixRows();
53+
final int rows = ot.numberOfDistinctRows();
54+
55+
// test sizes
56+
Assert.assertEquals(rows, exp.getFinalHypothesis().size() * alphabet.size() + 1); // all transitions + ε
57+
Assert.assertEquals(spRows, exp.getFinalHypothesis().size());
58+
Assert.assertEquals(lpRows, rows - spRows);
59+
60+
// test alphabet
61+
Assert.assertEquals(ot.getInputAlphabet(), alphabet);
62+
63+
// test rows
64+
for (int i = 0; i < spRows; i++) {
65+
Row<Character> row = ot.getRow(i);
66+
Word<Character> label = row.getLabel();
67+
Assert.assertTrue(row.isShortPrefixRow());
68+
Assert.assertEquals(ot.transformAccessSequence(label), label);
69+
for (int j = 0; j < alphabet.size(); j++) {
70+
Assert.assertNotNull(row.getSuccessor(j));
71+
}
72+
}
73+
final var sps = ot.getShortPrefixes();
74+
for (int i = spRows; i < rows; i++) {
75+
Row<Character> row = ot.getRow(i);
76+
Word<Character> label = row.getLabel();
77+
Assert.assertFalse(row.isShortPrefixRow());
78+
Assert.assertTrue(sps.contains(ot.transformAccessSequence(label)));
79+
for (int j = 0; j < alphabet.size(); j++) {
80+
Assert.assertNull(row.getSuccessor(j));
81+
}
82+
}
83+
84+
// test rendering
85+
final var sb = new StringBuilder();
86+
new ObservationTableASCIIWriter<>().write(ot, sb);
87+
88+
try (InputStream is = LLambdaDFAObservationTableTest.class.getResourceAsStream("/ot_dfa.txt");
89+
Reader r = IOUtil.asBufferedUTF8Reader(is)) {
90+
91+
final var expected = IOUtil.toString(r);
92+
Assert.assertEquals(sb.toString(), expected);
93+
}
94+
}
95+
}

0 commit comments

Comments
 (0)