All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.automatalib.incremental.mealy.dag.IncrementalMealyDAGBuilder Maven / Gradle / Ivy

Go to download

A library for incremental automata construction. This artifact contains algorithms for incrementally constructing DFAs (prefix-closed and non-prefix-closed), Mealy machines, and Moore machines from a finite, incrementally growing set of example inputs/outputs.

There is a newer version: 0.11.0
Show newest version
/* Copyright (C) 2013-2014 TU Dortmund
 * This file is part of AutomataLib, http://www.automatalib.net/.
 * 
 * AutomataLib is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 3.0 as published by the Free Software Foundation.
 * 
 * AutomataLib is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with AutomataLib; if not, see
 * http://www.gnu.de/documents/lgpl.en.html.
 */
package net.automatalib.incremental.mealy.dag;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import net.automatalib.automata.concepts.StateIDs;
import net.automatalib.automata.transout.MealyMachine;
import net.automatalib.commons.util.UnionFind;
import net.automatalib.graphs.dot.DelegateDOTHelper;
import net.automatalib.graphs.dot.GraphDOTHelper;
import net.automatalib.incremental.ConflictException;
import net.automatalib.incremental.mealy.AbstractIncrementalMealyBuilder;
import net.automatalib.words.Alphabet;
import net.automatalib.words.Word;
import net.automatalib.words.WordBuilder;

/**
 * Incrementally builds an (acyclic) Mealy machine, from a set of input and corresponding
 * output words.
 * 
 * @author Malte Isberner
 *
 * @param  input symbol class
 * @param  output symbol class
 */
public class IncrementalMealyDAGBuilder extends
	AbstractIncrementalMealyBuilder {
	
	public class GraphView extends AbstractGraphView {
		@Override
		public Collection getOutgoingEdges(State node) {
			List edges = new ArrayList();
			for (int i = 0; i < alphabetSize; i++) {
				if (node.getSuccessor(i) != null)
					edges.add(new TransitionRecord(node, i));
			}
			return edges;
		}
		@Override
		public State getTarget(TransitionRecord edge) {
			return edge.source.getSuccessor(edge.transIdx);
		}
		@Override
		public Collection getNodes() {
			return Collections.unmodifiableCollection(register.values());
		}
		@Override
		@Nullable
		public I getInputSymbol(TransitionRecord edge) {
			return inputAlphabet.getSymbol(edge.transIdx);
		}
		@Override
		@Nullable
		@SuppressWarnings("unchecked")
		public O getOutputSymbol(TransitionRecord edge) {
			return (O)edge.source.getOutput(edge.transIdx);
		}
		@Override
		@Nonnull
		public State getInitialNode() {
			return init;
		}
		@Override
		public GraphDOTHelper getGraphDOTHelper() {
			return new DelegateDOTHelper(super.getGraphDOTHelper()) {
				private int id = 0;
				@Override
				public boolean getNodeProperties(State node,
						Map properties) {
					if(!super.getNodeProperties(node, properties)) {
						return false;
					}
					properties.put(NodeAttrs.LABEL, "n" + (id++));
					if(node.isConfluence()) {
						properties.put(NodeAttrs.SHAPE, NodeShapes.OCTAGON);
					}
					return true;
				}
				
			};
		}
		
	}
	
	public class AutomatonView extends AbstractTransitionSystemView {
		@Override
		public State getSuccessor(TransitionRecord transition) {
			State src = transition.source;
			return src.getSuccessor(transition.transIdx);
		}
		@Override
		public State getInitialState() {
			return init;
		}
		@Override
		public TransitionRecord getTransition(State state, I input) {
			int inputIdx = inputAlphabet.getSymbolIndex(input);
			if(state.getSuccessor(inputIdx) == null) {
				return null;
			}
			return new TransitionRecord(state, inputIdx); 
		}
		@Override
		@SuppressWarnings("unchecked")
		public O getTransitionOutput(TransitionRecord transition) {
			State src = transition.source;
			return (O)src.getOutput(transition.transIdx);
		}
	}
	
	
	
	private final Map register = new HashMap<>();

	private final int alphabetSize;
	private final State init;

	/**
	 * Constructor.
	 * 
	 * @param inputAlphabet
	 *            the input alphabet to use
	 */
	public IncrementalMealyDAGBuilder(Alphabet inputAlphabet) {
		super(inputAlphabet);
		this.alphabetSize = inputAlphabet.size();
		StateSignature initSig = new StateSignature(alphabetSize);
		this.init = new State(initSig);
		register.put(null, init);
	}

	/**
	 * Retrieves the (internal) state reached by the given input word, or
	 * null if no information about the input word is present.
	 * 
	 * @param word
	 *            the input word
	 * @return the corresponding state
	 */
	private State getState(Word word) {
		State s = init;

		for (I sym : word) {
			int idx = inputAlphabet.getSymbolIndex(sym);
			s = s.getSuccessor(idx);
			if (s == null) {
				break;
			}
		}
		return s;
	}

	/**
	 * Checks whether there exists secured information about the output for the
	 * given word.
	 * 
	 * @param word
	 *            the input word
	 * @return a boolean indicating whether information about the output for the
	 *         given input word exists.
	 * @deprecated since 2014-01-22. Use {@link #hasDefinitiveInformation(Word)}
	 */
	@Deprecated
	public boolean isComplete(Word word) {
		return hasDefinitiveInformation(word);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * net.automatalib.incremental.IncrementalConstruction#hasDefinitiveInformation
	 * (net.automatalib.words.Word)
	 */
	@Override
	public boolean hasDefinitiveInformation(Word word) {
		State s = getState(word);
		return (s != null);
	}
	
	
	/**
	 * Retrieves the output word for the given input word. If no definitive
	 * information for the input word exists, the output for the longest known
	 * prefix will be returned.
	 * 
	 * @param word
	 *            the input word
	 * @param output
	 *            a {@link WordBuilder} for constructing the output word
	 * @return true if the information contained was complete (in this
	 *         case, word.length() == output.size() will hold),
	 *         false otherwise.
	 */
	@SuppressWarnings("unchecked")
	@Override
	public boolean lookup(Word word, List output) {
		State curr = init;
		for (I sym : word) {
			int idx = inputAlphabet.getSymbolIndex(sym);
			State succ = curr.getSuccessor(idx);
			if (succ == null) {
				return false;
			}
			output.add((O) curr.getOutput(idx));
			curr = succ;
		}

		return true;
	}

	/**
	 * Incorporates a pair of input/output words into the stored information.
	 * 
	 * @param word
	 *            the input word
	 * @param outputWord
	 *            the corresponding output word
	 * @throws ConflictException
	 *             if this information conflicts with information already stored
	 */
	@Override
	public void insert(Word word, Word outputWord) {
		int len = word.length();

		State curr = init;
		State conf = null;

		Deque path = new ArrayDeque<>();

		// Find the internal state in the automaton that can be reached by a
		// maximal prefix of the word (i.e., a path of secured information)
		Iterator outWordIterator = outputWord.iterator();
		for (I sym : word) {
			// During this, store the *first* confluence state (i.e., state with
			// multiple incoming edges).
			if (conf == null && curr.isConfluence()) {
				conf = curr;
			}

			int idx = inputAlphabet.getSymbolIndex(sym);
			State succ = curr.getSuccessor(idx);
			if (succ == null)
				break;

			// If a transition exists for the input symbol, it also has an
			// output symbol.
			// Check if this matches the provided one, otherwise there is a
			// conflict
			O outSym = outWordIterator.next();
			if (!Objects.equals(outSym, curr.getOutput(idx))) {
				throw new ConflictException("Error inserting "
						+ word.prefix(path.size() + 1) + " / "
						+ outputWord.prefix(path.size() + 1)
						+ ": Incompatible output symbols: " + outSym + " vs "
						+ curr.getOutput(idx));
			}
			path.push(new PathElem(curr, idx));
			curr = succ;
		}

		State last = curr;

		int prefixLen = path.size();

		// The information was already present - we do not need to continue
		if (prefixLen == len) {
			return;
		}

		if (conf != null) {
			if (conf == last) {
				conf = null;
			}
			last = hiddenClone(last);
		} else if (last != init) {
			hide(last);
		}

		// We then create a suffix path, i.e., a linear sequence of states
		// corresponding to
		// the suffix (more precisely: the suffix minus the first symbol, since
		// this is the
		// transition which is used for gluing the suffix path to the existing
		// automaton).
		Word suffix = word.subWord(prefixLen);
		Word suffixOut = outputWord.subWord(prefixLen);

		// Here we prepare the "gluing" transition
		I sym = suffix.firstSymbol();
		int suffTransIdx = inputAlphabet.getSymbolIndex(sym);
		O suffTransOut = suffixOut.firstSymbol();

		State suffixState = createSuffix(suffix.subWord(1),
				suffixOut.subWord(1));

		if (last != init) {
			last = unhide(last, suffTransIdx, suffixState, suffTransOut);
		} else {
			updateInitSignature(suffTransIdx, suffixState, suffTransOut);
		}

		if (path.isEmpty()) {
			return;
		}

		if (conf != null) {
			// If there was a confluence state, we have to clone all nodes on
			// the prefix path up to this state, in order to separate it from
			// other
			// prefixes reaching the confluence state (we do not know anything
			// about them
			// plus the suffix).
			PathElem next;
			do {
				next = path.pop();
				State state = next.state;
				int idx = next.transIdx;
				state = clone(state, idx, last);
				last = state;
			} while (next.state != conf);
		}

		// Finally, we have to refresh all the signatures, iterating backwards
		// until the updating becomes stable.
		while (path.size() > 1) {
			PathElem next = path.pop();
			State state = next.state;
			int idx = next.transIdx;
			State updated = updateSignature(state, idx, last);
			if (state == updated)
				return;
			last = updated;
		}

		int finalIdx = path.pop().transIdx;

		updateInitSignature(finalIdx, last);
	}

	/**
	 * Update the signature of the initial state. This requires special
	 * handling, as the initial state is not stored in the register (since it
	 * can never legally act as a predecessor).
	 * 
	 * @param idx
	 *            the transition index being changed
	 * @param succ
	 *            the new successor state
	 */
	private void updateInitSignature(int idx, State succ) {
		StateSignature sig = init.getSignature();
		State oldSucc = sig.successors[idx];
		if (oldSucc == succ)
			return;
		if (oldSucc != null)
			oldSucc.decreaseIncoming();
		sig.successors[idx] = succ;
		succ.increaseIncoming();
	}

	/**
	 * Update the signature of a state, changing only the successor state of a
	 * single transition index.
	 * 
	 * @param state
	 *            the state which's signature to update
	 * @param idx
	 *            the transition index to modify
	 * @param succ
	 *            the new successor state
	 * @return the resulting state, which can either be the same as the input
	 *         state (if the new signature is unique), or the result of merging
	 *         with another state.
	 */
	private State updateSignature(State state, int idx, State succ) {
		StateSignature sig = state.getSignature();
		if (sig.successors[idx] == succ)
			return state;

		register.remove(sig);
		if (sig.successors[idx] != null)
			sig.successors[idx].decreaseIncoming();
		sig.successors[idx] = succ;
		succ.increaseIncoming();
		sig.updateHashCode();
		return replaceOrRegister(state);
	}

	private State unhide(State state, int idx, State succ, O out) {
		StateSignature sig = state.getSignature();
		State prevSucc = sig.successors[idx];
		if (prevSucc != null) {
			prevSucc.decreaseIncoming();
		}
		sig.successors[idx] = succ;
		if (succ != null) {
			succ.increaseIncoming();
		}
		sig.outputs[idx] = out;
		sig.updateHashCode();
		return replaceOrRegister(state);
	}

	/**
	 * Updates the signature of the initial state, changing both the successor
	 * state and the output symbol.
	 * 
	 * @param idx
	 *            the transition index to change
	 * @param succ
	 *            the new successor state
	 * @param out
	 *            the output symbol
	 */
	private void updateInitSignature(int idx, State succ, O out) {
		StateSignature sig = init.getSignature();
		State oldSucc = sig.successors[idx];
		if (oldSucc == succ && Objects.equals(out, sig.outputs[idx])) {
			return;
		}
		if (oldSucc != null) {
			oldSucc.decreaseIncoming();
		}
		sig.successors[idx] = succ;
		sig.outputs[idx] = out;
		succ.increaseIncoming();
	}

	private State clone(State other, int idx, State succ) {
		StateSignature sig = other.getSignature();
		if (sig.successors[idx] == succ)
			return other;
		sig = sig.duplicate();
		sig.successors[idx] = succ;
		sig.updateHashCode();
		return replaceOrRegister(sig);
	}

	private State hiddenClone(State other) {
		StateSignature sig = other.getSignature().duplicate();

		for (int i = 0; i < alphabetSize; i++) {
			State succ = sig.successors[i];
			if (succ != null) {
				succ.increaseIncoming();
			}
		}
		return new State(sig);
	}

	private State replaceOrRegister(StateSignature sig) {
		State state = register.get(sig);
		if (state != null)
			return state;

		register.put(sig, state = new State(sig));
		for (int i = 0; i < sig.successors.length; i++) {
			State succ = sig.successors[i];
			if (succ != null)
				succ.increaseIncoming();
		}
		return state;
	}

	private void hide(State state) {
		assert state != init;
		StateSignature sig = state.getSignature();

		register.remove(sig);
	}

	private State replaceOrRegister(State state) {
		StateSignature sig = state.getSignature();
		State other = register.get(sig);
		if (other != null) {
			if (state != other) {
				for (int i = 0; i < sig.successors.length; i++) {
					State succ = sig.successors[i];
					if (succ != null) {
						succ.decreaseIncoming();
					}
				}
			}
			return other;
		}

		register.put(sig, state);
		return state;
	}

	private State createSuffix(Word suffix, Word suffixOut) {
		StateSignature sig = new StateSignature(alphabetSize);
		sig.updateHashCode();
		State last = replaceOrRegister(sig);

		int len = suffix.length();
		for (int i = len - 1; i >= 0; i--) {
			sig = new StateSignature(alphabetSize);
			I sym = suffix.getSymbol(i);
			O outsym = suffixOut.getSymbol(i);
			int idx = inputAlphabet.getSymbolIndex(sym);
			sig.successors[idx] = last;
			sig.outputs[idx] = outsym;
			sig.updateHashCode();
			last = replaceOrRegister(sig);
		}

		return last;
	}
	
	@Override
	public GraphView asGraph() {
		return new GraphView();
	}
	
	@Override
	public AutomatonView asTransitionSystem() {
		return new AutomatonView();
	}

	/*
	 * (non-Javadoc)
	 * @see net.automatalib.incremental.IncrementalConstruction#findSeparatingWord(java.lang.Object, java.util.Collection, boolean)
	 */
	@Override
	public Word findSeparatingWord(MealyMachine target,
			Collection inputs, boolean omitUndefined) {
		return doFindSeparatingWord(target, inputs, omitUndefined);
	}

	// /////////////////////////////////////////////////////////////////////
	// Equivalence test //
	// /////////////////////////////////////////////////////////////////////

	private static int getStateId(State state, Map ids) {
		Integer id = ids.get(state);
		if (id == null) {
			id = ids.size();
			ids.put(state, id);
		}
		return id.intValue();
	}

	private static final class Record {
		private final State state1;
		private final S state2;
		private final I reachedVia;
		private final Record reachedFrom;
		private final int depth;

		public Record(State state1, S state2, Record reachedFrom,
				I reachedVia) {
			this.state1 = state1;
			this.state2 = state2;
			this.reachedFrom = reachedFrom;
			this.reachedVia = reachedVia;
			this.depth = (reachedFrom != null) ? reachedFrom.depth + 1 : 0;
		}

		public Record(State state1, S state2) {
			this(state1, state2, null, null);
		}
	}

	private  Word doFindSeparatingWord(MealyMachine mealy,
			Collection inputs, boolean omitUndefined) {
		int thisStates = register.size();

		UnionFind uf = new UnionFind(thisStates + mealy.size());

		Map ids = new HashMap();

		State init1 = init;
		S init2 = mealy.getInitialState();

		if (init2 == null)
			return omitUndefined ? null : Word. epsilon();

		StateIDs mealyIds = mealy.stateIDs();

		int id1 = getStateId(init1, ids), id2 = mealyIds.getStateId(init2)
				+ thisStates;

		uf.link(id1, id2);

		Queue> queue = new ArrayDeque>();

		queue.offer(new Record(init1, init2));

		I lastSym = null;

		Record current;

		explore: while ((current = queue.poll()) != null) {
			State state1 = current.state1;
			S state2 = current.state2;

			for (I sym : inputs) {
				int idx = inputAlphabet.getSymbolIndex(sym);
				State succ1 = state1.getSuccessor(idx);
				if (succ1 == null)
					continue;

				T trans2 = mealy.getTransition(state2, sym);
				if (trans2 == null) {
					if (omitUndefined)
						continue;
					lastSym = sym;
					break explore;
				}

				Object out1 = state1.getOutput(idx);
				Object out2 = mealy.getTransitionOutput(trans2);
				if (!Objects.equals(out1, out2)) {
					lastSym = sym;
					break explore;
				}

				S succ2 = mealy.getSuccessor(trans2);

				id1 = getStateId(succ1, ids);
				id2 = mealyIds.getStateId(succ2) + thisStates;

				int r1 = uf.find(id1), r2 = uf.find(id2);

				if (r1 == r2)
					continue;

				uf.link(r1, r2);

				queue.offer(new Record<>(succ1, succ2, current, sym));
			}
		}

		if (current == null)
			return null;

		int ceLength = current.depth;
		if (lastSym != null)
			ceLength++;

		WordBuilder wb = new WordBuilder(null, ceLength);

		int index = ceLength;

		if (lastSym != null)
			wb.setSymbol(--index, lastSym);

		while (current.reachedFrom != null) {
			wb.setSymbol(--index, current.reachedVia);
			current = current.reachedFrom;
		}

		return wb.toWord();
	}
}