.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * This module provides the implementation of the Sparse OT learning algorithm as described in the paper "Learning Mealy Machines with Sparse Observation Tables".
+ *
+ * This module is provided by the following Maven dependency:
+ *
+ * <dependency>
+ * <groupId>de.learnlib</groupId>
+ * <artifactId>learnlib-sparse</artifactId>
+ * <version>${version}</version>
+ * </dependency>
+ *
+ */
+open module de.learnlib.algorithm.sparse {
+
+ requires de.learnlib.common.counterexample;
+ requires de.learnlib.common.util;
+ requires de.learnlib.api;
+ requires net.automatalib.core;
+ requires net.automatalib.api;
+ requires net.automatalib.common.util;
+
+ exports de.learnlib.algorithm.sparse;
+}
diff --git a/algorithms/active/sparse/src/test/java/sparse/it/SparseIT.java b/algorithms/active/sparse/src/test/java/sparse/it/SparseIT.java
new file mode 100644
index 000000000..c708097a3
--- /dev/null
+++ b/algorithms/active/sparse/src/test/java/sparse/it/SparseIT.java
@@ -0,0 +1,33 @@
+/* Copyright (C) 2013-2025 TU Dortmund University
+ * This file is part of LearnLib .
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sparse.it;
+
+import de.learnlib.algorithm.sparse.SparseLearner;
+import de.learnlib.oracle.MembershipOracle.MealyMembershipOracle;
+import de.learnlib.testsupport.it.learner.AbstractMealyLearnerIT;
+import de.learnlib.testsupport.it.learner.LearnerVariantList;
+import net.automatalib.alphabet.Alphabet;
+
+public class SparseIT extends AbstractMealyLearnerIT {
+
+ @Override
+ protected void addLearnerVariants(Alphabet alphabet,
+ int targetSize,
+ MealyMembershipOracle mqOracle,
+ LearnerVariantList.MealyLearnerVariantList variants) {
+ variants.addLearnerVariant("sparse", new SparseLearner<>(alphabet, mqOracle));
+ }
+}
From d43221e4dbcabcf9421b1258f1f46485b1ecc0b7 Mon Sep 17 00:00:00 2001
From: stateMachinist <190835587+stateMachinist@users.noreply.github.com>
Date: Mon, 11 Aug 2025 20:30:43 +0200
Subject: [PATCH 02/15] fixed comment
---
.../main/java/de/learnlib/algorithm/sparse/SparseLearner.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/SparseLearner.java b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/SparseLearner.java
index 585f4038e..4f7ff2630 100644
--- a/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/SparseLearner.java
+++ b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/SparseLearner.java
@@ -24,7 +24,7 @@
/**
* optimized implementation of the Ls learning algorithm,
- * as described in the appendix of the paper
+ * as described in section 6 of the paper "Learning Mealy Machines with Sparse Observation Tables"
*/
public class SparseLearner extends AbstractSparseLearner {
From dbeac250a506c9112709b8b0dd9ccf2b068ac31e Mon Sep 17 00:00:00 2001
From: stateMachinist <190835587+stateMachinist@users.noreply.github.com>
Date: Wed, 3 Sep 2025 16:23:09 +0200
Subject: [PATCH 03/15] adjustments for code style
---
algorithms/active/sparse/pom.xml | 16 +++++++
.../de/learnlib/algorithm/sparse/CoreRow.java | 7 +++-
...Learner.java => GenericSparseLearner.java} | 42 ++++++++++++-------
.../de/learnlib/algorithm/sparse/Leaf.java | 4 +-
.../de/learnlib/algorithm/sparse/Node.java | 6 +--
.../de/learnlib/algorithm/sparse/Row.java | 4 +-
.../learnlib/algorithm/sparse/Separator.java | 4 +-
.../algorithm/sparse/SparseLearner.java | 11 ++---
8 files changed, 62 insertions(+), 32 deletions(-)
rename algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/{AbstractSparseLearner.java => GenericSparseLearner.java} (92%)
diff --git a/algorithms/active/sparse/pom.xml b/algorithms/active/sparse/pom.xml
index 6e678f3db..24385d987 100644
--- a/algorithms/active/sparse/pom.xml
+++ b/algorithms/active/sparse/pom.xml
@@ -33,6 +33,22 @@ limitations under the License.
+
+ net.automatalib
+ automata-api
+
+
+ net.automatalib
+ automata-commons-util
+
+
+ net.automatalib
+ automata-core
+
+
+ de.learnlib
+ learnlib-api
+
de.learnlib
learnlib-util
diff --git a/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/CoreRow.java b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/CoreRow.java
index a526bd347..52816609e 100644
--- a/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/CoreRow.java
+++ b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/CoreRow.java
@@ -15,9 +15,12 @@
*/
package de.learnlib.algorithm.sparse;
-import net.automatalib.word.Word;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
-import java.util.*;
+import net.automatalib.word.Word;
class CoreRow extends Row {
final S state; // hypothesis state associated with this row
diff --git a/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/AbstractSparseLearner.java b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/GenericSparseLearner.java
similarity index 92%
rename from algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/AbstractSparseLearner.java
rename to algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/GenericSparseLearner.java
index 74cf4539b..444020d8d 100644
--- a/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/AbstractSparseLearner.java
+++ b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/GenericSparseLearner.java
@@ -15,6 +15,17 @@
*/
package de.learnlib.algorithm.sparse;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
import de.learnlib.algorithm.LearningAlgorithm.MealyLearner;
import de.learnlib.counterexample.LocalSuffixFinders;
import de.learnlib.oracle.MembershipOracle.MealyMembershipOracle;
@@ -26,11 +37,7 @@
import net.automatalib.common.util.Pair;
import net.automatalib.word.Word;
-import java.util.*;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-abstract class AbstractSparseLearner implements MealyLearner {
+class GenericSparseLearner implements MealyLearner {
private final Alphabet alphabet;
private final MealyMembershipOracle oracle;
@@ -49,10 +56,11 @@ abstract class AbstractSparseLearner implements MealyLearner {
private final Map, List> sufToVecs;
private final Map, Map, Integer>> sufToOutToIdx;
- AbstractSparseLearner(Alphabet alphabet,
- MealyMembershipOracle oracle,
- List> initialSuffixes,
- MutableMealyMachine initialHypothesis) {
+ protected GenericSparseLearner(Alphabet alphabet,
+ MealyMembershipOracle oracle,
+ List> initialSuffixes,
+ MutableMealyMachine emptyMachine) {
+ assert emptyMachine.size() == 0;
this.alphabet = alphabet;
this.oracle = oracle;
sufs = new ArrayDeque<>(initialSuffixes);
@@ -61,7 +69,7 @@ abstract class AbstractSparseLearner implements MealyLearner {
prefToFringe = new HashMap<>();
cells = new ArrayList<>();
cellToIdx = new HashMap<>();
- hyp = initialHypothesis;
+ hyp = emptyMachine;
stateToPrefix = new HashMap<>();
accSeq = p -> stateToPrefix.get(hyp.getState(p));
sufToVecs = new HashMap<>();
@@ -209,8 +217,9 @@ private Word query(Row r, Word suf) {
return out.suffix(suf.length());
}
- /** adds suffix-output pair to index if not yet contained
- * and returns a unique identifier representing the pair */
+ /**
+ * adds suffix-output pair to index if not yet contained
+ * and returns a unique identifier representing the pair. */
private int getUniqueCellIdx(Word suf, Word out) {
assert suf.length() == out.length();
final Pair, Word> cell = Pair.of(suf, out);
@@ -223,7 +232,7 @@ private int getUniqueCellIdx(Word suf, Word out) {
return idx;
}
- /** returns index of new core row */
+ /** returns index of new core row. */
private int moveToCore(FringeRow f, List cellIds) {
assert fRows.contains(f);
fRows.remove(f);
@@ -244,8 +253,9 @@ private int moveToCore(FringeRow f, List cellIds) {
return c.idx;
}
- /** takes fringe row and its observations, queries the missing entries
- * and returns a list containing the observations for all suffixes */
+ /**
+ * takes fringe row and its observations, queries the missing entries
+ * and returns a list containing the observations for all suffixes. */
private List completeRowObservations(FringeRow f, List cellIds) {
final List> sufsPresent = cellIds.stream().map(c -> this.cells.get(c).getFirst()).collect(Collectors.toList());
final List> sufsMissing = sufs.stream().filter(s -> !sufsPresent.contains(s)).collect(Collectors.toList());
@@ -316,4 +326,4 @@ private void updatePartitionMap(CoreRow c, Word suf, Word out) {
vecs.get(idx).set(c.idx);
}
-}
\ No newline at end of file
+}
diff --git a/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/Leaf.java b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/Leaf.java
index 322a2709d..10a8e1735 100644
--- a/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/Leaf.java
+++ b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/Leaf.java
@@ -31,7 +31,7 @@ class Leaf extends Node {
// to check whether the separator is still guaranteed to be optimal
// or if it needs to be recomputed.
- /** creates split leaf without observations */
+ /** creates split leaf without observations. */
Leaf() {
super(Collections.emptyList());
cRow = null;
@@ -41,7 +41,7 @@ class Leaf extends Node {
// timestamps will be updated automatically
}
- /** creates unsplit leaf associated with the given core row and observations */
+ /** creates unsplit leaf associated with the given core row and observations. */
Leaf(CoreRow cRow, int numCRows, int numSufs, List cellIds) {
super(cellIds);
this.cRow = cRow;
diff --git a/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/Node.java b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/Node.java
index 455505620..0ad49b557 100644
--- a/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/Node.java
+++ b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/Node.java
@@ -18,16 +18,16 @@
import java.util.BitSet;
import java.util.List;
-abstract class Node { // type parameters required for safe casting
+class Node { // type parameters required for safe casting
final List cellsIds; // cell identifiers of the fringe rows at this node
final BitSet remRows;
- Node(List cellsIds) {
+ protected Node(List cellsIds) {
this(cellsIds, new BitSet());
}
- Node(List cellsIds, BitSet remRows) {
+ protected Node(List cellsIds, BitSet remRows) {
this.cellsIds = cellsIds;
this.remRows = remRows;
}
diff --git a/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/Row.java b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/Row.java
index 1ad9c082d..26ee62442 100644
--- a/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/Row.java
+++ b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/Row.java
@@ -17,10 +17,10 @@
import net.automatalib.word.Word;
-abstract class Row { // type parameters required for safe casting
+class Row { // type parameters required for safe casting
final Word prefix;
- Row(Word prefix) {
+ protected Row(Word prefix) {
this.prefix = prefix;
}
}
diff --git a/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/Separator.java b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/Separator.java
index 1a278d4dd..61a5f2705 100644
--- a/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/Separator.java
+++ b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/Separator.java
@@ -15,13 +15,13 @@
*/
package de.learnlib.algorithm.sparse;
-import net.automatalib.word.Word;
-
import java.util.BitSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import net.automatalib.word.Word;
+
class Separator extends Node {
final Word suffix;
final Map, Node> branchMap;
diff --git a/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/SparseLearner.java b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/SparseLearner.java
index 4f7ff2630..6412190e9 100644
--- a/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/SparseLearner.java
+++ b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/SparseLearner.java
@@ -15,18 +15,19 @@
*/
package de.learnlib.algorithm.sparse;
+import java.util.Collections;
+import java.util.List;
+
import de.learnlib.oracle.MembershipOracle.MealyMembershipOracle;
import net.automatalib.alphabet.Alphabet;
import net.automatalib.automaton.transducer.impl.CompactMealy;
import net.automatalib.word.Word;
-import java.util.*;
-
/**
* optimized implementation of the Ls learning algorithm,
- * as described in section 6 of the paper "Learning Mealy Machines with Sparse Observation Tables"
+ * as described in section 6 of the paper "Learning Mealy Machines with Sparse Observation Tables".
*/
-public class SparseLearner extends AbstractSparseLearner {
+public class SparseLearner extends GenericSparseLearner {
public SparseLearner(Alphabet alphabet, MealyMembershipOracle oracle) {
this(alphabet, oracle, Collections.emptyList());
@@ -35,4 +36,4 @@ public SparseLearner(Alphabet alphabet, MealyMembershipOracle oracle) {
public SparseLearner(Alphabet alphabet, MealyMembershipOracle oracle, List> initialSuffixes) {
super(alphabet, oracle, initialSuffixes, new CompactMealy<>(alphabet));
}
-}
\ No newline at end of file
+}
From 13e7214ba804c5d833052620ec275a950d95c65e Mon Sep 17 00:00:00 2001
From: stateMachinist <190835587+stateMachinist@users.noreply.github.com>
Date: Wed, 3 Sep 2025 21:12:17 +0200
Subject: [PATCH 04/15] extended contributor list
---
pom.xml | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/pom.xml b/pom.xml
index 6245bdfce..4c88da799 100644
--- a/pom.xml
+++ b/pom.xml
@@ -153,6 +153,15 @@ limitations under the License.
Developer
+
+ Wolffhardt Schwabe
+ schwabe@tu-berlin.de
+ TU Berlin, Software and Embedded Systems Engineering
+ https://tu.berlin/sese/
+
+ Developer
+
+
- net.automatalib
- automata-api
+ de.learnlib
+ learnlib-api
- net.automatalib
- automata-commons-util
+ de.learnlib
+ learnlib-counterexamples
- net.automatalib
- automata-core
+ de.learnlib
+ learnlib-util
+
+
- de.learnlib
- learnlib-api
+ net.automatalib
+ automata-api
- de.learnlib
- learnlib-util
+ net.automatalib
+ automata-commons-util
- de.learnlib
- learnlib-counterexamples
+ net.automatalib
+ automata-core
+
+
de.learnlib.testsupport
learnlib-learner-it-support
diff --git a/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/CoreRow.java b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/CoreRow.java
index 52816609e..211ea8ba7 100644
--- a/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/CoreRow.java
+++ b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/CoreRow.java
@@ -23,17 +23,30 @@
import net.automatalib.word.Word;
class CoreRow extends Row {
- final S state; // hypothesis state associated with this row
- final int idx; // index in core row list
- final Map, Word> sufToOut; // maps suffixes to the outputs contained in this row
- final Set cellIds; // also store identifiers of suffix-output pairs for fast compatability checking
+
+ /**
+ * Hypothesis state associated with this row.
+ */
+ final S state;
+ /**
+ * Index in core row list.
+ */
+ final int idx;
+ /**
+ * Maps suffixes to the outputs contained in this row.
+ */
+ final Map, Word> sufToOut;
+ /**
+ * Also store identifiers of suffix-output pairs for fast compatibility checking.
+ */
+ final Set cellIds;
CoreRow(Word prefix, S state, int idx) {
super(prefix);
this.state = state;
this.idx = idx;
sufToOut = new HashMap<>();
- cellIds = new HashSet<>(); // use HashSet to enable fast containment checks
+ cellIds = new HashSet<>();
}
void addSuffix(Word suf, Word out, int cell) {
diff --git a/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/FringeRow.java b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/FringeRow.java
index ad033740f..f823d6005 100644
--- a/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/FringeRow.java
+++ b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/FringeRow.java
@@ -17,16 +17,35 @@
import net.automatalib.word.Word;
+/**
+ * Each fringe row represents a hypothesis transition.
+ *
+ * @param
+ * state type
+ * @param
+ * input symbol type
+ * @param
+ * output symbol type
+ */
class FringeRow extends Row {
- // each fringe row represents a hypothesis transition
- final S srcState; // source state
- final I transIn; // input symbol
- O transOut; // output symbol (determined dynamically)
+ /**
+ * Source state.
+ */
+ final S srcState;
+ /**
+ * Input symbol.
+ */
+ final I transIn;
+ /**
+ * Output symbol (determined dynamically).
+ */
+ O transOut;
+ /**
+ * For compression, fringe rows do not store observations directly. instead, they point to some leaf in a tree
+ * encoding their classification history. this trick avoids redundantly storing identical observations.
+ */
Leaf leaf;
- // for compression, fringe rows do not store observations directly.
- // instead, they point to some leaf in a tree encoding their classification history.
- // this trick avoids redundantly storing identical observations.
FringeRow(Word prefix, S srcState, Leaf leaf) {
super(prefix);
diff --git a/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/GenericSparseLearner.java b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/GenericSparseLearner.java
index 444020d8d..a2eec9838 100644
--- a/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/GenericSparseLearner.java
+++ b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/GenericSparseLearner.java
@@ -21,10 +21,11 @@
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.function.Function;
-import java.util.stream.Collectors;
import de.learnlib.algorithm.LearningAlgorithm.MealyLearner;
import de.learnlib.counterexample.LocalSuffixFinders;
@@ -34,6 +35,7 @@
import net.automatalib.alphabet.Alphabet;
import net.automatalib.automaton.transducer.MealyMachine;
import net.automatalib.automaton.transducer.MutableMealyMachine;
+import net.automatalib.common.util.HashUtil;
import net.automatalib.common.util.Pair;
import net.automatalib.word.Word;
@@ -41,26 +43,56 @@ class GenericSparseLearner implements MealyLearner {
private final Alphabet alphabet;
private final MealyMembershipOracle oracle;
- private final Deque> sufs; // suffixes
- private final List> cRows; // core rows
- private final Deque> fRows; // fringe rows
- private final Map