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

net.automatalib.util.minimizer.Minimizer 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.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 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 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 initialNodes) { Iterable 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); } } } }