Java tutorial
/* Copyright (C) 2013-2018 TU Dortmund * This file is part of AutomataLib, http://www.automatalib.net/. * * 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 net.automatalib.util.automata.ads; import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.google.common.collect.Sets; import net.automatalib.automata.transout.MealyMachine; import net.automatalib.commons.util.Pair; import net.automatalib.graphs.ads.ADSNode; import net.automatalib.graphs.ads.impl.ADSLeafNode; import net.automatalib.graphs.base.compact.CompactEdge; import net.automatalib.graphs.base.compact.CompactSimpleGraph; import net.automatalib.util.graphs.Path; import net.automatalib.util.graphs.ShortestPaths; import net.automatalib.util.graphs.traversal.GraphTraversal; import net.automatalib.words.Alphabet; import net.automatalib.words.Word; /** * Algorithm of Lee and Yannakakis for computing adaptive distinguishing sequences (of length at most n^2) in O(n^2) * time (where n denotes the number of states of the automaton). * <p> * See: D. Lee and M. Yannakakis - "Testing Finite-State Machines: State Identification and Verification", IEEE * Transactions on Computers 43.3 (1994) * * @author frohme */ public final class LeeYannakakis { private LeeYannakakis() { } /** * Computes an ADS using the algorithm of Lee and Yannakakis. * * @param automaton * The automaton for which an ADS should be computed * @param input * the input alphabet of the automaton * @param <S> * (hypothesis) state type * @param <I> * input alphabet type * @param <O> * output alphabet type * * @return A {@link LYResult} containing an adaptive distinguishing sequence (if existent) and a possible set of * indistinguishable states. */ public static <S, I, O> LYResult<S, I, O> compute(final MealyMachine<S, I, ?, O> automaton, final Alphabet<I> input) { final SplitTreeResult<S, I, O> str = computeSplitTree(automaton, input); if (str.isPresent()) { final Set<S> states = new HashSet<>(automaton.getStates()); return new LYResult<>(extractADS(automaton, str.get(), states, states.stream().collect(Collectors.toMap(Function.identity(), Function.identity())), null)); } return new LYResult<>(str.getIndistinguishableStates()); } private static <S, I, O> SplitTreeResult<S, I, O> computeSplitTree(final MealyMachine<S, I, ?, O> automaton, final Alphabet<I> input) { final SplitTree<S, I, O> st = new SplitTree<>(new HashSet<>(automaton.getStates())); final Set<SplitTree<S, I, O>> leaves = Sets.newHashSetWithExpectedSize(automaton.size()); leaves.add(st); while (leaves.stream().anyMatch(LeeYannakakis::needsRefinement)) { final int maxCardinality = leaves.stream().mapToInt(x -> x.getPartition().size()).max().getAsInt(); final Set<SplitTree<S, I, O>> R = leaves.stream().filter(x -> x.getPartition().size() == maxCardinality) .collect(Collectors.toSet()); final Map<Validity, Set<Pair<Word<I>, SplitTree<S, I, O>>>> validitySetMap = computeValidities( automaton, input, R, leaves); if (!validitySetMap.get(Validity.INVALID).isEmpty()) { final Set<Pair<Word<I>, SplitTree<S, I, O>>> set = validitySetMap.get(Validity.INVALID); final Set<S> indistinguishableStates = new HashSet<>(); for (final Pair<Word<I>, SplitTree<S, I, O>> pair : set) { indistinguishableStates.addAll(pair.getSecond().getPartition()); } return new SplitTreeResult<>(indistinguishableStates); } // a-valid partitions for (final Pair<Word<I>, SplitTree<S, I, O>> aPartition : validitySetMap.get(Validity.A_VALID)) { assert aPartition.getFirst().size() == 1 : "a-valid inputs should always contain exactly 1 symbol"; final I aValidInput = aPartition.getFirst().firstSymbol(); final SplitTree<S, I, O> nodeToRefine = aPartition.getSecond(); final Map<O, Set<S>> successorMap = nodeToRefine.getPartition().stream().collect( Collectors.groupingBy(s -> automaton.getOutput(s, aValidInput), Collectors.toSet())); nodeToRefine.setSequence(Word.fromSymbols(aValidInput)); leaves.remove(nodeToRefine); for (Map.Entry<O, Set<S>> entry : successorMap.entrySet()) { final SplitTree<S, I, O> child = new SplitTree<>(entry.getValue()); nodeToRefine.getSuccessors().put(entry.getKey(), child); leaves.add(child); } for (final S s : nodeToRefine.getPartition()) { nodeToRefine.getMapping().put(s, automaton.getSuccessor(s, aValidInput)); } } // b-valid partitions for (final Pair<Word<I>, SplitTree<S, I, O>> bPartition : validitySetMap.get(Validity.B_VALID)) { assert bPartition.getFirst().size() == 1 : "b-valid inputs should always contain exactly 1 symbol"; final I bValidInput = bPartition.getFirst().firstSymbol(); final SplitTree<S, I, O> nodeToRefine = bPartition.getSecond(); final Map<S, S> successorsToNodes = nodeToRefine.getPartition().stream().collect( Collectors.toMap(x -> automaton.getSuccessor(x, bValidInput), Function.identity())); final SplitTree<S, I, O> v = st.findLowestSubsetNode(successorsToNodes.keySet()) .orElseThrow(IllegalStateException::new); nodeToRefine.setSequence(v.getSequence().prepend(bValidInput)); leaves.remove(nodeToRefine); for (final Map.Entry<O, SplitTree<S, I, O>> entry : v.getSuccessors().entrySet()) { final Set<S> wSet = entry.getValue().getPartition(); final Set<S> intersection = new HashSet<>(successorsToNodes.keySet()); intersection.retainAll(wSet); if (!intersection.isEmpty()) { final Set<S> indistinguishableNodes = intersection.stream().map(successorsToNodes::get) .collect(Collectors.toSet()); final SplitTree<S, I, O> newChild = new SplitTree<>(indistinguishableNodes); nodeToRefine.getSuccessors().put(entry.getKey(), newChild); leaves.add(newChild); } } for (final S s : nodeToRefine.getPartition()) { nodeToRefine.getMapping().put(s, v.getMapping().get(automaton.getSuccessor(s, bValidInput))); } } // c-valid partitions for (final Pair<Word<I>, SplitTree<S, I, O>> cPartition : validitySetMap.get(Validity.C_VALID)) { final Word<I> cValidInput = cPartition.getFirst(); final SplitTree<S, I, O> nodeToRefine = cPartition.getSecond(); final Map<S, S> successorsToNodes = nodeToRefine.getPartition().stream().collect( Collectors.toMap(x -> automaton.getSuccessor(x, cValidInput), Function.identity())); final SplitTree<S, I, O> C = st.findLowestSubsetNode(successorsToNodes.keySet()) .orElseThrow(IllegalStateException::new); nodeToRefine.setSequence(cValidInput.concat(C.getSequence())); leaves.remove(nodeToRefine); for (final Map.Entry<O, SplitTree<S, I, O>> entry : C.getSuccessors().entrySet()) { final Set<S> wSet = entry.getValue().getPartition(); final Set<S> intersection = new HashSet<>(successorsToNodes.keySet()); intersection.retainAll(wSet); if (!intersection.isEmpty()) { final Set<S> indistinguishableNodes = intersection.stream().map(successorsToNodes::get) .collect(Collectors.toSet()); final SplitTree<S, I, O> newChild = new SplitTree<>(indistinguishableNodes); nodeToRefine.getSuccessors().put(entry.getKey(), newChild); leaves.add(newChild); } } for (final S s : nodeToRefine.getPartition()) { nodeToRefine.getMapping().put(s, C.getMapping().get(automaton.getSuccessor(s, cValidInput))); } } } return new SplitTreeResult<>(st); } private static <S, I, O> ADSNode<S, I, O> extractADS(final MealyMachine<S, I, ?, O> automaton, final SplitTree<S, I, O> st, final Set<S> currentSet, final Map<S, S> currentToInitialMapping, final ADSNode<S, I, O> predecessor) { if (currentSet.size() == 1) { final S currentNode = currentSet.iterator().next(); assert currentToInitialMapping.containsKey(currentNode); return new ADSLeafNode<>(predecessor, currentToInitialMapping.get(currentNode)); } final SplitTree<S, I, O> u = st.findLowestSubsetNode(currentSet).orElseThrow(IllegalStateException::new); final Pair<ADSNode<S, I, O>, ADSNode<S, I, O>> ads = ADSUtil.buildFromTrace(automaton, u.getSequence(), currentSet.iterator().next()); final ADSNode<S, I, O> head = ads.getFirst(); final ADSNode<S, I, O> tail = ads.getSecond(); head.setParent(predecessor); for (final Map.Entry<O, SplitTree<S, I, O>> entry : u.getSuccessors().entrySet()) { final O output = entry.getKey(); final SplitTree<S, I, O> tree = entry.getValue(); final Set<S> intersection = new HashSet<>(tree.getPartition()); intersection.retainAll(currentSet); if (!intersection.isEmpty()) { final Map<S, S> nextCurrentToInitialMapping = intersection.stream() .collect(Collectors.toMap(key -> u.getMapping().get(key), currentToInitialMapping::get)); final Set<S> nextCurrent = intersection.stream().map(x -> u.getMapping().get(x)) .collect(Collectors.toSet()); tail.getChildren().put(output, extractADS(automaton, st, nextCurrent, nextCurrentToInitialMapping, tail)); } } return head; } private static <S, I, O> boolean needsRefinement(final SplitTree<S, I, O> node) { return node.getPartition().size() > 1; } private static <S, I, O> boolean isValidInput(final MealyMachine<S, I, ?, O> automaton, final I input, final Set<S> states) { final Map<O, Set<S>> successors = new HashMap<>(); for (final S s : states) { final O output = automaton.getOutput(s, input); final S successor = automaton.getSuccessor(s, input); if (!successors.containsKey(output)) { successors.put(output, new HashSet<>()); } if (!successors.get(output).add(successor)) { return false; } } return true; } private static <S, I, O> Map<Validity, Set<Pair<Word<I>, SplitTree<S, I, O>>>> computeValidities( final MealyMachine<S, I, ?, O> automaton, final Alphabet<I> inputs, final Set<SplitTree<S, I, O>> R, final Set<SplitTree<S, I, O>> pi) { final Map<Validity, Set<Pair<Word<I>, SplitTree<S, I, O>>>> result = new EnumMap<>(Validity.class); final Map<S, Integer> stateToPartitionMap = new HashMap<>(); final BiMap<Integer, SplitTree<S, I, O>> partitionToNodeMap = HashBiMap.create(); int counter = 0; for (SplitTree<S, I, O> partition : pi) { for (final S s : partition.getPartition()) { final Integer previousValue = stateToPartitionMap.put(s, counter); assert previousValue == null : "Not a true partition"; } partitionToNodeMap.put(counter, partition); counter++; } for (final Validity v : Validity.values()) { result.put(v, new HashSet<>()); } final Set<SplitTree<S, I, O>> pendingCs = new HashSet<>(); final Map<Integer, Validity> partitionToClassificationMap = new HashMap<>(); final CompactSimpleGraph<I> implicationGraph = new CompactSimpleGraph<>(partitionToNodeMap.size()); for (int i = 0; i < partitionToNodeMap.size(); i++) { implicationGraph.addIntNode(); } partitionLoop: for (final SplitTree<S, I, O> B : R) { // general validity final Map<I, Boolean> validInputMap = inputs.stream().collect(Collectors.toMap(Function.identity(), input -> isValidInput(automaton, input, B.getPartition()))); // a valid for (final I i : inputs) { if (!validInputMap.get(i)) { continue; } final Set<O> outputs = B.getPartition().stream().map(s -> automaton.getOutput(s, i)) .collect(Collectors.toSet()); if (outputs.size() > 1) { result.get(Validity.A_VALID).add(new Pair<>(Word.fromSymbols(i), B)); partitionToClassificationMap.put(stateToPartitionMap.get(B.getPartition().iterator().next()), Validity.A_VALID); continue partitionLoop; } } // b valid for (final I i : inputs) { if (!validInputMap.get(i)) { continue; } final Set<Integer> successors = B.getPartition().stream() .map(s -> stateToPartitionMap.get(automaton.getSuccessor(s, i))) .collect(Collectors.toSet()); if (successors.size() > 1) { result.get(Validity.B_VALID).add(new Pair<>(Word.fromSymbols(i), B)); partitionToClassificationMap.put(stateToPartitionMap.get(B.getPartition().iterator().next()), Validity.B_VALID); continue partitionLoop; } } // c valid // we defer evaluation to later point in time, because we need to check if the target partitions are a- or b-valid for (final I i : inputs) { if (!validInputMap.get(i)) { continue; } final S nodeInPartition = B.getPartition().iterator().next(); final S successor = automaton.getSuccessor(nodeInPartition, i); final Integer partition = stateToPartitionMap.get(nodeInPartition); final Integer successorPartition = stateToPartitionMap.get(successor); if (!partition.equals(successorPartition)) { implicationGraph.connect(partition, successorPartition, i); pendingCs.add(B); } } if (pendingCs.contains(B)) { continue partitionLoop; } //if we haven't continued the loop up until here, there is no valid input result.get(Validity.INVALID).add(new Pair<>(null, B)); } //check remaining potential Cs pendingCLoop: for (final SplitTree<S, I, O> pendingC : pendingCs) { final Integer pendingPartition = partitionToNodeMap.inverse().get(pendingC); final Iterator<Integer> iter = GraphTraversal.bfIterator(implicationGraph, Collections.singleton(pendingPartition)); while (iter.hasNext()) { final Integer successor = iter.next(); final Validity successorValidity = partitionToClassificationMap.get(successor); if (successorValidity == Validity.A_VALID || successorValidity == Validity.B_VALID) { final Path<Integer, CompactEdge<I>> path = ShortestPaths.shortestPath(implicationGraph, pendingPartition, implicationGraph.size(), successor); final List<I> word = path.edgeList().stream().map(CompactEdge::getProperty) .collect(Collectors.toList()); result.get(Validity.C_VALID).add(new Pair<>(Word.fromList(word), pendingC)); continue pendingCLoop; } } result.get(Validity.INVALID).add(new Pair<>(null, pendingC)); } return result; } private enum Validity { A_VALID, B_VALID, C_VALID, INVALID } }