net.automatalib.util.minimizer.Minimizer 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.minimizer;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import net.automatalib.commons.smartcollections.DefaultLinkedList;
import net.automatalib.commons.smartcollections.ElementReference;
import net.automatalib.commons.smartcollections.IntrusiveLinkedList;
import net.automatalib.commons.smartcollections.UnorderedCollection;
import net.automatalib.commons.util.mappings.MutableMapping;
import net.automatalib.graphs.UniversalGraph;
import net.automatalib.util.graphs.traversal.GraphTraversal;
/**
* Automaton minimizer. The automata are accessed via the {@link UniversalGraph} interface, and may be partially
* defined. Note that undefined transitions are preserved, thus, they have no semantics that could be modeled otherwise
* wrt. this algorithm.
*
* The implemented algorithm is described in the paper "Minimizing incomplete automata" by Marie-Pierre Beal and Maxime
* Crochemore.
*
* @param
* state class.
* @param
* transition label class.
*
* @author Malte Isberner
*/
public final class Minimizer {
private static final ThreadLocal> LOCAL_INSTANCE =
ThreadLocal.withInitial(Minimizer::new);
// The following attributes may be reused. Most of them are used
// as local variables in the split() method, but storing them
// as attributes helps to avoid costly re-allocations.
private final DefaultLinkedList> splitters = new DefaultLinkedList<>();
private final IntrusiveLinkedList> letterList = new IntrusiveLinkedList<>();
private final IntrusiveLinkedList> stateList = new IntrusiveLinkedList<>();
private final IntrusiveLinkedList> splitBlocks = new IntrusiveLinkedList<>();
private final IntrusiveLinkedList> newBlocks = new IntrusiveLinkedList<>();
private final IntrusiveLinkedList> finalList = new IntrusiveLinkedList<>();
// These attributes belong to a specific minimization process.
private MutableMapping> stateStorage;
private UnorderedCollection> partition;
private int numBlocks;
/**
* Default constructor.
*/
private Minimizer() {
}
/**
* Minimizes an automaton. The automaton is not minimized directly, instead, a {@link MinimizationResult} structure
* is returned. The automaton is interfaced using an adapter implementing the {@link UniversalGraph} interface.
*
* @param
* state class.
* @param
* transition label class.
* @param graph
* the automaton interface.
*
* @return the result structure.
*/
public static MinimizationResult minimize(UniversalGraph graph) {
return minimize(graph, null);
}
public static MinimizationResult minimize(UniversalGraph graph,
Collection extends S> start) {
Minimizer minimizer = getLocalInstance();
return minimizer.performMinimization(graph, start);
}
/**
* Retrieves the local instance of this minimizer.
*
* The minimizer acts like a singleton of which each thread possesses their own. The minimizer instance returned by
* this method is the one belonging to the calling thread. Therefore, it is not safe to share such an instance
* between two threads.
*
* @param
* state class.
* @param
* transition label class.
*
* @return The minimizers local instance.
*/
@SuppressWarnings("unchecked")
public static Minimizer getLocalInstance() {
return (Minimizer) LOCAL_INSTANCE.get();
}
/**
* Performs the minimization of an automaton.
*
* The automaton is accessed via a {@link UniversalGraph}. The result of the minimization process is effectively a
* partition on the set of states, each element (block) in this partition contains equivalent states that can be
* merged in a minimized automaton.
*
* @param
* edge identifier class.
* @param graph
* the automaton interface.
*
* @return a {@link MinimizationResult} structure, containing the state partition.
*/
public MinimizationResult performMinimization(UniversalGraph graph,
Collection extends S> initialNodes) {
// Initialize the data structures (esp. state records) and build
// the initial partition.
Collection> initialBlocks = initialize(graph, initialNodes);
// Add all blocks from the initial partition as an element
// of the partition, and as a potential splitter.
partition = new UnorderedCollection<>(initialBlocks.size());
///splitters.hintNextCapacity(initialBlocks.size());
for (Block block : initialBlocks) {
if (block == null || block.isEmpty()) {
continue;
}
addToPartition(block);
addToSplitterQueue(block);
numBlocks++;
}
// Split the blocks of the partition, until no splitters
// remain
while (!splitters.isEmpty()) {
Block block = splitters.choose();
removeFromSplitterQueue(block);
split(block);
updateBlocks();
}
// Return the result.
MinimizationResult result = new MinimizationResult<>(stateStorage, partition);
// Ensure the garbage collection isn't hampered
stateStorage = null;
partition = null;
numBlocks = 0;
return result;
}
public MinimizationResult performMinimization(UniversalGraph graph) {
return performMinimization(graph, null);
}
/**
* Builds the initial data structures and performs the initial partitioning.
*/
private Collection> initialize(UniversalGraph graph,
Collection extends S> initialNodes) {
Iterable extends S> origStates;
if (initialNodes == null || initialNodes.isEmpty()) {
origStates = graph.getNodes();
} else {
origStates = GraphTraversal.depthFirstOrder(graph, initialNodes);
}
Map> transitionMap = new HashMap<>();
stateStorage = graph.createStaticNodeMapping();
int numStates = 0;
for (S origState : origStates) {
State state = new State<>(numStates++, origState);
stateStorage.put(origState, state);
stateList.add(state);
}
InitialPartitioning initPartitioning = new HashMapInitialPartitioning<>(graph);
for (State state : stateList) {
S origState = state.getOriginalState();
Block block = initPartitioning.getBlock(origState);
block.addState(state);
for (E edge : graph.getOutgoingEdges(origState)) {
S origTarget = graph.getTarget(edge);
State target = stateStorage.get(origTarget);
L label = graph.getEdgeProperty(edge);
TransitionLabel transition = transitionMap.computeIfAbsent(label, TransitionLabel::new);
Edge edgeObj = new Edge<>(state, target, transition);
state.addOutgoingEdge(edgeObj);
target.addIncomingEdge(edgeObj);
}
}
stateList.quickClear();
return initPartitioning.getInitialBlocks();
}
/**
* Adds a block to the partition.
*/
private void addToPartition(Block block) {
ElementReference ref = partition.referencedAdd(block);
block.setPartitionReference(ref);
}
/**
* Adds a block as a potential splitter.
*/
private void addToSplitterQueue(Block block) {
ElementReference ref = splitters.referencedAdd(block);
block.setSplitterQueueReference(ref);
}
/**
* Removes a block from the splitter queue. This is done when it is split completely and thus no longer existant.
*/
private boolean removeFromSplitterQueue(Block block) {
ElementReference ref = block.getSplitterQueueReference();
if (ref == null) {
return false;
}
splitters.remove(ref);
block.setSplitterQueueReference(null);
return true;
}
/**
* This method realizes the core of the actual minimization, the "split" procedure.
*
* A split separates in each block the states, if any, which have different transition characteristics wrt. a
* specified block, the splitter.
*
* This method does not perform actual splits, but instead it modifies the splitBlocks attribute to containing the
* blocks that could potentially be split. The information on the subsets into which a block is split is contained
* in the sub-blocks of the blocks in the result list.
*
* The actual splitting is performed by the method updateBlocks().
*/
private void split(Block splitter) {
// STEP 1: Collect the states that have outgoing edges
// pointing to states inside the currently considered blocks.
// Also, a list of transition labels occuring on these
// edges is created.
for (State state : splitter.getStates()) {
for (Edge edge : state.getIncoming()) {
TransitionLabel transition = edge.getTransitionLabel();
State newState = edge.getSource();
// Blocks that only contain a single state cannot
// be split any further, and thus are of no
// interest.
if (newState.isSingletonBlock()) {
continue; //continue;
}
if (transition.addToSet(newState)) {
letterList.add(transition);
}
}
}
// STEP 2: Build the signatures. A signature of a state
// is a sequence of the transition labels of its outgoing
// edge that point into the considered split block.
// The iteration over the label list in the outer loop
// guarantees a consistent ordering of the transition labels.
for (TransitionLabel letter : letterList) {
for (State state : letter.getSet()) {
if (state.addToSignature(letter)) {
stateList.add(state);
state.setSplitPoint(false);
}
}
letter.clearSet();
}
letterList.clear();
// STEP 3: Discriminate the states. This is done by weak
// sorting the states. At the end of the weak sort, the finalList
// will contain the states in such an order that only states belonging
// to the same block having the same signature will be contiguous.
// First, initialize the buckets of each block. This is done
// for grouping the states by their corresponding block.
for (State state : stateList) {
Block block = state.getBlock();
if (block.addToBucket(state)) {
splitBlocks.add(block);
}
}
stateList.clear();
for (Block block : splitBlocks) {
stateList.concat(block.getBucket());
}
// Now, the states are ordered according to their signatures
int i = 0;
while (!stateList.isEmpty()) {
for (State state : stateList) {
TransitionLabel letter = state.getSignatureLetter(i);
if (letter == null) {
finalList.pushBack(state);
} else if (letter.addToBucket(state)) {
letterList.add(letter);
}
// If this state was the first to be added to the respective
// bucket, or it differs from the previous entry in the previous
// letter, it is a split point.
if (state.getPrev() == null) {
state.setSplitPoint(true);
} else if (i > 0 && state.getPrev().getSignatureLetter(i - 1) != state.getSignatureLetter(i - 1)) {
state.setSplitPoint(true);
}
}
stateList.clear();
for (TransitionLabel letter : letterList) {
stateList.concat(letter.getBucket());
}
letterList.clear();
i++;
}
Block prevBlock = null;
State prev = null;
for (State state : finalList) {
Block currBlock = state.getBlock();
if (currBlock != prevBlock) {
currBlock.createSubBlock();
prevBlock = currBlock;
} else if (state.isSplitPoint()) {
currBlock.createSubBlock();
}
currBlock.addToSubBlock(state);
if (prev != null) {
prev.reset();
}
prev = state;
}
if (prev != null) {
prev.reset();
}
finalList.clear();
// Step 4 of the algorithm is done in the method
// updateBlocks()
}
/**
* This method performs the actual splitting of blocks, using the sub block information stored in each block
* object.
*/
private void updateBlocks() {
for (Block block : splitBlocks) {
// Ignore blocks that have no elements in their sub blocks.
int inSubBlocks = block.getElementsInSubBlocks();
if (inSubBlocks == 0) {
continue;
}
boolean blockRemains = (inSubBlocks < block.size());
boolean reuseBlock = !blockRemains;
List>> subBlocks = block.getSubBlocks();
// If there is only one sub block which contains all elements of
// the block, then no split needs to be performed.
if (!blockRemains && subBlocks.size() == 1) {
block.clearSubBlocks();
continue;
}
Iterator>> subBlockIt = subBlocks.iterator();
if (reuseBlock) {
UnorderedCollection> first = subBlockIt.next();
block.getStates().swap(first);
updateBlockReferences(block);
}
while (subBlockIt.hasNext()) {
UnorderedCollection> subBlockStates = subBlockIt.next();
if (blockRemains) {
for (State state : subBlockStates) {
block.removeState(state.getBlockReference());
}
}
Block subBlock = new Block<>(numBlocks++, subBlockStates);
updateBlockReferences(subBlock);
newBlocks.add(subBlock);
addToPartition(subBlock);
}
newBlocks.add(block);
block.clearSubBlocks();
// If the split block previously was in the queue, add all newly
// created blocks to the queue. Otherwise, it's enough to add
// all but the largest
if (removeFromSplitterQueue(block)) {
addAllToSplitterQueue(newBlocks);
} else {
addAllButLargest(newBlocks);
}
newBlocks.clear();
}
splitBlocks.clear();
}
/**
* Sets the blockReference-attribute of each state in the collection to the corresponding ElementReference of the
* collection.
*/
private static void updateBlockReferences(Block block) {
UnorderedCollection> states = block.getStates();
for (ElementReference ref : states.references()) {
State state = states.get(ref);
state.setBlockReference(ref);
state.setBlock(block);
}
}
/**
* Adds all but the largest block of a given collection to the splitter queue.
*/
private void addAllToSplitterQueue(Collection> blocks) {
for (Block block : blocks) {
addToSplitterQueue(block);
}
}
private void addAllButLargest(Collection> blocks) {
Block largest = null;
for (Block block : blocks) {
if (largest == null) {
largest = block;
} else if (block.size() > largest.size()) {
addToSplitterQueue(largest);
largest = block;
} else {
addToSplitterQueue(block);
}
}
}
}