From 024165af80224828ef57e564244dd25beca1b448 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Mon, 20 Apr 2026 19:36:48 +0200 Subject: [PATCH 1/4] sparse: initial support for growing alphabets --- .../sparse/GenericSparseLearner.java | 56 ++++++++++++++----- .../algorithm/sparse/SparseLearner.java | 8 +-- .../sparse/SparseGrowingAlphabetTest.java | 29 ++++++++++ 3 files changed, 75 insertions(+), 18 deletions(-) create mode 100644 algorithms/active/sparse/src/test/java/de/learnlib/algorithm/sparse/SparseGrowingAlphabetTest.java 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 678183285..c01352b14 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 @@ -28,19 +28,21 @@ import de.learnlib.AccessSequenceTransformer; import de.learnlib.algorithm.LearningAlgorithm.MealyLearner; import de.learnlib.counterexample.LocalSuffixFinders; -import de.learnlib.oracle.MembershipOracle.MealyMembershipOracle; +import de.learnlib.oracle.MembershipOracle; import de.learnlib.query.DefaultQuery; import de.learnlib.util.mealy.MealyUtil; import net.automatalib.alphabet.Alphabet; +import net.automatalib.alphabet.SupportsGrowingAlphabet; import net.automatalib.automaton.transducer.MealyMachine; import net.automatalib.automaton.transducer.MutableMealyMachine; import net.automatalib.common.util.Pair; import net.automatalib.word.Word; -class GenericSparseLearner implements MealyLearner, AccessSequenceTransformer { +class GenericSparseLearner & SupportsGrowingAlphabet, S, I, O> + implements MealyLearner, AccessSequenceTransformer, SupportsGrowingAlphabet { private final Alphabet alphabet; - private final MealyMembershipOracle oracle; + private final MembershipOracle> oracle; /** * Suffixes. @@ -76,7 +78,7 @@ class GenericSparseLearner implements MealyLearner, AccessSequenc /** * Hypothesis. */ - private final MutableMealyMachine hyp; + private final M hyp; /** * Maps each state to its core row prefix. @@ -99,9 +101,9 @@ class GenericSparseLearner implements MealyLearner, AccessSequenc private final Map, Map, Integer>> sufToOutToIdx; protected GenericSparseLearner(Alphabet alphabet, - MealyMembershipOracle oracle, + MembershipOracle> oracle, List> initialSuffixes, - MutableMealyMachine emptyMachine) { + M emptyMachine) { this.alphabet = alphabet; this.oracle = oracle; sufs = new ArrayDeque<>(initialSuffixes); @@ -131,7 +133,7 @@ public void startLearning() { cRows.add(c); sufs.forEach(s -> addSuffixToCoreRow(c, s)); stateToPrefix.put(init, c.prefix); - extendFringe(c, init, new Leaf<>(c, 1, sufs.size(), Collections.emptyList())); + extendFringe(c, new Leaf<>(c, 1, sufs.size(), Collections.emptyList())); fRows.forEach(f -> query(f, Word.epsilon())); // query transition outputs // initially, transition outputs must be queried manually, // for later transitions, they derive from suffix queries @@ -162,6 +164,26 @@ public Word transformAccessSequence(Word word) { return accSeq.apply(word); } + @Override + public void addAlphabetSymbol(I i) { + if (!this.alphabet.containsSymbol(i)) { + this.alphabet.asGrowingAlphabetOrThrowException().addSymbol(i); + } + + this.hyp.addAlphabetSymbol(i); + + final Leaf l = new Leaf<>(); + for (CoreRow c : cRows) { + addFringeRow(c, i, l); + } + + // If the suffixes are empty, we have not started the learning process yet. + // In this case, treat the symbol as if it was part of the initial alphabet. + if (!this.hyp.getStates().isEmpty()) { + updateHypothesis(); + } + } + private void updateHypothesis() { for (FringeRow f : fRows) { classifyFringePrefix(f); @@ -303,7 +325,7 @@ private int moveToCore(FringeRow f, List cellIds) { } cRows.add(c); - extendFringe(c, state, new Leaf<>()); + extendFringe(c, new Leaf<>()); assert c.cellIds.size() == sufs.size(); assert c == cRows.get(c.idx); return c.idx; @@ -321,16 +343,22 @@ private List completeRowObservations(FringeRow f, List c, S state, Leaf leaf) { + /** + * Add missing fringe rows for new transitions. + */ + private void extendFringe(CoreRow c, Leaf l) { for (I i : alphabet) { - // add missing fringe rows for new transitions - final Word prefix = c.prefix.append(i); - final FringeRow fRow = new FringeRow<>(prefix, state, leaf); - prefToFringe.put(prefix, fRow); - fRows.push(fRow); // prioritize new rows during classification + addFringeRow(c, i, l); } } + private void addFringeRow(CoreRow c, I i, Leaf l) { + final Word prefix = c.prefix.append(i); + final FringeRow f = new FringeRow<>(prefix, c.state, l); + prefToFringe.put(prefix, f); + fRows.push(f); // prioritize new rows during classification + } + private void identifyNewState(DefaultQuery> q) { final Word cex = q.getInput(); final int idxSuf = LocalSuffixFinders.findRivestSchapire(q, accSeq::apply, hyp, oracle); 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 3e9a6301d..ba718585f 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 @@ -18,7 +18,7 @@ import java.util.Collections; import java.util.List; -import de.learnlib.oracle.MembershipOracle.MealyMembershipOracle; +import de.learnlib.oracle.MembershipOracle; import net.automatalib.alphabet.Alphabet; import net.automatalib.automaton.transducer.impl.CompactMealy; import net.automatalib.word.Word; @@ -28,13 +28,13 @@ * Learning Mealy Machines with Sparse Observation Tables * by Wolffhardt Schwabe, Paul Kogel, and Sabine Glesner. */ -public class SparseLearner extends GenericSparseLearner { +public class SparseLearner extends GenericSparseLearner, Integer, I, O> { - public SparseLearner(Alphabet alphabet, MealyMembershipOracle oracle) { + public SparseLearner(Alphabet alphabet, MembershipOracle> oracle) { this(alphabet, oracle, Collections.emptyList()); } - public SparseLearner(Alphabet alphabet, MealyMembershipOracle oracle, List> initialSuffixes) { + public SparseLearner(Alphabet alphabet, MembershipOracle> oracle, List> initialSuffixes) { super(alphabet, oracle, initialSuffixes, new CompactMealy<>(alphabet)); } } diff --git a/algorithms/active/sparse/src/test/java/de/learnlib/algorithm/sparse/SparseGrowingAlphabetTest.java b/algorithms/active/sparse/src/test/java/de/learnlib/algorithm/sparse/SparseGrowingAlphabetTest.java new file mode 100644 index 000000000..887ee4188 --- /dev/null +++ b/algorithms/active/sparse/src/test/java/de/learnlib/algorithm/sparse/SparseGrowingAlphabetTest.java @@ -0,0 +1,29 @@ +/* Copyright (C) 2013-2026 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 de.learnlib.algorithm.sparse; + +import de.learnlib.oracle.MembershipOracle.MealyMembershipOracle; +import de.learnlib.testsupport.AbstractGrowingAlphabetMealyTest; +import net.automatalib.alphabet.Alphabet; + +public class SparseGrowingAlphabetTest extends AbstractGrowingAlphabetMealyTest> { + + @Override + protected SparseLearner getLearner(MealyMembershipOracle oracle, + Alphabet alphabet) { + return new SparseLearner<>(alphabet, oracle); + } +} From 6408bb39d307aaf0778b6dd76fe0bda84bfec711 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Mon, 20 Apr 2026 19:37:07 +0200 Subject: [PATCH 2/4] procedural: include sparse in testing --- algorithms/active/procedural/pom.xml | 5 +++++ .../de/learnlib/algorithm/procedural/spmm/it/SPMMIT.java | 2 ++ 2 files changed, 7 insertions(+) diff --git a/algorithms/active/procedural/pom.xml b/algorithms/active/procedural/pom.xml index c04efb36b..7383bd476 100644 --- a/algorithms/active/procedural/pom.xml +++ b/algorithms/active/procedural/pom.xml @@ -98,6 +98,11 @@ limitations under the License. learnlib-observation-pack test + + de.learnlib + learnlib-sparse + test + de.learnlib learnlib-ttt diff --git a/algorithms/active/procedural/src/test/java/de/learnlib/algorithm/procedural/spmm/it/SPMMIT.java b/algorithms/active/procedural/src/test/java/de/learnlib/algorithm/procedural/spmm/it/SPMMIT.java index 8a5e6a19e..70fb34d19 100644 --- a/algorithms/active/procedural/src/test/java/de/learnlib/algorithm/procedural/spmm/it/SPMMIT.java +++ b/algorithms/active/procedural/src/test/java/de/learnlib/algorithm/procedural/spmm/it/SPMMIT.java @@ -34,6 +34,7 @@ import de.learnlib.algorithm.procedural.spmm.manager.DefaultATManager; import de.learnlib.algorithm.procedural.spmm.manager.OptimizingATManager; import de.learnlib.algorithm.rivestschapire.RivestSchapireMealy; +import de.learnlib.algorithm.sparse.SparseLearner; import de.learnlib.algorithm.ttt.mealy.TTTLearnerMealy; import de.learnlib.oracle.MembershipOracle; import de.learnlib.oracle.MembershipOracle.MealyMembershipOracle; @@ -61,6 +62,7 @@ protected void addLearnerVariants(ProceduralInputAlphabet alphabet, builder.addLearnerVariant(TTTLambdaMealy::new); builder.addLearnerVariant(RivestSchapireMealy::new); builder.addLearnerVariant(TTTLearnerMealy::new); + builder.addLearnerVariant(SparseLearner::new); } private static class Builder { From a4ee91c7c02566b776d2ff292a782e5916d404ee Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Mon, 20 Apr 2026 19:37:27 +0200 Subject: [PATCH 3/4] aaar: include sparse in testing + cleanup --- algorithms/active/aaar/pom.xml | 5 ++ .../learnlib/algorithm/aaar/AAARTestUtil.java | 57 ++++++------------- 2 files changed, 23 insertions(+), 39 deletions(-) diff --git a/algorithms/active/aaar/pom.xml b/algorithms/active/aaar/pom.xml index 3da26115b..4d928a844 100644 --- a/algorithms/active/aaar/pom.xml +++ b/algorithms/active/aaar/pom.xml @@ -96,6 +96,11 @@ limitations under the License. learnlib-observation-pack test + + de.learnlib + learnlib-sparse + test + de.learnlib learnlib-ttt diff --git a/algorithms/active/aaar/src/test/java/de/learnlib/algorithm/aaar/AAARTestUtil.java b/algorithms/active/aaar/src/test/java/de/learnlib/algorithm/aaar/AAARTestUtil.java index 7bcf2f149..6c8042135 100644 --- a/algorithms/active/aaar/src/test/java/de/learnlib/algorithm/aaar/AAARTestUtil.java +++ b/algorithms/active/aaar/src/test/java/de/learnlib/algorithm/aaar/AAARTestUtil.java @@ -16,10 +16,8 @@ package de.learnlib.algorithm.aaar; import java.util.Arrays; -import java.util.Collections; import java.util.List; -import de.learnlib.acex.AcexAnalyzers; import de.learnlib.algorithm.LearningAlgorithm.DFALearner; import de.learnlib.algorithm.LearningAlgorithm.MealyLearner; import de.learnlib.algorithm.LearningAlgorithm.MooreLearner; @@ -27,8 +25,6 @@ import de.learnlib.algorithm.kv.mealy.KearnsVaziraniMealy; import de.learnlib.algorithm.lambda.ttt.dfa.TTTLambdaDFA; import de.learnlib.algorithm.lambda.ttt.mealy.TTTLambdaMealy; -import de.learnlib.algorithm.lstar.ce.ObservationTableCEXHandlers; -import de.learnlib.algorithm.lstar.closing.ClosingStrategies; import de.learnlib.algorithm.lstar.dfa.ClassicLStarDFA; import de.learnlib.algorithm.lstar.mealy.ExtensibleLStarMealy; import de.learnlib.algorithm.lstar.moore.ExtensibleLStarMoore; @@ -38,10 +34,10 @@ import de.learnlib.algorithm.rivestschapire.RivestSchapireDFA; import de.learnlib.algorithm.rivestschapire.RivestSchapireMealy; import de.learnlib.algorithm.rivestschapire.RivestSchapireMoore; +import de.learnlib.algorithm.sparse.SparseLearner; import de.learnlib.algorithm.ttt.dfa.TTTLearnerDFA; import de.learnlib.algorithm.ttt.mealy.TTTLearnerMealy; import de.learnlib.algorithm.ttt.moore.TTTLearnerMoore; -import de.learnlib.counterexample.LocalSuffixFinders; import net.automatalib.common.util.Pair; import net.automatalib.word.Word; @@ -55,63 +51,46 @@ public static List, I, final ComboConstructor, I, Boolean> lstar = ClassicLStarDFA::new; final ComboConstructor, I, Boolean> rs = RivestSchapireDFA::new; - final ComboConstructor, I, Boolean> kv = - (alph, mqo) -> new KearnsVaziraniDFA<>(alph, mqo, true, AcexAnalyzers.BINARY_SEARCH_FWD); - final ComboConstructor, I, Boolean> dt = - (alph, mqo) -> new OPLearnerDFA<>(alph, mqo, LocalSuffixFinders.RIVEST_SCHAPIRE, true, true); - final ComboConstructor, I, Boolean> ttt = - (alph, mqo) -> new TTTLearnerDFA<>(alph, mqo, AcexAnalyzers.BINARY_SEARCH_FWD); - final ComboConstructor, I, Boolean> lambda = (alph, mqo) -> new TTTLambdaDFA<>(alph, mqo, mqo); + final ComboConstructor, I, Boolean> kv = KearnsVaziraniDFA::new; + final ComboConstructor, I, Boolean> op = OPLearnerDFA::new; + final ComboConstructor, I, Boolean> ttt = TTTLearnerDFA::new; + final ComboConstructor, I, Boolean> lambda = TTTLambdaDFA::new; return Arrays.asList(Pair.of("L*", lstar), Pair.of("RS", rs), Pair.of("KV", kv), - Pair.of("DT", dt), + Pair.of("OP", op), Pair.of("TTT", ttt), Pair.of("TTTLambda", lambda)); } public static List, I, Word>>> getMealyLearners() { - final ComboConstructor, I, Word> lstar = - (alph, mqo) -> new ExtensibleLStarMealy<>(alph, - mqo, - Collections.emptyList(), - ObservationTableCEXHandlers.CLASSIC_LSTAR, - ClosingStrategies.CLOSE_FIRST); + final ComboConstructor, I, Word> lstar = ExtensibleLStarMealy::new; final ComboConstructor, I, Word> rs = RivestSchapireMealy::new; - final ComboConstructor, I, Word> kv = - (alph, mqo) -> new KearnsVaziraniMealy<>(alph, mqo, true, AcexAnalyzers.BINARY_SEARCH_FWD); - final ComboConstructor, I, Word> dt = - (alph, mqo) -> new OPLearnerMealy<>(alph, mqo, LocalSuffixFinders.RIVEST_SCHAPIRE, true); - final ComboConstructor, I, Word> ttt = - (alph, mqo) -> new TTTLearnerMealy<>(alph, mqo, AcexAnalyzers.BINARY_SEARCH_FWD); - final ComboConstructor, I, Word> lambda = - (alph, mqo) -> new TTTLambdaMealy<>(alph, mqo, mqo); + final ComboConstructor, I, Word> kv = KearnsVaziraniMealy::new; + final ComboConstructor, I, Word> op = OPLearnerMealy::new; + final ComboConstructor, I, Word> sparse = SparseLearner::new; + final ComboConstructor, I, Word> ttt = TTTLearnerMealy::new; + final ComboConstructor, I, Word> lambda = TTTLambdaMealy::new; return Arrays.asList(Pair.of("L*", lstar), Pair.of("RS", rs), Pair.of("KV", kv), - Pair.of("DT", dt), + Pair.of("OP", op), + Pair.of("Sparse", sparse), Pair.of("TTT", ttt), Pair.of("TTTLambda", lambda)); } public static List, I, Word>>> getMooreLearners() { - final ComboConstructor, I, Word> lstar = - (alph, mqo) -> new ExtensibleLStarMoore<>(alph, - mqo, - Collections.emptyList(), - ObservationTableCEXHandlers.CLASSIC_LSTAR, - ClosingStrategies.CLOSE_FIRST); + final ComboConstructor, I, Word> lstar = ExtensibleLStarMoore::new; final ComboConstructor, I, Word> rs = RivestSchapireMoore::new; - final ComboConstructor, I, Word> dt = - (alph, mqo) -> new OPLearnerMoore<>(alph, mqo, LocalSuffixFinders.RIVEST_SCHAPIRE, true); - final ComboConstructor, I, Word> ttt = - (alph, mqo) -> new TTTLearnerMoore<>(alph, mqo, AcexAnalyzers.BINARY_SEARCH_FWD); + final ComboConstructor, I, Word> op = OPLearnerMoore::new; + final ComboConstructor, I, Word> ttt = TTTLearnerMoore::new; - return Arrays.asList(Pair.of("L*", lstar), Pair.of("RS", rs), Pair.of("DT", dt), Pair.of("TTT", ttt)); + return Arrays.asList(Pair.of("L*", lstar), Pair.of("RS", rs), Pair.of("OP", op), Pair.of("TTT", ttt)); } } From 83a723a237031b28901456e13fba68d675e3ec26 Mon Sep 17 00:00:00 2001 From: Markus Frohme Date: Wed, 1 Jul 2026 23:43:25 +0200 Subject: [PATCH 4/4] fix corner cases in addAlphabetSymbol Co-authored-by: stateMachinist --- .../sparse/GenericSparseLearner.java | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) 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 c01352b14..d660ae7e2 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 @@ -166,22 +166,25 @@ public Word transformAccessSequence(Word word) { @Override public void addAlphabetSymbol(I i) { - if (!this.alphabet.containsSymbol(i)) { - this.alphabet.asGrowingAlphabetOrThrowException().addSymbol(i); + alphabet.asGrowingAlphabetOrThrowException().addSymbol(i); + hyp.addAlphabetSymbol(i); + if (cRows.isEmpty()) { + return; // learning has not started yet } - this.hyp.addAlphabetSymbol(i); - - final Leaf l = new Leaf<>(); - for (CoreRow c : cRows) { - addFringeRow(c, i, l); + // add new fringe rows + if (cRows.size() == 1) { + // there is only a single state yet + final Leaf l = fRows.getFirst().leaf; + assert l != null; + final FringeRow f = addFringeRow(cRows.get(0), i, l); + query(f, Word.epsilon()); // for the first state, transition outputs must be queried manually + } else { + final Leaf l = new Leaf<>(); // fringe rows that spawn together should share the same leaf + cRows.forEach(c -> addFringeRow(c, i, l)); } - // If the suffixes are empty, we have not started the learning process yet. - // In this case, treat the symbol as if it was part of the initial alphabet. - if (!this.hyp.getStates().isEmpty()) { - updateHypothesis(); - } + updateHypothesis(); } private void updateHypothesis() { @@ -344,7 +347,7 @@ private List completeRowObservations(FringeRow f, List c, Leaf l) { for (I i : alphabet) { @@ -352,11 +355,15 @@ private void extendFringe(CoreRow c, Leaf l) { } } - private void addFringeRow(CoreRow c, I i, Leaf l) { + /** + * Creates a new fringe row and returns it after integrating it into the internal data structures. + */ + private FringeRow addFringeRow(CoreRow c, I i, Leaf l) { final Word prefix = c.prefix.append(i); final FringeRow f = new FringeRow<>(prefix, c.state, l); prefToFringe.put(prefix, f); fRows.push(f); // prioritize new rows during classification + return f; } private void identifyNewState(DefaultQuery> q) {