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}