/** * Copyright (C) 2012-2013 Selventa, Inc. * * This file is part of the OpenBEL Framework. * * 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 3 of the License, or * (at your option) any later version. * * The OpenBEL Framework 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 Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with the OpenBEL Framework. If not, see <http://www.gnu.org/licenses/>. * * Additional Terms under LGPL v3: * * This license does not authorize you and you are prohibited from using the * name, trademarks, service marks, logos or similar indicia of Selventa, Inc., * or, in the discretion of other licensors or authors of the program, the * name, trademarks, service marks, logos or similar indicia of such authors or * licensors, in any marketing or advertising materials relating to your * distribution of the program or any covered product. This restriction does * not waive or limit your obligation to keep intact all copyright notices set * forth in the program as delivered to you. * * If you distribute the program in whole or in part, or any modified version * of the program, and you assume contractual liability to the recipient with * respect to the program or modified version, then you will indemnify the * authors and licensors of the program for any liabilities that these * contractual assumptions directly impose on those licensors and authors. */ package org.openbel.framework.api; import static org.openbel.framework.api.EdgeDirectionType.BOTH; import static org.openbel.framework.common.BELUtilities.sizedHashSet; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.Stack; import org.openbel.framework.api.Kam.KamEdge; import org.openbel.framework.api.Kam.KamNode; import org.openbel.framework.common.InvalidArgument; /** * Implements a basic {@link PathFinder} implementation with a max depth * constraint. * * <p> * The max search depth constraint default is defined at * {@link BasicPathFinder#DEFAULT_MAX_SEARCH_DEPTH}. To override this * constraints create with {@link BasicPathFinder#BasicPathFinder(int)}. * </p> * * @author Anthony Bargnesi {@code <abargnesi@selventa.com>} */ public class BasicPathFinder implements PathFinder { /** * Defines the default max search depth constraint to a depth of {@value}. */ public final static int DEFAULT_MAX_SEARCH_DEPTH = 4; /** * Holds the KAM network to path find over. */ private final Kam kam; /** * Holds the max search depth to use when path finding. */ private final int maxSearchDepth; /** * Constructs the path finder using the default max search depth defined * in {@link BasicPathFinder#DEFAULT_MAX_SEARCH_DEPTH}. * * @param kam {@link Kam}, the KAM network to path find over, which cannot * be null */ public BasicPathFinder(final Kam kam) { this.kam = kam; this.maxSearchDepth = DEFAULT_MAX_SEARCH_DEPTH; } /** * Constructs the path finder using the max search depth constraint defined * in the value <tt>maxSearchDepth</tt>. This value must be greater than * zero. * * @param kam {@link Kam}, the KAM network to path find over, which cannot * be null * @param maxSearchDepth <tt>int</tt> the max search depth constraint to * use when path finding, which must be greater than zero */ public BasicPathFinder(final Kam kam, final int maxSearchDepth) { this.kam = kam; this.maxSearchDepth = maxSearchDepth; } /** * {@inheritDoc} */ @Override public SimplePath[] findPaths(KamNode source, KamNode target) { if (source == null) { throw new InvalidArgument("Source kam node cannot be null."); } if (target == null) { throw new InvalidArgument("Target kam node cannot be null."); } if (!kam.contains(source)) { throw new InvalidArgument("Source does not exist in KAM."); } if (!kam.contains(target)) { throw new InvalidArgument("Target does not exist in KAM."); } final Set<KamNode> targets = sizedHashSet(1); targets.add(target); List<SimplePath> pathsFound = runDepthFirstSearch(kam, source, targets); return pathsFound.toArray(new SimplePath[pathsFound.size()]); } /** * {@inheritDoc} */ @Override public SimplePath[] findPaths(KamNode[] sources, KamNode[] targets) { if (sources == null || sources.length == 0) { throw new InvalidArgument( "Source kam nodes cannot be null or empty."); } if (targets == null || targets.length == 0) { throw new InvalidArgument( "Target kam nodes cannot be null or empty."); } for (final KamNode source : sources) { if (!kam.contains(source)) { throw new InvalidArgument("Source does not exist in KAM."); } } final Set<KamNode> targetSet = new HashSet<KamNode>(targets.length); for (final KamNode target : targets) { if (!kam.contains(target)) { throw new InvalidArgument("Target does not exist in KAM."); } targetSet.add(target); } final List<SimplePath> pathsFound = new ArrayList<SimplePath>(); for (final KamNode source : sources) { if (!kam.contains(source)) { throw new InvalidArgument("Source does not exist in KAM."); } pathsFound.addAll(runDepthFirstSearch(kam, source, targetSet)); } return pathsFound.toArray(new SimplePath[pathsFound.size()]); } /** * {@inheritDoc} */ @Override public SimplePath[] interconnect(KamNode[] sources) { if (sources == null || sources.length < 2) { throw new InvalidArgument( "Source kam nodes cannot be null and must contain at least two source nodes."); } // build out target set, check that each node is in the KAM final Set<KamNode> targetSet = new HashSet<KamNode>(sources.length); for (int i = 0; i < sources.length; i++) { final KamNode source = sources[i]; if (!kam.contains(source)) { throw new InvalidArgument("Source does not exist in KAM."); } targetSet.add(source); } final List<SimplePath> pathsFound = new ArrayList<SimplePath>(); for (final KamNode source : sources) { // remove source from target before search to prevent search the same // paths twice in the bidirectional search targetSet.remove(source); pathsFound.addAll(runDepthFirstSearch(kam, source, targetSet)); } return pathsFound.toArray(new SimplePath[pathsFound.size()]); } /** * {@inheritDoc} */ @Override public SimplePath[] scan(KamNode source) { if (source == null) { throw new InvalidArgument("Source kam node cannot be null."); } if (!kam.contains(source)) { throw new InvalidArgument("Source does not exist in KAM."); } final List<SimplePath> pathsFound = runDepthFirstScan(kam, source); return pathsFound.toArray(new SimplePath[pathsFound.size()]); } /** * {@inheritDoc} */ @Override public SimplePath[] scan(KamNode[] sources) { if (sources == null || sources.length == 0) { throw new InvalidArgument( "Source kam nodes cannot be null or empty."); } for (final KamNode source : sources) { if (!kam.contains(source)) { throw new InvalidArgument("Source does not exist in KAM."); } } final List<SimplePath> pathsFound = new ArrayList<SimplePath>(); for (final KamNode source : sources) { pathsFound.addAll(runDepthFirstScan(kam, source)); } return pathsFound.toArray(new SimplePath[pathsFound.size()]); } /** * Initializes and executes the depth-first search from {@link KamNode} * source to a {@link Set} of {@link KamNode} targets. * * @param kam {@link Kam}, the kam to traverse * @param source {@link KamNode}, the source kam node * @param targets {@link Set} of {@link KamNode}, the target kam nodes * @return the resulting paths from this depth-first search */ private List<SimplePath> runDepthFirstSearch(final Kam kam, final KamNode source, final Set<KamNode> targets) { final SetStack<KamNode> nodeStack = new SetStack<KamNode>(); nodeStack.add(source); final SetStack<KamEdge> edgeStack = new SetStack<KamEdge>(); final List<SimplePath> pathResults = new ArrayList<SimplePath>(); int initialDepth = 0; runDepthFirstSearch(kam, source, source, targets, initialDepth, nodeStack, edgeStack, pathResults); return pathResults; } /** * Initializes and executes a depth-first scan from {@link KamNode} source * given a max search depth ({@link BasicPathFinder#maxSearchDepth}). * * @param kam {@link Kam}, the kam to traverse * @param source {@link KamNode}, the source kam node * @return the resulting paths from this depth-first scan */ private List<SimplePath> runDepthFirstScan(final Kam kam, final KamNode source) { final SetStack<KamEdge> edgeStack = new SetStack<KamEdge>(); final SetStack<KamNode> nodeStack = new SetStack<KamNode>(); final List<SimplePath> pathResults = new ArrayList<SimplePath>(); // push on start node nodeStack.push(source); int initialDepth = 0; runDepthFirstScan(kam, source, source, initialDepth, nodeStack, edgeStack, pathResults); return pathResults; } /** * Runs a recursive depth-first search from a {@link KamNode} in search of * the <tt>target</tt> node. When a {@link SimplePath} is found to the * <tt>target</tt> the {@link Stack} of {@link KamEdge} is collected and * the algorithm continues.<br/><br/> * <p> * This depth-first search exhaustively walks the entire {@link Kam} * and finds all paths from <tt>source</tt> to <tt>target</tt>. * </p> * * @param kam {@link Kam}, the kam to traverse * @param cnode {@link KamNode} the current node to evaluate * @param source {@link KamNode} the source to search from * @param targets {@link Set} of {@link KamNode}, the targets to search to * @param nodeStack {@link Stack} of {@link KamNode} that holds the nodes * on the current path from the <tt>source</tt> * @param edgeStack {@link Stack} of {@link KamEdge} that holds the edges * on the current path from the <tt>source</tt> * @param pathResults the resulting paths from source to targets */ private void runDepthFirstSearch(final Kam kam, final KamNode cnode, final KamNode source, final Set<KamNode> targets, int depth, final SetStack<KamNode> nodeStack, final SetStack<KamEdge> edgeStack, final List<SimplePath> pathResults) { depth += 1; if (depth > maxSearchDepth) { return; } // get adjacent edges final Set<KamEdge> edges = kam.getAdjacentEdges(cnode, BOTH); for (final KamEdge edge : edges) { if (pushEdge(edge, nodeStack, edgeStack)) { final KamNode edgeOppositeNode = nodeStack.peek(); // we have found a path from source to target if (targets.contains(edgeOppositeNode)) { final SimplePath newPath = new SimplePath(kam, source, nodeStack.peek(), edgeStack.toStack()); pathResults.add(newPath); } else { runDepthFirstSearch(kam, edgeOppositeNode, source, targets, depth, nodeStack, edgeStack, pathResults); } nodeStack.pop(); edgeStack.pop(); } } } /** * Runs a recursive depth-first scan from a {@link KamNode} source until a * max search depth ({@link BasicPathFinder#maxSearchDepth}) is reached. * When the max search depth is reached a {@link SimplePath} is added, * containing the {@link Stack} of {@link KamEdge}, and the algorithm continues. * * @param kam {@link Kam}, the kam to traverse * @param cnode {@link KamNode}, the current node to evaluate * @param source {@link KamNode}, the node to search from * @param depth <tt>int</tt>, the current depth of this scan recursion * @param nodeStack {@link Stack} of {@link KamNode}, the nodes on the * current scan from the <tt>source</tt> * @param edgeStack {@link Stack} of {@link KamEdge}, the edges on the * current scan from the <tt>source</tt> * @param pathResults the resulting paths scanned from source */ private void runDepthFirstScan(final Kam kam, final KamNode cnode, final KamNode source, int depth, final SetStack<KamNode> nodeStack, final SetStack<KamEdge> edgeStack, final List<SimplePath> pathResults) { depth += 1; final Set<KamEdge> edges = kam.getAdjacentEdges(cnode, BOTH); for (final KamEdge edge : edges) { if (pushEdge(edge, nodeStack, edgeStack)) { if (depth == maxSearchDepth) { final SimplePath newPath = new SimplePath(kam, source, nodeStack.peek(), edgeStack.toStack()); pathResults.add(newPath); } else { // continue depth first scan runDepthFirstScan(kam, nodeStack.peek(), source, depth, nodeStack, edgeStack, pathResults); } edgeStack.pop(); nodeStack.pop(); } else if (endOfBranch(edgeStack, edge, edges.size())) { final SimplePath newPath = new SimplePath(kam, source, nodeStack.peek(), edgeStack.toStack()); pathResults.add(newPath); } } } /** * Determines whether the end of a branch was found. This can indicate * that a path should be captures up to the leaf node. * * @param edgeStack {@link Stack} of {@link KamEdge} that holds the edges * on the current path * @param edge {@link KamEdge}, the edge to evaluate * @param edgeCount <tt>int</tt>, the number of adjacent edges to the * last visited {@link KamNode} * @return <tt>true</tt> if this edge marks the end of a branch, * <tt>false</tt> otherwise if it does not */ private boolean endOfBranch(final SetStack<KamEdge> edgeStack, KamEdge edge, int edgeCount) { if (edgeStack.contains(edge) && edgeCount == 1) { return true; } return false; } /** * Determines whether the edge can be traversed. This implementation will * check if the {@link KamNode} or {@link KamEdge} have been visited to * ensure there are no cycles in the resulting paths. * <p> * If the edge can be travered it will be placed on the edge {@link Stack}, * and the edge's unvisited node will be placed on the node {@link Stack}. * </p> * * @param edge {@link KamEdge}, the kam edge to evaluate * @param nodeStack {@link Stack} of {@link KamNode}, the nodes on the * current scan from the <tt>source</tt> * @param edgeStack {@link Stack} of {@link KamEdge}, the edges on the * current scan from the <tt>source</tt> * @return <tt>true</tt> if the edge will be traversed, <tt>false</tt> if * not */ private boolean pushEdge(final KamEdge edge, final SetStack<KamNode> nodeStack, final SetStack<KamEdge> edgeStack) { if (edgeStack.contains(edge)) { return false; } final KamNode currentNode = nodeStack.peek(); final KamNode edgeOppositeNode = (edge.getSourceNode() == currentNode ? edge .getTargetNode() : edge.getSourceNode()); if (nodeStack.contains(edgeOppositeNode)) { return false; } nodeStack.push(edgeOppositeNode); edgeStack.push(edge); return true; } /** * Data Structure with both (subsets of) Set and Stack APIs that * allows for O(1) addition and retrieval as well as O(1) contains. This leads * to an approximate 20% performance gain in tested pathfind operations. * <br> * The unique {@link KamElement} ID is used as the discriminator in an attempt * to avoid hashCode collisions. In debugging the full Kam however, this did not * lead to significant performance improvements. * * @author Steve Ungerer */ private final static class SetStack<T extends KamElement> { private Stack<T> elements = new Stack<T>(); private HashSet<Integer> set = new HashSet<Integer>(); public boolean contains(T o) { return set.contains(o.getId()); } public void add(T obj) { elements.add(obj); set.add(obj.getId()); } public void push(T obj) { elements.push(obj); set.add(obj.getId()); } public T peek() { return elements.peek(); } public T pop() { T obj = elements.pop(); set.remove(obj.getId()); return obj; } public Stack<T> toStack() { return elements; } } }