001/* Copyright (C) 2013 TU Dortmund 002 * This file is part of LearnLib, http://www.learnlib.de/. 003 * 004 * LearnLib is free software; you can redistribute it and/or 005 * modify it under the terms of the GNU Lesser General Public 006 * License version 3.0 as published by the Free Software Foundation. 007 * 008 * LearnLib is distributed in the hope that it will be useful, 009 * but WITHOUT ANY WARRANTY; without even the implied warranty of 010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 011 * Lesser General Public License for more details. 012 * 013 * You should have received a copy of the GNU Lesser General Public 014 * License along with LearnLib; if not, see 015 * <http://www.gnu.de/documents/lgpl.en.html>. 016 */ 017package de.learnlib.algorithms.baselinelstar; 018 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.LinkedHashSet; 023import java.util.List; 024import java.util.Set; 025 026import net.automatalib.automata.fsa.DFA; 027import net.automatalib.words.Alphabet; 028import net.automatalib.words.Word; 029import de.learnlib.api.LearningAlgorithm.DFALearner; 030import de.learnlib.api.MembershipOracle; 031import de.learnlib.oracles.DefaultQuery; 032 033/** 034 * Implementation of the L* algorithm by Dana Angluin 035 * 036 * @param <I> 037 * input symbol class. 038 */ 039public class BaselineLStar<I> implements DFALearner<I> { 040 041 private final Alphabet<I> alphabet; 042 043 private final MembershipOracle<I, Boolean> oracle; 044 045 private ObservationTable<I> observationTable; 046 047 private boolean startLearningAlreadyCalled; 048 049 /** 050 * Initializes a newly created baseline L* implementation. After this, the method 051 * {@link #startLearning()} may be called once. 052 * 053 * @param alphabet 054 * The {@link Alphabet} to learn. 055 * @param oracle 056 * The {@link MembershipOracle} which is used for membership queries. 057 */ 058 public BaselineLStar(Alphabet<I> alphabet, MembershipOracle<I, Boolean> oracle) { 059 this.alphabet = alphabet; 060 this.oracle = oracle; 061 this.observationTable = new ObservationTable<>(); 062 063 LinkedHashSet<Word<I>> initialCandidates = observationTable.getCandidates(); 064 065 for (I alphabetSymbol : alphabet) { 066 initialCandidates.add(Word.fromLetter(alphabetSymbol)); 067 } 068 } 069 070 @Override 071 public void startLearning() { 072 if (startLearningAlreadyCalled) { 073 throw new IllegalStateException("startLearning may only be called once!"); 074 } 075 076 final List<Word<I>> allSuffixes = observationTable.getSuffixes(); 077 078 processMembershipQueriesForStates(observationTable.getStates(), allSuffixes); 079 processMembershipQueriesForStates(observationTable.getCandidates(), allSuffixes); 080 081 makeTableClosedAndConsistent(); 082 083 startLearningAlreadyCalled = true; 084 } 085 086 @Override 087 public boolean refineHypothesis(DefaultQuery<I, Boolean> ceQuery) { 088 if (!startLearningAlreadyCalled) { 089 throw new IllegalStateException("Unable to refine hypothesis before first learn iteration!"); 090 } 091 092 LinkedHashSet<Word<I>> states = observationTable.getStates(); 093 LinkedHashSet<Word<I>> candidates = observationTable.getCandidates(); 094 095 LinkedHashSet<Word<I>> prefixes = prefixesOfWordNotInStates(ceQuery.getInput()); 096 097 states.addAll(prefixes); 098 removeStatesFromCandidates(); 099 100 LinkedHashSet<Word<I>> newCandidates = getNewCandidatesFromPrefixes(prefixes); 101 candidates.addAll(newCandidates); 102 103 processMembershipQueriesForStates(prefixes, observationTable.getSuffixes()); 104 processMembershipQueriesForStates(newCandidates, observationTable.getSuffixes()); 105 106 makeTableClosedAndConsistent(); 107 108 return true; 109 } 110 111 private LinkedHashSet<Word<I>> prefixesOfWordNotInStates(Word<I> word) { 112 LinkedHashSet<Word<I>> states = observationTable.getStates(); 113 114 LinkedHashSet<Word<I>> prefixes = new LinkedHashSet<>(); 115 for (Word<I> prefix : prefixesOfWord(word)) { 116 if (!states.contains(prefix)) { 117 prefixes.add(prefix); 118 } 119 } 120 121 return prefixes; 122 } 123 124 private void removeStatesFromCandidates() { 125 LinkedHashSet<Word<I>> states = observationTable.getStates(); 126 LinkedHashSet<Word<I>> candidates = observationTable.getCandidates(); 127 candidates.removeAll(states); 128 } 129 130 private LinkedHashSet<Word<I>> getNewCandidatesFromPrefixes(LinkedHashSet<Word<I>> prefixes) { 131 LinkedHashSet<Word<I>> newCandidates = new LinkedHashSet<>(); 132 133 for (Word<I> prefix : prefixes) { 134 Set<Word<I>> possibleCandidates = appendAlphabetSymbolsToWord(prefix); 135 for (Word<I> possibleCandidate :possibleCandidates) { 136 if (!observationTable.getStates().contains(possibleCandidate)) { 137 newCandidates.add(possibleCandidate); 138 } 139 } 140 } 141 142 return newCandidates; 143 } 144 145 /** 146 * Appends each symbol of the alphabet (with size m) to the given word (with size w), 147 * thus returning m words with a length of w+1. 148 * 149 * @param word 150 * The {@link Word} to which the {@link Alphabet} is appended. 151 * @return 152 * A set with the size of the alphabet, containing each time the word 153 * appended with an alphabet symbol. 154 */ 155 private LinkedHashSet<Word<I>> appendAlphabetSymbolsToWord(Word<I> word) { 156 LinkedHashSet<Word<I>> newCandidates = new LinkedHashSet<>(alphabet.size()); 157 for (I alphabetSymbol : alphabet) { 158 Word<I> newCandidate = word.append(alphabetSymbol); 159 newCandidates.add(newCandidate); 160 } 161 return newCandidates; 162 } 163 164 @Override 165 public DFA<?, I> getHypothesisModel() { 166 if (!startLearningAlreadyCalled) { 167 throw new IllegalStateException("Unable to get hypothesis model before first learn iteration!"); 168 } 169 170 return observationTable.toAutomaton(alphabet); 171 } 172 173 /** 174 * After calling this method the observation table is both closed and consistent. 175 */ 176 private void makeTableClosedAndConsistent() { 177 boolean closedAndConsistent = false; 178 179 while (!closedAndConsistent) { 180 closedAndConsistent = true; 181 182 if (!observationTable.isClosed()) { 183 closedAndConsistent = false; 184 closeTable(); 185 } 186 187 if (!observationTable.isConsistentWithAlphabet(alphabet)) { 188 closedAndConsistent = false; 189 ensureConsistency(); 190 } 191 } 192 } 193 194 /** 195 * After calling this method the observation table is closed. 196 */ 197 private void closeTable() { 198 Word<I> candidate = observationTable.findUnclosedState(); 199 200 while (candidate != null) { 201 observationTable.getStates().add(candidate); 202 observationTable.getCandidates().remove(candidate); 203 204 LinkedHashSet<Word<I>> newCandidates = appendAlphabetSymbolsToWord(candidate); 205 206 observationTable.getCandidates().addAll(newCandidates); 207 208 processMembershipQueriesForStates(newCandidates, observationTable.getSuffixes()); 209 210 candidate = observationTable.findUnclosedState(); 211 } 212 } 213 214 /** 215 * After calling this method the observation table is consistent. 216 */ 217 private void ensureConsistency() { 218 InconsistencyDataHolder<I> dataHolder = observationTable.findInconsistentSymbol(alphabet); 219 220 Word<I> witness = observationTable.determineWitnessForInconsistency(dataHolder); 221 Word<I> newSuffix = Word.fromSymbols(dataHolder.getDifferingSymbol()).concat(witness); 222 observationTable.getSuffixes().add(newSuffix); 223 224 List<Word<I>> singleSuffixList = Collections.singletonList(newSuffix); 225 226 processMembershipQueriesForStates(observationTable.getStates(), singleSuffixList); 227 processMembershipQueriesForStates(observationTable.getCandidates(), singleSuffixList); 228 } 229 230 /** 231 * When new states are added to the observation table, this method fills the table values. For each 232 * given state it sends one membership query for each specified suffix symbol to the oracle of the 233 * form (state,symbol). 234 * 235 * @param states 236 * The new states which should be evaluated. 237 * @param suffixes 238 * The suffixes which are appended to the states before sending the resulting word to the oracle. 239 */ 240 private void processMembershipQueriesForStates(LinkedHashSet<Word<I>> states, Collection<Word<I>> suffixes) { 241 List<DefaultQuery<I, Boolean>> queries = new ArrayList<>(states.size()); 242 for (Word<I> state : states) { 243 for (Word<I> suffix : suffixes) { 244 queries.add(new DefaultQuery<I, Boolean>(state, suffix)); 245 } 246 } 247 248 oracle.processQueries(queries); 249 250 251 for(DefaultQuery<I,Boolean> query : queries) { 252 Word<I> state = query.getPrefix(); 253 Word<I> suffix = query.getSuffix(); 254 observationTable.addResult(state, suffix, query.getOutput()); 255 } 256 } 257 258 /** 259 * A {@link Word} of the length n may have n prefixes of the length 1-n. 260 * This method returns all of them. 261 * 262 * @param word 263 * The word for which the prefixes should be returned. 264 * @return A list of all prefixes for the given word. 265 */ 266 // FIXME: This is superseded by Word.suffixes(boolean). Please adapt -misberner 267 private static <I> List<Word<I>> prefixesOfWord(Word<I> word) { 268 List<Word<I>> prefixes = new ArrayList<>(word.size()); 269 for (int i = 1; i <= word.size(); i++) { 270 prefixes.add(word.prefix(i)); 271 } 272 return prefixes; 273 } 274 275 public String getStringRepresentationOfObservationTable() { 276 return ObservationTablePrinter.getPrintableStringRepresentation(observationTable); 277 } 278 279}