/*
* Grapht, an open source dependency injector.
* Copyright 2014-2015 various contributors (see CONTRIBUTORS.txt)
* Copyright 2010-2014 Regents of the University of Minnesota
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.grouplens.grapht.graph;
import com.google.common.base.Functions;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Merges graphs to remove redundant nodes. This takes graphs and merges them, pruning redundant
* nodes within the graphs and between graphs previously merged. It remembers graphs it has
* previously seen to allow nodes to be reused across multiple graphs.
*
* @param <V> The vertex type of graphs to merge.
* @param <E> The edge type of graphs to merge.
* @since 0.7
* @author <a href="http://grouplens.org">GroupLens Research</a>
*/
public class MergePool<V,E> {
// TODO Allow arbitrary equivalence relations over graph nodes so this class is less specialized.
private static final Logger logger = LoggerFactory.getLogger(MergePool.class);
private final Set<DAGNode<V,E>> pool;
private MergePool() {
pool = Sets.newLinkedHashSet();
}
/**
* Create a merge pool that checks node labels for equality.
*
* @param <V> The node label type.
* @param <E> The edge label type.
* @return A new merge pool.
*/
public static <V,E> MergePool<V,E> create() {
return new MergePool<V, E>();
}
/**
* Merge and simplify a graph. This will coalesce redundant nodes (equivalent labels and
* outgoing edge destinations), and will prefer to use nodes from graphs seen previously.
* This allows deduplication across multiple graphs.
*
* <p><strong>Noteo:</strong> edge labels are ignored for the purpose of merging.</p>
*
* @param graph The graph to simplify.
* @return The new simplified, merged graph.
*/
public DAGNode<V,E> merge(DAGNode<V, E> graph) {
List<DAGNode<V, E>> sorted = graph.getSortedNodes();
Map<Pair<V,Set<DAGNode<V,E>>>, DAGNode<V,E>> nodeTable = Maps.newHashMap();
for (DAGNode<V, E> node: pool) {
Pair<V, Set<DAGNode<V, E>>> key =
Pair.of(node.getLabel(), node.getAdjacentNodes());
assert !nodeTable.containsKey(key);
nodeTable.put(key, node);
}
// We want to map nodes to their previous merged versions
Map<DAGNode<V,E>, DAGNode<V,E>> mergedMap = Maps.newHashMap();
// Now start processing nodes
for (DAGNode<V, E> toMerge: sorted) {
V sat = toMerge.getLabel();
// Resolve the merged neighbors of this node. They have already been
// merged, since we are going in topological order.
Set<DAGNode<V, E>> neighbors =
FluentIterable.from(toMerge.getOutgoingEdges())
.transform(DAGEdge.<V,E>extractTail())
.transform(Functions.forMap(mergedMap))
.toSet();
// See if we have already created an equivalent to this node
DAGNode<V, E> newNode = nodeTable.get(Pair.of(sat, neighbors));
if (newNode == null) {
// No, let's start building one
DAGNodeBuilder<V,E> bld = DAGNode.newBuilder();
boolean changed = false;
bld.setLabel(sat);
logger.debug("Adding new node to merged graph for satisfaction: {}", sat);
for (DAGEdge<V, E> edge: toMerge.getOutgoingEdges()) {
// create a new edge with the merged tail and same label
DAGNode<V, E> filtered = mergedMap.get(edge.getTail());
bld.addEdge(filtered, edge.getLabel());
// have we made a change to this node?
changed |= !filtered.equals(edge.getTail());
}
if (changed) {
// one of the node's neighbors has been replaced with merged version
// so use the new node
newNode = bld.build();
} else {
// no edges were changed, leave the node unmodified
newNode = toMerge;
}
nodeTable.put(Pair.of(sat, neighbors), newNode);
} else {
logger.debug("Node already in merged graph for satisfaction: {}", toMerge.getLabel());
}
// update merge map so future equivalent nodes get replaced with this one
mergedMap.put(toMerge, newNode);
}
// now let's find our return value - what did we merge the graph root to?
DAGNode<V, E> newRoot = mergedMap.get(graph);
// remember all its nodes for future merge operations
pool.addAll(newRoot.getReachableNodes());
// and we're done
return newRoot;
}
}