diff options
Diffstat (limited to 'tensorflow/tensorboard/components/tf-graph-common/lib/render.ts')
-rw-r--r-- | tensorflow/tensorboard/components/tf-graph-common/lib/render.ts | 1360 |
1 files changed, 1360 insertions, 0 deletions
diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/render.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/render.ts new file mode 100644 index 0000000000..363f006fd5 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/render.ts @@ -0,0 +1,1360 @@ +/// <reference path="graph.ts" /> +/// <reference path="hierarchy.ts" /> + +/** + * Package for the Render Hierarchy for TensorFlow graph. + */ + +module tf.graph.render { + +/** + * Color parameters for node encoding. + * @type {Object} + */ +export let MetanodeColors = { + SATURATION: 0.6, + LIGHTNESS: 0.85, + /** + * Neutral color to use when the node is expanded (used when coloring by + * compute time, memory and device). + */ + EXPANDED_COLOR: "#f0f0f0", + /** + * Standard hue values for node color palette. + */ + HUES: [220, 100, 180, 40, 20, 340, 260, 300, 140, 60], + STRUCTURE_PALETTE: function(id: number, lightened? : boolean) { + // The code below is a flexible way to computationally create a set + // of colors that go well together. + let hues = MetanodeColors.HUES; + let n = hues.length; + let hue = hues[id % n]; + let m = Math.sin(hue * Math.PI / 360); + let sat = lightened ? 30 : 90 - 60 * m; + let light = lightened ? 95 : 80; + return d3.hsl(hue, .01 * sat, .01 * light).toString(); + }, + DEVICE_PALETTE: function (index: number): string { + return MetanodeColors.STRUCTURE_PALETTE(index); + }, + UNKNOWN: "#eee", + GRADIENT_OUTLINE: "#888" +}; + +/** + * Parameters that affect how the graph is rendered on the screen. + */ +interface RenderGraphParams { + /** + * Whether to extract high degree nodes from the core part of the graph. + */ + enableExtraction: boolean; + /** + * Maximum in-degree that a node can have without being considered as + * high in-degree node. + */ + maxInDegree: number; + /** + * Maximum out-degree that a node can have without being considered as + * high out-degree node. + */ + maxOutDegree: number; + /** + * Maximum number of control edges a node can have before they aren't + * displayed. + */ + maxControlDegree: number; + /** + * Types patterns for predefined out-extract nodes, which are + * sink-like nodes that will be extracted from the main graph. + */ + outExtractTypes: string[]; + + /** + * Types patterns for predefined in-extract nodes, which are + * source-like nodes that will be extracted from the main graph. + */ + inExtractTypes: string[]; + + /** + * When removing edges from a high degree node, remove all of its edges if + * detachAllEdgesForHighDegree is true. Otherwise remove all in-edges if + * the node has high in-degree, or all out-edges if the node has high + * out-degree. + */ + detachAllEdgesForHighDegree: boolean; + + /** + * After extracting high in/out degree nodes and predefined + * source-like/sink-like, extract isolated nodes to the side + * if this extractIsolatedNodesWithAnnotationsOnOneSide is true. + */ + extractIsolatedNodesWithAnnotationsOnOneSide: boolean; + + /** + * Whether to add bridge nodes and edges to the core when building the + * subhierarchy of an expanded metanode. See buildSubhierarchy(). + */ + enableBridgegraph: boolean; + + /** + * 2 colors, for the minimum and maximum value respectively, whenever we + * have a gradient scale. + */ + minMaxColors: string[]; + + /** + * Maximum number of annotations to be displayed on a node. + */ + maxAnnotations: number; +} + +/** + * Stores the rendering information, such as x and y coordinates, + * for each node in the graph. + */ +export class RenderGraphInformation { + private hierarchy: hierarchy.Hierarchy; + private index: {[nodeName: string]: RenderNodeInformation}; + private params: RenderGraphParams; + private deviceColorMap: d3.scale.Ordinal<string, string>; + private memoryUsageScale: d3.scale.Linear<string, string>; + private computeTimeScale: d3.scale.Linear<string, string>; + // Since the rendering information for each node is constructed lazily, + // upon node's expansion by the user, we keep a map between the node's name and + // whether the rendering information was already constructed for that node. + private hasSubhierarchy: {[nodeName: string]: boolean}; + root: RenderGroupNodeInformation; + + constructor(hierarchy: hierarchy.Hierarchy, params: RenderGraphParams) { + this.hierarchy = hierarchy; + this.index = {}; + this.deviceColorMap = d3.scale.ordinal<string>() + .domain(hierarchy.devices) + .range(_.map(d3.range(hierarchy.devices.length), + MetanodeColors.DEVICE_PALETTE)); + + let topLevelGraph = hierarchy.root.metagraph; + // Find the maximum and minimum memory usage. + let memoryExtent = d3.extent(topLevelGraph.nodes(), + (nodeName, index) => { + let node = topLevelGraph.node(nodeName); + // Some ops don't have stats at all. + if (node.stats != null) { + return node.stats.totalBytes; + } + }); + this.memoryUsageScale = d3.scale.linear<string, string>() + .domain(memoryExtent) + .range(params.minMaxColors); + + // Find also the minimum and maximum compute time. + let computeTimeExtent = d3.extent(topLevelGraph.nodes(), (nodeName, index) => { + let node = topLevelGraph.node(nodeName); + // Some ops don't have stats at all. + if (node.stats != null) { + return node.stats.totalMicros; + } + }); + this.computeTimeScale = d3.scale.linear<string, string>() + .domain(computeTimeExtent) + .range(params.minMaxColors); + + // Maps node name to whether the rendering hierarchy was already constructed. + this.hasSubhierarchy = {}; + this.params = params; + this.root = new RenderGroupNodeInformation(hierarchy.root); + this.index[hierarchy.root.name] = this.root; + this.buildSubhierarchy(hierarchy.root.name); + this.root.expanded = true; + } + + getRenderNodeByName(nodeName: string): RenderNodeInformation { + return this.index[nodeName]; + } + + /** + * Return the nearest ancestor node, including itself, that is visible + * in the visualization. This method is used so that we can select + * (highlight) a node that isn't drawn yet, by selecting (highlighting) + * its nearest ancestor that has been drawn. + */ + getNearestVisibleAncestor(name: string): string { + let path = getHierarchicalPath(name); + for (let i = 0; i < path.length; i++) { + let nodeName = path[i]; + // Op nodes have expanded set to false by default. + if (!this.getRenderNodeByName(nodeName).expanded) { + return nodeName; + } + } + // Fallthrough. If everything was expanded return the node. + return name; + } + + // TODO(jimbo): Delete this an any code it touches (all deprecated). + setDepth(depth: number): void { + setGroupNodeDepth(this.root, +depth); + } + + buildSubhierarchy(nodeName: string): void { + // Terminate if the rendering hierarchy was already constructed + // for this node. + if (nodeName in this.hasSubhierarchy) { + return; + } + + let renderNodeInfo = this.index[nodeName]; + + // If it is not a meta node or a series node, don't do anything. + if (renderNodeInfo.node.type !== NodeType.META && + renderNodeInfo.node.type !== NodeType.SERIES) { + return; + } + + // At this point we know the rendering information is about a group node. + let renderGroupNodeInfo = <RenderGroupNodeInformation> renderNodeInfo; + let metagraph = renderGroupNodeInfo.node.metagraph; + let coreGraph = renderGroupNodeInfo.coreGraph; + + // Create render nodes to represent each child from the metagraph. Although + // these will initially be added to the coreGraph, they may later be + // extracted. Also, due to extraction, the coreGraph may contain disjoint + // groups between which there is no visible path (other than annotations). + _.each(metagraph.nodes(), childName => { + + let childNode = metagraph.node(childName); + let childRenderInfo = childNode.isGroupNode ? + new RenderGroupNodeInformation(<GroupNode>childNode) : + new RenderNodeInformation(childNode); + this.index[childName] = childRenderInfo; + coreGraph.setNode(childName, childRenderInfo); + + if (childRenderInfo.node.stats != null) { + childRenderInfo.memoryColor = + this.memoryUsageScale(childRenderInfo.node.stats.totalBytes); + childRenderInfo.computeTimeColor = + this.computeTimeScale(childRenderInfo.node.stats.totalMicros); + } + + if (!childNode.isGroupNode) { + _.each((<OpNode>childNode).inEmbeddings, embedding => { + let renderMetaedgeInfo = new RenderMetaedgeInformation(null); + addInAnnotation(childRenderInfo, embedding, null, renderMetaedgeInfo, + AnnotationType.CONSTANT, this.params); + this.index[embedding.name] = new RenderNodeInformation(embedding); + }); + _.each((<OpNode>childNode).outEmbeddings, embedding => { + let renderMetaedgeInfo = new RenderMetaedgeInformation(null); + addOutAnnotation(childRenderInfo, embedding, null, renderMetaedgeInfo, + AnnotationType.SUMMARY, this.params); + this.index[embedding.name] = new RenderNodeInformation(embedding); + }); + let device = (<OpNode>childRenderInfo.node).device; + if (device != null) { + childRenderInfo.deviceColors = [{ + color: this.deviceColorMap(device), + proportion: 1.0 + }]; + } + } else { + // Make a list of tuples (device, proportion), where proportion + // is the fraction of op nodes that have that device. + let pairs = _.pairs((<GroupNode> childNode).deviceHistogram); + if (pairs.length > 0) { + // Compute the total # of devices. + let numDevices = _.sum(pairs, _.last); + childRenderInfo.deviceColors = _.map(pairs, pair => { + return { + color: this.deviceColorMap(pair[0]), + // Normalize to a proportion of total # of devices. + proportion: pair[1] / numDevices + }; + }); + } + } + }); + + // Add render metaedge info for edges in the metagraph. + _.each(metagraph.edges(), edgeObj => { + let metaedge = metagraph.edge(edgeObj); + let renderMetaedgeInfo = new RenderMetaedgeInformation(metaedge); + coreGraph.setEdge(edgeObj.v, edgeObj.w, renderMetaedgeInfo); + }); + + if (this.params.enableExtraction && + renderGroupNodeInfo.node.type === NodeType.META) { + extractHighDegrees(renderGroupNodeInfo, this.params); + } + + // Record that we constructed the rendering hierarchy for this node, so we + // don't construct it another time. + this.hasSubhierarchy[nodeName] = true; + + // Look up the parent node's render information and short circuit if none. + let parentNode = renderGroupNodeInfo.node.parentNode; + if (!parentNode) { + return; + } + let parentNodeInfo = + <RenderGroupNodeInformation> this.index[parentNode.name]; + + // Utility function for computing the name of a bridge node. + let getBridgeNodeName = (inbound, ...rest) => + rest.concat([inbound ? "IN" : "OUT"]).join("~~"); + + // Build out the bridgegraph. + let bridgegraph = this.hierarchy.getBridgegraph(nodeName); + + // Look for popular nodes so we can make annotations instead of paths. + let otherCounts = { + // Counts of edges coming INTO other nodes by name (outgoing from self). + in: <{[nodeName: string]: number}> {}, + // Counts of edges going OUT from other nodes by name (coming into self). + out: <{[nodeName: string]: number}> {}, + // Counts of all control edges involving other nodes by name. + control: <{[nodeName: string]: number}> {}, + }; + _.each(bridgegraph.edges(), e => { + // An edge is inbound if its destination node is in the metagraph. + let inbound = !!metagraph.node(e.w); + let otherName = inbound ? e.v : e.w; + let metaedge = bridgegraph.edge(e); + if (!metaedge.numRegularEdges) { + otherCounts.control[otherName] = + (otherCounts.control[otherName] || 0) + 1; + } else if (inbound) { + otherCounts.out[otherName] = (otherCounts.out[otherName] || 0) + 1; + } else { + otherCounts.in[otherName] = (otherCounts.in[otherName] || 0) + 1; + } + }); + + // Add annotations and edges for bridgegraph relationships. + let hierarchyNodeMap = this.hierarchy.getNodeMap(); + _.each(bridgegraph.edges(), bridgeEdgeObj => { + let bridgeMetaedge = bridgegraph.edge(bridgeEdgeObj); + + // Determine whether this bridge edge is incoming by checking the + // metagraph for a node that matches the destination end. + let inbound = !!metagraph.node(bridgeEdgeObj.w); + + // Based on the direction of the edge, one endpoint will be an immediate + // child of this renderNodeInfo, and the other endpoint will be a sibling + // of the parent (or an ancestor further up). + let [childName, otherName] = + inbound ? + [bridgeEdgeObj.w, bridgeEdgeObj.v] : + [bridgeEdgeObj.v, bridgeEdgeObj.w]; + + let childRenderInfo = this.index[childName]; + let otherRenderInfo = this.index[otherName]; + let otherNode = + otherRenderInfo ? + otherRenderInfo.node : + hierarchyNodeMap[otherName]; + + // Determine whether this edge is a control edge between nodes where + // either node is high-degree with respect to control edges. This will + // be a signal to show it as an annotation instead of a bridge edge. + let isHighDegreeControlEdge = !bridgeMetaedge.numRegularEdges && + otherCounts.control[otherName] > this.params.maxControlDegree; + + let [annotations, childAnnotations] = + inbound ? + [renderNodeInfo.inAnnotations, childRenderInfo.inAnnotations] : + [renderNodeInfo.outAnnotations, childRenderInfo.outAnnotations]; + + let isOtherHighDegree = + inbound ? + otherCounts.out[otherName] > this.params.maxOutDegree : + otherCounts.in[otherName] > this.params.maxInDegree; + + // The adjoining render metaedge info from the parent's coreGraph, if any. + // It will either be a Metaedge involving this node directly, if it + // previously came from a metagraph, or it'll be a Metaedge involving + // a previously created bridge node standing in for the other node. + let adjoiningMetaedge = null; + + // We can only hope to render a bridge path if: + // - bridgegraph paths are enabled, + // - the other node is not too high-degree, + // - the child is in the core (not extracted for being high-degree), and + // - there's a path (in the traversal sense) between child and other. + let canDrawBridgePath = false; + if (this.params.enableBridgegraph && + !isOtherHighDegree && + !isHighDegreeControlEdge && + childRenderInfo.isInCore()) { + + // Utility function for finding an adjoining metaedge. + let findAdjoiningMetaedge = targetName => { + let adjoiningEdgeObj: graphlib.EdgeObject = + inbound ? + { v: targetName, w: nodeName } : + { v: nodeName, w: targetName }; + return <RenderMetaedgeInformation> + parentNodeInfo.coreGraph.edge(adjoiningEdgeObj); + }; + + adjoiningMetaedge = findAdjoiningMetaedge(otherName); + if (!adjoiningMetaedge) { + adjoiningMetaedge = findAdjoiningMetaedge( + getBridgeNodeName(inbound, otherName, parentNode.name)); + } + + canDrawBridgePath = !!adjoiningMetaedge; + } + + // Although dataflow edges are acyclic, control dependency edges may + // actually point "backwards" in the graph. If this bridgeMetaedge is + // a control dependency, we need to determine whether it's backwards + // pointing so that we render it appropriately. + // + // For instance, say we're rendering a graph with nodes named A/B and Z/Y, + // and we're currently rendering the bridgegraph for A. Further, let's say + // that there was an original BaseEdge from A/B->Z/Y and a CONTROL EDGE + // from Z/Y=>A/B. + // + // +----------------+ + // | A | + // | +-----+ | +------+ + // | | B |>----->|>------->| Z | + // | | | | | | + // | | | * | | | + // | | |<=====<|<=======<| | + // | +-----+ | +------+ + // +----------------+ + // + // When we render the subhierarchy for Metanode A, we'll come across a + // control-only Metaedge in the bridgegraph from Z=>A/B (*). The question + // is whether this edge is backwards. + // + // To answer that question, we follow the chain of adjoining metaedges + // until we reach the topmost one. In this case, that's the control-only + // Metaedge Z=>A in the ROOT's metagraph. We determine that this edge + // is backwards by looking at the topological ordering of ROOT's metagraph + // (which ignores control edges) and seeing that Z comes AFTER A. + // + // The property of being backwards is independent of whether the edge + // is inbound or outbound. In the preceeding example, if we were building + // the subhierarchy for Z, we'd find bridge edge Z/Y=>A, walk to its + // topmost adjoining metaedge Z=>A and discover that it's backwards. + let backwards = false; + if (adjoiningMetaedge && !bridgeMetaedge.numRegularEdges) { + // Find the top-most adjoining render metaedge information, and the + // GroupNode whose metagraph must contain the associated metaedge. + let topAdjoiningMetaedge = adjoiningMetaedge; + let topGroupNode = parentNodeInfo.node; + while (topAdjoiningMetaedge.adjoiningMetaedge) { + topAdjoiningMetaedge = topAdjoiningMetaedge.adjoiningMetaedge; + topGroupNode = <GroupNode>topGroupNode.parentNode; + } + + // Check against the topological ordering for the top node. The current + // bridge metaedge we're evaluating is backwards if its source comes + // after its destination. + let ordering = this.hierarchy.getTopologicalOrdering(topGroupNode.name); + let e = topAdjoiningMetaedge.metaedge; + backwards = ordering[e.v] > ordering[e.w]; + } + + // Render backwards control edges as annotations. + canDrawBridgePath = canDrawBridgePath && !backwards; + + // If we can't make a bridge path for any reason, then we add an + // annotation instead. + if (!canDrawBridgePath) { + childAnnotations.push(new Annotation( + otherNode, + otherRenderInfo, + new RenderMetaedgeInformation(bridgeMetaedge), + AnnotationType.SHORTCUT, + inbound), this.params); + return; + } + + // At this point, all conditions have been met for drawing a bridge path. + + // Find or create the IN/OUT node representing otherNode. + let bridgeContainerName = getBridgeNodeName(inbound, nodeName); + let bridgeNodeName = getBridgeNodeName(inbound, otherName, nodeName); + let bridgeNodeRenderInfo = coreGraph.node(bridgeNodeName); + if (!bridgeNodeRenderInfo) { + + // Find or create the directional container for the bridge node. + let bridgeContainerInfo = coreGraph.node(bridgeContainerName); + if (!bridgeContainerInfo) { + let bridgeContainerNode: BridgeNode = { + // Important node properties. + name: bridgeContainerName, + type: NodeType.BRIDGE, + // Unused node properties. + isGroupNode: false, + cardinality: 0, + parentNode: null, + stats: null, + // BridgeNode properties. + inbound: inbound, + }; + bridgeContainerInfo = + new RenderNodeInformation(bridgeContainerNode); + this.index[bridgeContainerName] = bridgeContainerInfo; + coreGraph.setNode(bridgeContainerName, bridgeContainerInfo); + } + + let bridgeNode: BridgeNode = { + // Important node properties. + name: bridgeNodeName, + type: NodeType.BRIDGE, + // Unimportant node properties. + isGroupNode: false, + cardinality: 1, + parentNode: null, + stats: null, + // BridgeNode properties. + inbound: inbound, + }; + bridgeNodeRenderInfo = new RenderNodeInformation(bridgeNode); + this.index[bridgeNodeName] = bridgeNodeRenderInfo; + coreGraph.setNode(bridgeNodeName, bridgeNodeRenderInfo); + + // Set bridgeNode to be a graphlib child of the container node. + coreGraph.setParent(bridgeNodeName, bridgeContainerName); + bridgeContainerInfo.node.cardinality++; + } + + // Create and add a bridge render metaedge. + let bridgeRenderMetaedge = + new RenderMetaedgeInformation(bridgeMetaedge); + bridgeRenderMetaedge.adjoiningMetaedge = adjoiningMetaedge; + inbound ? + coreGraph.setEdge(bridgeNodeName, childName, bridgeRenderMetaedge) : + coreGraph.setEdge(childName, bridgeNodeName, bridgeRenderMetaedge); + + }); // End _.each(bridgegraph.edges). + + // For each bridge container (IN and/or OUT), add structural edges between + // terminal nodes and that container. A terminal node is one which has no + // non-bridge edges in the direction of the container. + // + // For example, consider a Metanode A which contains two child nodes A/B + // and A/C. Let's say it has one edge in the metagraph from A/B->A/C, and + // one edge in the bridgegraph from Z->A/C. + // + // At this point, we've added a container bridge node IN to house all + // incoming bridge nodes. We'v alse added a bridge node Z' (with parent IN) + // to A, and a bridge edge from Z'->C. + // + // +----------------------+ + // | A +---+ | + // | +------>| C | | + // | | +---+ | + // | | ^ | + // | | | | + // | | +----|----+ | + // | | | IN | | | + // | +---+ | +---+ | | + // | | B | | | Z'| | | + // | +---+ | +---+ | | + // | +---------+ | + // +----------------------+ + // + // With no other help, dagre would lay out B and Z' on the same level, + // because both of them have no incoming edges. In other words, B is a + // terminal node in the INCOMING direction. + // + // But we want to force dagre to lay out Z' (and everything in IN) lower + // than all non-bridge nodes, so that there's enough room for the bridge + // edges after they've been adjusted to meet up with paths coming in from + // outside. + // + // To force Z' (and all other bridge nodes) to be lowest in the graph, we + // identify terminal nodes like B and give them structural edges to + // a new structural bridge node S which we add to IN. + // + // +----------------------+ + // | A +---+ | + // | +--->| C | | + // | | +---+ | + // | +---+ ^ | + // | | B | | | + // | +---+ | | + // | ^ | | + // | | | | + // | +----|------|----+ | + // | |IN | | | | + // | | +---+ +---+ | | + // | | | S | | Z'| | | + // | | +---+ +---+ | | + // | +----------------+ | + // +----------------------+ + // + // This ensures that dagre will lay out the bridge containers strictly at + // the ends of the graph. The structural edges will never be seen in the + // visualization except as a debugging aid. + _.each([true, false], inbound => { + let bridgeContainerName = getBridgeNodeName(inbound, nodeName); + let bridgeContainerInfo = coreGraph.node(bridgeContainerName); + if (!bridgeContainerInfo) { + return; + } + _.each(coreGraph.nodes(), childName => { + // Short-circuit if this child is a bridge node or it's not a terminal + // node in the direction we're interested in. + let childNodeInfo = coreGraph.node(childName); + if (childNodeInfo.node.type === NodeType.BRIDGE) { + return; + } + let isTerminal = inbound ? + !coreGraph.predecessors(childName).length : + !coreGraph.successors(childName).length; + if (!isTerminal) { + return; + } + + // Find or create a bridge node in the container for all structural + // metaedges. It would have been nice to skip this step and simply + // set a metaedge between the terminal node and the container node, but + // in that case, something about the graph upsets dagre.layout()'s + // longestPath algorithm (was getting errors due to an undefined). + let structuralNodeName = + getBridgeNodeName(inbound, nodeName, "STRUCTURAL_TARGET"); + let structuralRenderInfo = coreGraph.node(structuralNodeName); + if (!structuralRenderInfo) { + let bridgeNode: BridgeNode = { + // Important Node properties. + name: structuralNodeName, + type: NodeType.BRIDGE, + // Unimportant Node properties. + isGroupNode: false, + cardinality: 1, + parentNode: null, + stats: null, + // BridgeNode properties. + inbound: inbound, + }; + structuralRenderInfo = new RenderNodeInformation(bridgeNode); + structuralRenderInfo.structural = true; + this.index[structuralNodeName] = structuralRenderInfo; + coreGraph.setNode(structuralNodeName, structuralRenderInfo); + bridgeContainerInfo.node.cardinality++; + coreGraph.setParent(structuralNodeName, bridgeContainerName); + } + + // Create the structural Metaedge and insert it. + let structuralMetaedgeInfo = new RenderMetaedgeInformation(null); + structuralMetaedgeInfo.structural = true; + structuralMetaedgeInfo.weight--; // Reduce weight for dagre layout. + inbound ? + coreGraph.setEdge( + structuralNodeName, childName, structuralMetaedgeInfo) : + coreGraph.setEdge( + childName, structuralNodeName, structuralMetaedgeInfo); + }); + }); + } +} + +/** + * A class for rendering annotation object which contains label + * about the node embedded as annotation, type of annotation and the location + * of both the annotation's node and edge. + * + * Annotation objects include embedded constants, embedded summary, and + * edge shortcuts. + */ +export class Annotation { + node: Node; + renderNodeInfo: RenderNodeInformation; + renderMetaedgeInfo: RenderMetaedgeInformation; + annotationType: AnnotationType; + /** + * Center position of annotation relative to the host + * node's center x. + */ + dx: number; + /** + * Center position of annotation relative to the host + * node's center y. + */ + dy: number; + width: number; + height: number; + /** + * A flag whether it is an in-annotation (if true) or + * out-annotation (if false). + */ + isIn: boolean; + /** Label horizontal offset from the end of the node shape */ + labelOffset: number; + /** + * Array of points for edges from the annotation to its host + * node. Each point contains the point location, relative to + * the host node's center. + */ + points: {dx: number, dy: number}[]; + + /** + * Creates a new Annotation. + * + * @param node The underlying node this annotation points to. + * @param renderNodeInfo The render information for the underlying node + * this annotation points to. This can be null if the annotation + * denotes an embedding (constant, summary), in which case we + * use the node property. + * @param renderMetaedgeInfo The render information for the edge associated + * with the annotation. + * @param type The type of the annotation. + * @param isIn True if it is an in-annotation. False if it is an + * out-annotation. + */ + constructor(node: Node, renderNodeInfo: RenderNodeInformation, + renderMetaedgeInfo: RenderMetaedgeInformation, type: AnnotationType, + isIn: boolean) { + this.node = node; + this.renderNodeInfo = renderNodeInfo; + this.renderMetaedgeInfo = renderMetaedgeInfo; + this.annotationType = type; + // Properties specified by layout + this.dx = 0; + this.dy = 0; + this.width = 0; + this.height = 0; + + this.isIn = isIn; + this.points = []; + } +}; + +export enum AnnotationType {SHORTCUT, CONSTANT, SUMMARY, ELLIPSIS}; + +/** + * Manages a list of annotations. Two will be used for each + * RenderNodeInformation, one for in annotations and one for out annotations. + */ +export class AnnotationList { + /** + * List of visually drawable annotations, may include an ellipses annotation + * if the number added exceeds the number specified by maxAnnotations. + */ + list: Annotation[]; + + /** + * Set of nodes which have been added as annotations to this list, so we can + * prevent duplicates. + */ + nodeNames: { [nodeName: string]: boolean }; + + constructor() { + this.list = []; + this.nodeNames = {}; + } + + /** + * Append an annotation to the list, or a stand-in ellipsis annotation instead + * if this would make it too many. + */ + push(annotation: Annotation, params: RenderGraphParams): void { + if (annotation.node.name in this.nodeNames) { + return; // Skip duplicate annotation. + } + this.nodeNames[annotation.node.name] = true; + + if (this.list.length < params.maxAnnotations) { + this.list.push(annotation); + return; + } + + let lastAnnotation = this.list[this.list.length - 1]; + if (lastAnnotation.annotationType === AnnotationType.ELLIPSIS) { + let ellipsisNode = <EllipsisNode>lastAnnotation.node; + ellipsisNode.setNumMoreNodes(++ellipsisNode.numMoreNodes); + return; + } + + let ellipsisNode = new tf.graph.EllipsisNodeImpl(1); + this.list.push(new Annotation(ellipsisNode, + new RenderNodeInformation(ellipsisNode), null, + AnnotationType.ELLIPSIS, annotation.isIn)); + } +} + +/** + * Contains rendering information about a node in the hierarchical graph. + */ +export class RenderNodeInformation { + /** Reference to the original underlying Node from the hierarchical graph. */ + node: Node; + /** Whether the node is expanded or not. */ + expanded: boolean; + /** + * List of rendering information about in-annotations like constants and + * shortcuts to high-degree nodes. + */ + inAnnotations: AnnotationList; + /** List of rendering information about out-annotations (e.g. summary nodes) */ + outAnnotations: AnnotationList; + + // --- Params specified by layout --- // + + /** Center x position */ + x: number; + /** Center y position */ + y: number; + /** Width of the node's shape */ + width: number; + /** Height of the node's shape. */ + height: number; + /** Width of the bounding box for all in-annotations. */ + inboxWidth: number; + /** Width of the bounding box for all out-annotations. */ + outboxWidth: number; + /** + * Whether the node should be excluded from the scene. + * This is only used when there are too many items in a series so we only + * want to include top N ones. + */ + // TODO(jimbo): Now that series rendering is non-recursive, remove this and + // all its uses from the code base. + excluded: boolean; + + // --- Params used in drawing the bridge paths --- // + + /** + * All bridge nodes are meant to be invisible, but whereas most represent a + * relationship from the underlying graph hierarchy, some exist solely for + * layout reasons. Specifically, those bridge nodes which have only structural + * rendering metaedges. + */ + structural: boolean; + + // --- Params for the size of the node box --- // + + /** Label vertical offset from the center of node shape */ + labelOffset: number; + /** X-space between each extracted node and the core graph. */ + extractXOffset: number; + /** Rectangle radius (for making rounded rectangle) */ + radius: number; + + // --- Params for expanded node --- // + + /** Label height for expanded node. */ + labelHeight: number; + // Paddings between inner subscene and the border of the expanded node. + paddingTop: number; + paddingLeft: number; + paddingRight: number; + paddingBottom: number; + + /** Width of the whole node including its shape and its annotations */ + outerWidth: number; + /** Height of the whole node including its shape and its annotations */ + outerHeight: number; + /** + * Whether a node is extracted as source-like (having high out-degree or matching + * predefined in-extract pattern.) + */ + isInExtract: boolean; + /** + * Whether a node is extracted as sink-like (having high in-degree or matching + * predefined out-extract pattern.) + */ + isOutExtract: boolean; + + /** + * List of (color, proportion) tuples based on the proportion of devices of + * its children. If this node is an op node, this list will have only one + * color with proportion 1.0. + */ + deviceColors: {color: string, proportion: number}[]; + + /** + * Color according to the memory usage of this node. + */ + memoryColor: string; + + /** + * Color according to the compute time of this node. + */ + computeTimeColor: string; + + constructor(node: Node) { + this.node = node; + this.expanded = false; + this.inAnnotations = new AnnotationList(); + this.outAnnotations = new AnnotationList(); + // Params specified by layout + this.x = 0; + this.y = 0; + this.width = 0; + this.height = 0; + this.inboxWidth = 0; + this.outboxWidth = 0; + + this.excluded = false; + + // Params for bridge paths. + this.structural = false; + + // Params for node box. + this.labelOffset = 0; + this.extractXOffset = 0; + this.radius = 0; + + // Params for expanded node + this.labelHeight = 0; + this.paddingTop = 0; + this.paddingLeft = 0; + this.paddingRight = 0; + this.paddingBottom = 0; + + this.outerWidth = 0; + this.outerHeight = 0; + this.isInExtract = false; + this.isOutExtract = false; + } + + isInCore(): boolean { + return !this.isInExtract && !this.isOutExtract; + } +} + +/** + * Contains rendering information about a Metaedge from the underlying + * hierarchical graph. It may be from either a metagraph or a bridgegraph. + */ +export class RenderMetaedgeInformation { + /** + * Reference to the original underlying Metaedge from the hierarchical graph, + * if any. This will be null for the edges which connect OpNodes to their + * embeddings, for example. + */ + metaedge: Metaedge; + + /** + * Reference to the adjoining RenderMeteaedgeInformation from the parent's + * coreGraph. This is used during layout to determine the point at which this + * edge should touch the node's bounding box. This property will be null for + * edges which terminate at a node on both ends (all non-bridge edges). + */ + adjoiningMetaedge: RenderMetaedgeInformation; + + /** + * Most of the time, a RenderMetaedgeInformation object represents a real + * edge between nodes in the underlying graph structure. But sometimes, an + * edge only exsts for layout purposes. These structural edges are added + * during buildSubhierarchy() to force dagre.layout() to put bridge nodes + * at the ends of the flow. + * @see buildSubhierarchy() + */ + structural: boolean; + + /** + * Weight of the edge, used by dagre when deciding how important an edge is. + * Edges with higher weight are made shorter and straighter. The default + * dagre uses is 1. + */ + weight: number; + + /** + * X and Y coordinate pairs of the points in the path of the edge. + * @see tf.graph.node.subsceneAdjustPaths + */ + points: any[]; + + /** + * D3 selection of the group containing the path that displays this edge. + */ + edgeGroup: d3.Selection<RenderMetaedgeInformation>; + + constructor(metaedge: Metaedge) { + this.metaedge = metaedge; + this.adjoiningMetaedge = null; + this.structural = false; + this.weight = 1; + } +} + +function addInAnnotation(node: RenderNodeInformation, predecessor: Node, + predecessorRenderInfo: RenderNodeInformation, edge: any, + type: AnnotationType, params: RenderGraphParams): void { + let annotation = new Annotation(predecessor, predecessorRenderInfo, edge, + type, true); + node.inAnnotations.push(annotation, params); +} + +function addOutAnnotation(node: RenderNodeInformation, successor: Node, + successorRenderInfo: RenderNodeInformation, edge: any, + type: AnnotationType, params: RenderGraphParams): void { + let annotation = new Annotation(successor, successorRenderInfo, edge, + type, false); + node.outAnnotations.push(annotation, params); +} + +function setGraphDepth(graph: graphlib.Graph<RenderNodeInformation, any>, + depth: number) { + _.each(graph.nodes(), nodeName => { + let child = graph.node(nodeName); + child.expanded = depth > 1; // set all child of depth 1 to collapsed + if (depth > 0) { + switch (child.node.type) { + case NodeType.META: + case NodeType.SERIES: + setGroupNodeDepth(<RenderGroupNodeInformation>child, depth - 1); + break; + // Do nothing for leaf + } + } + }); +}; + +export class RenderGroupNodeInformation extends RenderNodeInformation { + node: GroupNode; + /** + * The core graph is derived from the underlying node's metagraph, minus + * the extracted source-like and sink-like nodes. + */ + coreGraph: graphlib.Graph<RenderNodeInformation, RenderMetaedgeInformation>; + /** Size of the bounding box for a metanode's core graph. */ + coreBox: { + width: number, + height: number, + }; + /** Size of the bounding box for a metanode's isolated in-extract children. */ + inExtractBox: {width: number, height: number}; + /** Size of the bounding box for a metanode's isolated out-extract children. */ + outExtractBox: {width: number, height: number}; + /** Array of isolated in-extract nodes. */ + isolatedInExtract: RenderNodeInformation[]; + /** Array of isolated out-extract nodes. */ + isolatedOutExtract: RenderNodeInformation[]; + + constructor(groupNode: GroupNode) { + super(groupNode); + let metagraph = groupNode.metagraph; + let gl = metagraph.graph(); + this.coreGraph = + createGraph<RenderNodeInformation, RenderMetaedgeInformation>( + gl.name, GraphType.CORE, { compound: true }); + this.coreBox = {width: 0, height: 0}; + this.inExtractBox = {width: 0, height: 0}; + this.outExtractBox = {width: 0, height: 0}; + this.isolatedInExtract = []; + this.isolatedOutExtract = []; + } +} + +function setGroupNodeDepth(renderInfo: RenderGroupNodeInformation, + depth: number): void { + if (renderInfo.coreGraph) { + setGraphDepth(renderInfo.coreGraph, depth); + } +} + +/** + * Remove an edge from the graph and add annotations to both ends of the edge. + * + * @param The core graph. + * @param v Source name. + * @param w Sink name. + */ +function createShortcut(graph: graphlib.Graph<RenderNodeInformation, {}>, + v: string, w: string, params: RenderGraphParams) { + let src = graph.node(v); + let sink = graph.node(w); + let edge = graph.edge(v, w); + + // Add each annotation. + addOutAnnotation(src, sink.node, sink, edge, AnnotationType.SHORTCUT, params); + addInAnnotation(sink, src.node, src, edge, AnnotationType.SHORTCUT, params); + + // Remove the edge from the core graph. + graph.removeEdge(v, w); +} + +/** + * Remove edges from a node, and set its isOutExtract property to true, + * and remove the node and move it to isolatedOutExtract. + * + * If detachAllEdgesForHighDegree is true, extract all of its edges. + * Otherwise, only extract all in-edges. + */ +function makeOutExtract(renderNode: RenderGroupNodeInformation, n: string, + params: RenderGraphParams) { + let graph = renderNode.coreGraph; + + graph.node(n).isOutExtract = true; + + _.each(graph.predecessors(n), (p, index) => { + createShortcut(graph, p, n, params); + }); + + if (params.detachAllEdgesForHighDegree) { + _.each(graph.successors(n), (s, index) => { + createShortcut(graph, n, s, params); + }); + } + + if (params.detachAllEdgesForHighDegree || graph.neighbors(n).length === 0) { + renderNode.isolatedOutExtract.push(graph.node(n)); + graph.removeNode(n); + } +} + +/** + * Remove edges from a node, set its isInExtract property to true, + * and remove the node and move it to isolatedInExtract. + * If detachAllEdgesForHighDegree is true, extract all of its edges. + * Otherwise, only remove all out-edges. + */ +function makeInExtract(renderNode: RenderGroupNodeInformation, n: string, + params: RenderGraphParams) { + let graph = renderNode.coreGraph; + graph.node(n).isInExtract = true; + + _.each(graph.successors(n), (s, index) => { + createShortcut(graph, n, s, params); + }); + + if (params.detachAllEdgesForHighDegree) { + _.each(graph.predecessors(n), (p, index) => { + createShortcut(graph, p, n, params); + }); + } + + // Remove the node from the core graph if conditions are met. + if (params.detachAllEdgesForHighDegree || graph.neighbors(n).length === 0) { + renderNode.isolatedInExtract.push(graph.node(n)); + graph.removeNode(n); + } +} + +/** + * Check whether the node's type is a member of the given list of types. + * + * @param node Node. + * @param types List of type to match. + */ +function hasTypeIn(node: Node, types: string[]): boolean { + if (node.type === NodeType.OP) { + for (let i = 0; i < types.length; i++) { + if ((<OpNode>node).op === types[i]) { return true; } + } + } else if (node.type === NodeType.META) { + let rootOpNode = (<Metanode>node).getRootOp(); + if (rootOpNode) { + for (let i = 0; i < types.length; i++) { + if (rootOpNode.op === types[i]) { return true; } + } + } + } + return false; +} + +/** Remove edges from pre-defined out-extract patterns */ +function extractPredefinedSink(renderNode: RenderGroupNodeInformation, + params: RenderGraphParams) { + let graph = renderNode.coreGraph; + _.each(graph.nodes(), n => { + let renderInfo = graph.node(n); + if (hasTypeIn(renderInfo.node, params.outExtractTypes)) { + makeOutExtract(renderNode, n, params); + } + }); +} + +/** Remove edges from pre-defined in-extract patterns */ +function extractPredefinedSource(renderNode: RenderGroupNodeInformation, + params: RenderGraphParams) { + let graph = renderNode.coreGraph; + + _.each(graph.nodes(), n => { + let renderInfo = graph.node(n); + if (hasTypeIn(renderInfo.node, params.inExtractTypes)) { + makeInExtract(renderNode, n, params); + } + }); +} + +/** Extract from nodes with in-degree > maxInDegree */ +function extractHighInDegree(renderNode: RenderGroupNodeInformation, + params: RenderGraphParams) { + let graph = renderNode.coreGraph; + let maxInDegree = params.maxInDegree; + + // detect first so degrees don't get affected by other removal + let highInDegreeNames = _.filter(graph.nodes(), n => { + // Count the in-degree based on only regular edges, unless there are + // no regular edges, in which case use the number of control edges. + // This is done so that control edges don't effect if nodes are extracted + // from the core graph, unless the node is only used for control. + let numEdgesToCount = _.reduce(graph.predecessors(n), (numEdgesToCount, pred) => { + let metaedge = graph.edge(pred, n).metaedge; + return numEdgesToCount + (metaedge.numRegularEdges ? 1 : 0); + }, 0); + if (numEdgesToCount === 0 && graph.predecessors(n).length > 0) { + numEdgesToCount = graph.predecessors(n).length; + } + return numEdgesToCount > maxInDegree; + }); + + _.each(highInDegreeNames, n => { + makeOutExtract(renderNode, n, params); + }); +} + +/** Extract nodes with out-degree > maxOutDegree */ +function extractHighOutDegree(renderNode: RenderGroupNodeInformation, + params: RenderGraphParams) { + let graph = renderNode.coreGraph; + let maxOutDegree = params.maxOutDegree; + + // detect first so degrees don't get affected by other removal + let highOutDegreeNames = _.filter(graph.nodes(), n => { + // Count the out-degree based on only regular edges, unless there are + // no regular edges, in which case use the number of control edges. + // This is done so that control edges don't effect if nodes are extracted + // from the core graph, unless the node is only used for control. + let numEdgesToCount = _.reduce(graph.successors(n), (numEdgesToCount, succ) => { + let metaedge = graph.edge(n, succ).metaedge; + return numEdgesToCount + (metaedge.numRegularEdges ? 1 : 0); + }, 0); + if (numEdgesToCount === 0 && graph.successors(n).length > 0) { + numEdgesToCount = graph.successors(n).length; + } + return numEdgesToCount > maxOutDegree; + }); + + _.each(highOutDegreeNames, n => { + makeInExtract(renderNode, n, params); + }); +} + +/** Remove control edges from nodes that have too many control edges */ +function removeControlEdges(renderNode: RenderGroupNodeInformation, + params: RenderGraphParams) { + let graph = renderNode.coreGraph; + + // Collect control edges into a map by node name. + let map = <{[nodeName: string]: graphlib.EdgeObject[]}>{}; + _.each(graph.edges(), e => { + if (!graph.edge(e).metaedge.numRegularEdges) { + (map[e.v] = map[e.v] || []).push(e); + (map[e.w] = map[e.w] || []).push(e); + } + }); + + // For each node with too many control edges, turn them into annotations. + _.each(map, (edges, nodeName) => { + if (edges.length > params.maxControlDegree) { + _.each(edges, e => createShortcut(graph, e.v, e.w, params)); + } + }); +} + +/** + * Given an integer, picks a hue that is far apart from other colors. + * The formula for picking color that avoid collision is: + * hue = (color range * golden ratio * index) % color range + */ +export function mapIndexToHue(id: number): number { + let GOLDEN_RATIO = 1.61803398875; + // Hue of 0 is reserved for the gray nodes. + let MIN_HUE = 1; + let MAX_HUE = 359; + let COLOR_RANGE = MAX_HUE - MIN_HUE; + return MIN_HUE + ((COLOR_RANGE * GOLDEN_RATIO * id) % COLOR_RANGE); +}; + +/** + * Remove edges and add to annotation instead. + * + * For root node, consider predefined types for source and sink. + * We do not extract predefined type from non-root so that Variables and the + * sgd node (op type = "NoOp") do not get extract from inside own group. + * + * The order of extraction is important here as swapping the order can totally + * screw up the graph layout. + * + * @param {Render.Node} renderNode Node to manipulate. + * @param {Object} params render Graph construction parameters. See + * <tf-graph-params>'s output + */ +function extractHighDegrees(renderNode: RenderGroupNodeInformation, + params: RenderGraphParams) { + if (params.outExtractTypes) { + extractPredefinedSink(renderNode, params); + } + + // This has to come before extract high in-degree to protect the core part + // that takes many variables. + if (params.inExtractTypes) { + extractPredefinedSource(renderNode, params); + } + + // This has to come before extract high out-degree to protect the core part + // that output to many places as there are more high-degree sinks than + // sources. + + if (params.maxInDegree) { + extractHighInDegree(renderNode, params); + } + + if (params.maxOutDegree) { + extractHighOutDegree(renderNode, params); + } + + if (params.maxControlDegree) { + removeControlEdges(renderNode, params); + } + + // Extract isolated nodes, which can be + // (1) source-like and sink-like nodes that are not originally isolated but + // become isolated after further removal. + // (2) isolated nodes with annotations on one-side. These might be either + // - nodes that originally have high out-degree but because we remove + // high in-degree nodes first, they no longer have high in-degree when + // we check. (Detecting all high-degree before removing also leads to + // another problem.) + // - nodes that do not have high degree, but their neighbors are all + // extracted, so it might make sense to extract them too. + + let graph = renderNode.coreGraph; + _.each(graph.nodes(), n => { + let child = graph.node(n); + let degree = graph.neighbors(n).length; + + if (degree === 0) { + let hasOutAnnotations = child.outAnnotations.list.length > 0; + let hasInAnnotations = child.inAnnotations.list.length > 0; + + if (child.isInExtract) { // Is source-like. + // This case only happens if detachAllEdgesForHighDegree is false. + // (Otherwise all source-like nodes are all isolated already.) + renderNode.isolatedInExtract.push(child); + graph.removeNode(n); + } else if (child.isOutExtract) { // Is sink-like. + // This case only happens if detachAllEdgesForHighDegree is false. + // // (Otherwise all sink-like nodes are all isolated already.) + renderNode.isolatedOutExtract.push(child); + graph.removeNode(n); + } else if (params.extractIsolatedNodesWithAnnotationsOnOneSide) { + if (hasOutAnnotations && !hasInAnnotations) { + child.isInExtract = true; // for ones with high out-annotations + renderNode.isolatedInExtract.push(child); + graph.removeNode(n); + } else if (hasInAnnotations && !hasOutAnnotations) { + child.isOutExtract = true; // for ones with high in-annotations + renderNode.isolatedOutExtract.push(child); + graph.removeNode(n); + } else { + // if a low degree node has both in- & out- annotations, do nothing + // because it is unclear which side it should go to. + } + } + } + }); +} +} // close module tf.graph.render |