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

net.automatalib.util.automata.ads.LeeYannakakis Maven / Gradle / Ivy

Go to download

This artifact provides various common utility operations for analyzing and manipulating automata and graphs, such as traversal, minimization and copying.

There is a newer version: 0.11.0
Show newest version
/* Copyright (C) 2013-2019 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.transducers.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).
 * 

* 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 * (hypothesis) state type * @param * input alphabet type * @param * output alphabet type * * @return A {@link LYResult} containing an adaptive distinguishing sequence (if existent) and a possible set of * indistinguishable states. */ public static LYResult compute(final MealyMachine automaton, final Alphabet input) { final SplitTreeResult str = computeSplitTree(automaton, input); if (str.isPresent()) { final Set 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 SplitTreeResult computeSplitTree(final MealyMachine automaton, final Alphabet input) { final SplitTree st = new SplitTree<>(new HashSet<>(automaton.getStates())); final Set> 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> R = leaves.stream().filter(x -> x.getPartition().size() == maxCardinality).collect(Collectors.toSet()); final Map, SplitTree>>> validitySetMap = computeValidities(automaton, input, R, leaves); if (!validitySetMap.get(Validity.INVALID).isEmpty()) { final Set, SplitTree>> set = validitySetMap.get(Validity.INVALID); final Set indistinguishableStates = new HashSet<>(); for (final Pair, SplitTree> pair : set) { indistinguishableStates.addAll(pair.getSecond().getPartition()); } return new SplitTreeResult<>(indistinguishableStates); } // a-valid partitions for (final Pair, SplitTree> 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 nodeToRefine = aPartition.getSecond(); final Map> 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> entry : successorMap.entrySet()) { final SplitTree 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, SplitTree> 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 nodeToRefine = bPartition.getSecond(); final Map successorsToNodes = nodeToRefine.getPartition() .stream() .collect(Collectors.toMap(x -> automaton.getSuccessor(x, bValidInput), Function.identity())); final SplitTree v = st.findLowestSubsetNode(successorsToNodes.keySet()).orElseThrow(IllegalStateException::new); nodeToRefine.setSequence(v.getSequence().prepend(bValidInput)); leaves.remove(nodeToRefine); for (final Map.Entry> entry : v.getSuccessors().entrySet()) { final Set wSet = entry.getValue().getPartition(); final Set intersection = new HashSet<>(successorsToNodes.keySet()); intersection.retainAll(wSet); if (!intersection.isEmpty()) { final Set indistinguishableNodes = intersection.stream().map(successorsToNodes::get).collect(Collectors.toSet()); final SplitTree 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, SplitTree> cPartition : validitySetMap.get(Validity.C_VALID)) { final Word cValidInput = cPartition.getFirst(); final SplitTree nodeToRefine = cPartition.getSecond(); final Map successorsToNodes = nodeToRefine.getPartition() .stream() .collect(Collectors.toMap(x -> automaton.getSuccessor(x, cValidInput), Function.identity())); final SplitTree C = st.findLowestSubsetNode(successorsToNodes.keySet()).orElseThrow(IllegalStateException::new); nodeToRefine.setSequence(cValidInput.concat(C.getSequence())); leaves.remove(nodeToRefine); for (final Map.Entry> entry : C.getSuccessors().entrySet()) { final Set wSet = entry.getValue().getPartition(); final Set intersection = new HashSet<>(successorsToNodes.keySet()); intersection.retainAll(wSet); if (!intersection.isEmpty()) { final Set indistinguishableNodes = intersection.stream().map(successorsToNodes::get).collect(Collectors.toSet()); final SplitTree 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 ADSNode extractADS(final MealyMachine automaton, final SplitTree st, final Set currentSet, final Map currentToInitialMapping, final ADSNode predecessor) { if (currentSet.size() == 1) { final S currentNode = currentSet.iterator().next(); assert currentToInitialMapping.containsKey(currentNode); return new ADSLeafNode<>(predecessor, currentToInitialMapping.get(currentNode)); } final SplitTree u = st.findLowestSubsetNode(currentSet).orElseThrow(IllegalStateException::new); final Pair, ADSNode> ads = ADSUtil.buildFromTrace(automaton, u.getSequence(), currentSet.iterator().next()); final ADSNode head = ads.getFirst(); final ADSNode tail = ads.getSecond(); head.setParent(predecessor); for (final Map.Entry> entry : u.getSuccessors().entrySet()) { final O output = entry.getKey(); final SplitTree tree = entry.getValue(); final Set intersection = new HashSet<>(tree.getPartition()); intersection.retainAll(currentSet); if (!intersection.isEmpty()) { final Map nextCurrentToInitialMapping = intersection.stream() .collect(Collectors.toMap(key -> u.getMapping() .get(key), currentToInitialMapping::get)); final Set 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 boolean needsRefinement(final SplitTree node) { return node.getPartition().size() > 1; } private static boolean isValidInput(final MealyMachine automaton, final I input, final Set states) { final Map> 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 Map, SplitTree>>> computeValidities(final MealyMachine automaton, final Alphabet inputs, final Set> R, final Set> pi) { final Map, SplitTree>>> result = new EnumMap<>(Validity.class); final Map stateToPartitionMap = new HashMap<>(); final BiMap> partitionToNodeMap = HashBiMap.create(); int counter = 0; for (SplitTree 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> pendingCs = new HashSet<>(); final Map partitionToClassificationMap = new HashMap<>(); final CompactSimpleGraph implicationGraph = new CompactSimpleGraph<>(partitionToNodeMap.size()); for (int i = 0; i < partitionToNodeMap.size(); i++) { implicationGraph.addIntNode(); } partitionLoop: for (final SplitTree B : R) { // general validity final Map 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 outputs = B.getPartition().stream().map(s -> automaton.getOutput(s, i)).collect(Collectors.toSet()); if (outputs.size() > 1) { result.get(Validity.A_VALID).add(Pair.of(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 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(Pair.of(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(Pair.of(null, B)); } //check remaining potential Cs pendingCLoop: for (final SplitTree pendingC : pendingCs) { final Integer pendingPartition = partitionToNodeMap.inverse().get(pendingC); final Iterator 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> path = ShortestPaths.shortestPath(implicationGraph, pendingPartition, implicationGraph.size(), successor); final List word = path.edgeList().stream().map(CompactEdge::getProperty).collect(Collectors.toList()); result.get(Validity.C_VALID).add(Pair.of(Word.fromList(word), pendingC)); continue pendingCLoop; } } result.get(Validity.INVALID).add(Pair.of(null, pendingC)); } return result; } private enum Validity { A_VALID, B_VALID, C_VALID, INVALID } }