net.automatalib.util.automata.ads.LeeYannakakis Maven / Gradle / Ivy
Show all versions of automata-util Show documentation
/* 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
}
}