diff --git a/algorithms/active/pom.xml b/algorithms/active/pom.xml index 26f706662..7578c4742 100644 --- a/algorithms/active/pom.xml +++ b/algorithms/active/pom.xml @@ -43,6 +43,7 @@ limitations under the License. observation-pack observation-pack-vpa procedural + sparse ttt ttt-vpa diff --git a/algorithms/active/sparse/pom.xml b/algorithms/active/sparse/pom.xml new file mode 100644 index 000000000..6e678f3db --- /dev/null +++ b/algorithms/active/sparse/pom.xml @@ -0,0 +1,49 @@ + + + + 4.0.0 + + + de.learnlib + learnlib-algorithms-active-parent + 0.19.0-SNAPSHOT + ../pom.xml + + + learnlib-sparse + + LearnLib :: Algorithms :: Sparse + + This artifact provides the implementation of the Sparse OT learning algorithm as described in the paper "Learning Mealy Machines with Sparse Observation Tables". + + + + + de.learnlib + learnlib-util + + + de.learnlib + learnlib-counterexamples + + + de.learnlib.testsupport + learnlib-learner-it-support + + + 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/AbstractSparseLearner.java new file mode 100644 index 000000000..74cf4539b --- /dev/null +++ b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/AbstractSparseLearner.java @@ -0,0 +1,319 @@ +/* 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 de.learnlib.algorithm.sparse; + +import de.learnlib.algorithm.LearningAlgorithm.MealyLearner; +import de.learnlib.counterexample.LocalSuffixFinders; +import de.learnlib.oracle.MembershipOracle.MealyMembershipOracle; +import de.learnlib.query.DefaultQuery; +import de.learnlib.util.mealy.MealyUtil; +import net.automatalib.alphabet.Alphabet; +import net.automatalib.automaton.transducer.MealyMachine; +import net.automatalib.automaton.transducer.MutableMealyMachine; +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 { + + 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, FringeRow> prefToFringe; // fringe prefix to row + private final List, Word>> cells; // list of unique cells + private final Map, Word>, Integer> cellToIdx; // maps each unique cell to its list index + private final MutableMealyMachine hyp; // hypothesis + private final Map> stateToPrefix; // maps each state to its core row prefix + private final Function, Word> accSeq; // access sequence + + // for fast suffix ranking, we track for each suffix + // how the core rows are partitioned by it + private final Map, List> sufToVecs; + private final Map, Map, Integer>> sufToOutToIdx; + + AbstractSparseLearner(Alphabet alphabet, + MealyMembershipOracle oracle, + List> initialSuffixes, + MutableMealyMachine initialHypothesis) { + this.alphabet = alphabet; + this.oracle = oracle; + sufs = new ArrayDeque<>(initialSuffixes); + cRows = new ArrayList<>(); + fRows = new ArrayDeque<>(); + prefToFringe = new HashMap<>(); + cells = new ArrayList<>(); + cellToIdx = new HashMap<>(); + hyp = initialHypothesis; + stateToPrefix = new HashMap<>(); + accSeq = p -> stateToPrefix.get(hyp.getState(p)); + sufToVecs = new HashMap<>(); + sufToOutToIdx = new HashMap<>(); + } + + @Override + public MealyMachine getHypothesisModel() { + return hyp; + } + + @Override + public void startLearning() { + final S init = hyp.addInitialState(); + final CoreRow c = new CoreRow<>(Word.epsilon(), init, 0); + 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())); + 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 + updateHypothesis(); + } + + @Override + public boolean refineHypothesis(DefaultQuery> q) { + final DefaultQuery> qs = MealyUtil.shortenCounterExample(hyp, q); + if (qs == null) { + return false; + } + + final int oldSize = hyp.size(); + identifyNewState(qs); + assert hyp.size() > oldSize; + updateHypothesis(); + assert hyp.size() == cRows.size(); + refineHypothesis(q); // recursively exhaust counterexample + return true; + } + + private void updateHypothesis() { + for (FringeRow f : fRows) { + classifyFringePrefix(f); + if (f.leaf == null) { + updateHypothesis(); + return; + } else { + assert f.transOut != null; + hyp.setTransition(f.srcState, f.transIn, f.leaf.cRow.state, f.transOut); + } + } + } + + private void classifyFringePrefix(FringeRow f) { + f.leaf.update(cRows, sufs.size()); + if (f.leaf.isUnsplit()) { + return; + } + + final Separator sep = f.leaf.sep; + if (sep != null) { + followNode(f, sep); + } else { + f.leaf.sep = new Separator<>(pickSuffix(f.leaf.remRows), f.leaf.remRows, f.leaf.cellsIds); + followNode(f, f.leaf.sep); + } + } + + private Word pickSuffix(BitSet remRows) { + assert remRows.length() <= cRows.size(); + Word bestSuf = sufs.getFirst(); + int bestRank = Integer.MAX_VALUE; + final BitSet vec = new BitSet(); + for (Word s : sufs) { + int maxOccur = 0; + int sumOccur = 0; // checksum + for (BitSet rows : sufToVecs.get(s)) { + vec.or(remRows); + vec.and(rows); + final int occur = vec.cardinality(); + maxOccur = Math.max(maxOccur, occur); + sumOccur += occur; + } + + assert sumOccur == remRows.cardinality(); + if (maxOccur < bestRank) { + // among equally ranked suffixes, pick youngest + // (mind that suffixes are stored/iterated LIFO) + bestSuf = s; + bestRank = maxOccur; + if (bestRank == 1) { // optimization: no better suffix is possible + return bestSuf; + } + } + } + + assert bestRank < remRows.cardinality(); + return bestSuf; + } + + private void followNode(FringeRow f, Node n) { + if (n instanceof Leaf) { + final Leaf l = (Leaf) n; + assert l.isUnsplit(); + f.leaf = l; + return; + } + + final Separator sep = (Separator) n; + final Word out = query(f, sep.suffix); + final int cellIdx = getUniqueCellIdx(sep.suffix, out); + final Node next = sep.branchMap.get(out); + if (next != null) { + followNode(f, next); + return; + } + + final BitSet remRows = new BitSet(); + sep.remRows.stream().filter(i -> cRows.get(i).cellIds.contains(cellIdx)).forEach(remRows::set); + final List cellIds = new ArrayList<>(sep.cellsIds); // important: copy elements! + cellIds.add(cellIdx); + if (remRows.isEmpty()) { + // no compatible core prefix + f.leaf = null; + moveToCore(f, cellIds); + } else if (remRows.cardinality() == 1) { + final Leaf l = new Leaf<>(cRows.get(remRows.nextSetBit(0)), cRows.size(), sufs.size(), cellIds); + sep.branchMap.put(out, l); + followNode(f, l); + } else { + final Separator s = new Separator<>(pickSuffix(remRows), remRows, cellIds); + sep.branchMap.put(out, s); + followNode(f, s); + } + } + + private Word query(Row r, Word suf) { + final Word out = oracle.answerQuery(r.prefix.concat(suf)); + if (r instanceof FringeRow) { + final FringeRow f = (FringeRow) r; + f.transOut = out.prefix(f.prefix.length()).lastSymbol(); + } + + return out.suffix(suf.length()); + } + + /** 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); + final int idx = cellToIdx.computeIfAbsent(cell, c -> cellToIdx.size()); + if (idx == cells.size()) { + cells.add(cell); + } + + assert cellToIdx.size() == cells.size(); + return idx; + } + + /** returns index of new core row */ + private int moveToCore(FringeRow f, List cellIds) { + assert fRows.contains(f); + fRows.remove(f); + final S state = hyp.addState(); + final CoreRow c = new CoreRow<>(f.prefix, state, cRows.size()); + stateToPrefix.put(state, c.prefix); + assert f.transOut != null; + hyp.setTransition(f.srcState, f.transIn, state, f.transOut); + for (Integer cellIdx : completeRowObservations(f, cellIds)) { + final Pair, Word> cell = this.cells.get(cellIdx); + addCellToCoreRow(c, cell.getFirst(), cell.getSecond(), cellIdx); + } + + cRows.add(c); + extendFringe(c, state, new Leaf<>()); + assert c.cellIds.size() == sufs.size(); + assert c == cRows.get(c.idx); + return c.idx; + } + + /** 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()); + final List cellIdsFull = new ArrayList<>(cellIds); // important: copy elements! + sufsMissing.forEach(s -> cellIdsFull.add(getUniqueCellIdx(s, query(f, s)))); + return cellIdsFull; + } + + private void extendFringe(CoreRow c, S state, Leaf leaf) { + 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 + } + } + + private void identifyNewState(DefaultQuery> q) { + final Word cex = q.getInput(); + final int idxSuf = LocalSuffixFinders.findRivestSchapire(q, accSeq::apply, hyp, oracle); + final int idxSym = idxSuf - 1; + final Word u = accSeq.apply(cex.prefix(idxSym)); + final Word ui = u.append(cex.getSymbol(idxSym)); + final FringeRow f = prefToFringe.get(ui); + assert f.leaf.isUnsplit(); + final int cRowIdx = moveToCore(f, f.leaf.cellsIds); + if (f.leaf.cRow.cellIds.containsAll(cRows.get(cRowIdx).cellIds)) { + // only add new suffix if the row is not yet distinguished + addSuffixToTable(cex.subWord(idxSuf)); + } + } + + private void addSuffixToTable(Word suf) { + assert !sufs.contains(suf); + sufs.push(suf); + // this might be an extension of an existing suffix + // -> storing/iterating suffixes in LIFO order + // exploits caching when filling core rows + + // similarly, since core rows are prefix-closed, + // cache hit rate for adding suffixes is maximized + // by iterating core rows in LIFO order + for (int i = cRows.size() - 1; i >= 0; i--) { + addSuffixToCoreRow(cRows.get(i), suf); + } + } + + private void addSuffixToCoreRow(CoreRow c, Word suf) { + final Word out = query(c, suf); + final int cellIdx = getUniqueCellIdx(suf, out); + addCellToCoreRow(c, suf, out, cellIdx); + } + + private void addCellToCoreRow(CoreRow c, Word suf, Word out, Integer cellIdx) { + c.addSuffix(suf, out, cellIdx); + updatePartitionMap(c, suf, out); + } + + private void updatePartitionMap(CoreRow c, Word suf, Word out) { + final Map, Integer> outToIdx = sufToOutToIdx.computeIfAbsent(suf, s -> new HashMap<>()); + final int idx = outToIdx.computeIfAbsent(out, o -> outToIdx.size()); + final List vecs = sufToVecs.computeIfAbsent(suf, s -> new ArrayList<>()); + assert idx <= vecs.size(); + if (idx == vecs.size()) { + vecs.add(new BitSet()); + } + + 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/CoreRow.java b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/CoreRow.java new file mode 100644 index 000000000..a526bd347 --- /dev/null +++ b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/CoreRow.java @@ -0,0 +1,40 @@ +/* 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 de.learnlib.algorithm.sparse; + +import net.automatalib.word.Word; + +import java.util.*; + +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 + + 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 + } + + void addSuffix(Word suf, Word out, int cell) { + sufToOut.put(suf, out); + cellIds.add(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 new file mode 100644 index 000000000..ad033740f --- /dev/null +++ b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/FringeRow.java @@ -0,0 +1,37 @@ +/* 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 de.learnlib.algorithm.sparse; + +import net.automatalib.word.Word; + +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) + 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); + this.srcState = srcState; + this.transIn = prefix.lastSymbol(); + this.leaf = leaf; + } +} 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 new file mode 100644 index 000000000..322a2709d --- /dev/null +++ b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/Leaf.java @@ -0,0 +1,84 @@ +/* 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 de.learnlib.algorithm.sparse; + +import java.util.Collections; +import java.util.List; + +class Leaf extends Node { + + final CoreRow cRow; + private boolean split; + private int lastNumCRows; + private int lastNumSufs; + Separator sep; + // split leafs always remember how many core rows and suffixes + // the table contained at their last visit. + // this information is used as a logical timestamp + // to check whether the separator is still guaranteed to be optimal + // or if it needs to be recomputed. + + /** creates split leaf without observations */ + Leaf() { + super(Collections.emptyList()); + cRow = null; + split = true; + lastNumCRows = 0; + lastNumSufs = 0; + // timestamps will be updated automatically + } + + /** 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; + remRows.set(cRow.idx); + split = false; + lastNumCRows = numCRows; + lastNumSufs = numSufs; + } + + boolean isUnsplit() { + return !split; + } + + void update(List> cRows, int numSufs) { + assert lastNumCRows <= cRows.size(); + if (lastNumCRows == cRows.size()) { + assert lastNumSufs == numSufs; + return; + } else if (numSufs > lastNumSufs) { + lastNumSufs = numSufs; + sep = null; + } + + // since suffixes and core rows grow monotonically, + // the separator only needs to be recomputed whenever + // new compatible core prefixes emerge + // or when the suffix set grows. + + for (int i = lastNumCRows; i < cRows.size(); i++) { + final CoreRow c = cRows.get(i); + if (c.cellIds.containsAll(cellsIds)) { + remRows.set(c.idx); + split = true; + sep = null; + } + } + + lastNumCRows = cRows.size(); + } +} 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 new file mode 100644 index 000000000..455505620 --- /dev/null +++ b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/Node.java @@ -0,0 +1,34 @@ +/* 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 de.learnlib.algorithm.sparse; + +import java.util.BitSet; +import java.util.List; + +abstract 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) { + this(cellsIds, new BitSet()); + } + + 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 new file mode 100644 index 000000000..1ad9c082d --- /dev/null +++ b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/Row.java @@ -0,0 +1,26 @@ +/* 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 de.learnlib.algorithm.sparse; + +import net.automatalib.word.Word; + +abstract class Row { // type parameters required for safe casting + final Word prefix; + + 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 new file mode 100644 index 000000000..1a278d4dd --- /dev/null +++ b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/Separator.java @@ -0,0 +1,34 @@ +/* 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 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; + +class Separator extends Node { + final Word suffix; + final Map, Node> branchMap; + + Separator(Word suffix, BitSet remRows, List cells) { + super(cells, remRows); + this.suffix = suffix; + branchMap = new HashMap<>(); + } +} 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 new file mode 100644 index 000000000..4f7ff2630 --- /dev/null +++ b/algorithms/active/sparse/src/main/java/de/learnlib/algorithm/sparse/SparseLearner.java @@ -0,0 +1,38 @@ +/* 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 de.learnlib.algorithm.sparse; + +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" + */ +public class SparseLearner extends AbstractSparseLearner { + + public SparseLearner(Alphabet alphabet, MealyMembershipOracle oracle) { + this(alphabet, oracle, Collections.emptyList()); + } + + public SparseLearner(Alphabet alphabet, MealyMembershipOracle oracle, List> initialSuffixes) { + super(alphabet, oracle, initialSuffixes, new CompactMealy<>(alphabet)); + } +} \ No newline at end of file diff --git a/algorithms/active/sparse/src/main/java/module-info.java b/algorithms/active/sparse/src/main/java/module-info.java new file mode 100644 index 000000000..d59599e07 --- /dev/null +++ b/algorithms/active/sparse/src/main/java/module-info.java @@ -0,0 +1,39 @@ +/* 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. + */ + +/** + * 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)); + } +}