{ "cells": [ { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ " # Inspecting Node Graphs\n", " \n", "Given a graph, traversal is a common task for many workflows including shader code generation.\n", " \n", "This book will cover the basics of:\n", "1. Traversing upstream from a \"root\" node.\n", "2. Extracting connectivity information from a Edge structure during traversal.\n", "3. Extracting grouping information (GraphElement membership) during traversal.\n", "3. Traversing in to and out of graphs via interfaces.\n", "4. Example logic for viewing nodegraphs using traversal logic.\n", "\n", "Note that the key low level interfaces used for traversal are:\n", "1. Port getConnectedOutput() for finding the output connected to an input.\n", "2. Port getConnectedNode() for finding the node connected to an input. This uses the previous interface.\n", "3. Input getInterfaceInput()() for finding if an input is connected to an input interface on a node graph. \n", "\n" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Setup\n", "\n", "The basic setup as outlined in the \"Basics\" book imports the MaterialX module, creates a working document, loads in the standard library definitions, and provides a predicate to skip definitions when writing documents. " ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [], "source": [ "import MaterialX as mx\n", "\n", "# Version check\n", "from mtlxutils.mxbase import *\n", "haveVersion1387 = haveVersion(1, 38,7) \n", "if not haveVersion1387:\n", " print(\"** Warning: Recommended version is 1.38.7 for tutorials. Have version: \", mx.__version__)\n", "\n", "from mtlxutils.mxfile import MtlxFile as mxf\n", "doc, libFiles, status = mxf.createWorkingDocument()" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## GraphElement Traversal \n", "\n", "The easiest way to see what how a set of nodes is connected up is by using a GraphIterator which can be accessed via the \n", "traverseGraph() interface on an element. The iterator will traverse upstream starting from the element. Note that the iterator will only work on certain types of elements. A general rule is whatever is considered \"renderable\" by the utility findRenderableElements() can be used. Outputs and material nodes are the recommended starting points. \n", "\n", "In this example we load in an example graph, and traverse it this way.\n", "The key element that is returned from the iterator is an Edge. The edge provides the connection information of what is:\n", "* the upstream node\n", "* the downstream node\n", "* the downstream ``\n", "\n", "The utility `printEdge()` is provided as an example of how to access information on an `Edge`." ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [], "source": [ "def printEdge(edge):\n", " \"Sample utility to print out the basic information about an edge\"\n", "\n", " upstreamElem = edge.getUpstreamElement()\n", " downstreamElem = edge.getDownstreamElement()\n", " connectingElem = edge.getConnectingElement()\n", "\n", " downstreamPath = ''; \n", " if connectingElem:\n", " downstreamPath = connectingElem.getNamePath()\n", " else:\n", " downstreamPath = downstreamElem.getNamePath()\n", "\n", " # Print out information about the edge with an \"arrow\" to show direction\n", " # of data flow \n", " print('Edge: ' + upstreamElem.getNamePath() + ' --> ' + downstreamPath)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "This utility is used during the traversal of every edge. \n", "\n", "As it is possible to visit the same edge more than once, we keep a set of unique edges `processedEdges` to skip duplicates. To avoid this an additional utility `findEdge()` has been added to perform `Edge` comparisons. This explicit comparator is **only required in Python** as the C++ equality operator for `Edge` is not currently exposed in the Python API." ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [], "source": [ "\n", "def findEdge(edge, processedEdges):\n", " \"Edge equality comparitor\"\n", " for pe in processedEdges:\n", " # Note: the comparison (pe == edge) does not work \n", " if (pe.getUpstreamElement() == edge.getUpstreamElement() and\n", " pe.getDownstreamElement() == edge.getDownstreamElement() and\n", " pe.getConnectingElement() == edge.getConnectingElement()):\n", " return True\n", " return False" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "A simple usage example follows:\n", "\n", "1. The \"marble\" sample graph is read in. \n", "2. Within this graph we look for \"material\" nodes to use as the root for traversal.\n", "3. For each root, a `GraphIterator` is used via `traverseGraph()`.\n", "4. A list of edges is found and then printed out." ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Edge: NG_marble1/color_mix --> SR_marble1/base_color\n", "Edge: NG_marble1/power --> NG_marble1/color_mix/mix\n", "Edge: NG_marble1/add_xyz --> NG_marble1/scale_xyz/in1\n", "Edge: NG_marble1/noise --> NG_marble1/scale_noise/in1\n", "Edge: NG_marble1/scale --> NG_marble1/bias/in1\n", "Edge: NG_marble1/color_mix --> SR_marble1/subsurface_color\n", "Edge: SR_marble1 --> Marble_3D/surfaceshader\n", "Edge: NG_marble1/sin --> NG_marble1/scale/in1\n", "Edge: NG_marble1/bias --> NG_marble1/power/in1\n", "Edge: NG_marble1/obj_pos --> NG_marble1/add_xyz/in1\n", "Edge: NG_marble1/scale_pos --> NG_marble1/noise/position\n", "Edge: NG_marble1/obj_pos --> NG_marble1/scale_pos/in1\n", "Edge: NG_marble1/scale_xyz --> NG_marble1/sum/in1\n", "Edge: NG_marble1/scale_noise --> NG_marble1/sum/in2\n", "Edge: NG_marble1/sum --> NG_marble1/sin/in\n" ] } ], "source": [ "# Read in sample graph\n", "mx.readFromXmlFile(doc, 'data/standard_surface_marble_solid.mtlx')\n", "\n", "# Find the material nodes and traverse starting from them.\n", "roots = doc.getMaterialNodes()\n", "\n", "# Keep a list of edges already visited\n", "processedEdges = set()\n", "for root in roots:\n", " for edge in root.traverseGraph():\n", " if not findEdge(edge, processedEdges):\n", " processedEdges.add(edge)\n", "\n", "# Examine the edge list\n", "for edge in processedEdges:\n", " printEdge(edge)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Based on the path information printed out, it can be seen that traversal occurs not just at the document level but into (and out of) child nodegraph containers (`GraphElements`).\n", "\n", "Tracking of what nodes are in which graphs can be added to see node groupings. The utility functions `updateSubgraphItem` and `updateSubgraph` are added to build a dictionary of `{ GraphElement, [ children Elements ]}`. Note that the top level `Document` has an empty string for it's path." ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Root Document:\n", "- Marble_3D\n", "- SR_marble1\n", "NG_marble1:\n", "- NG_marble1/add_xyz\n", "- NG_marble1/bias\n", "- NG_marble1/color_mix\n", "- NG_marble1/noise\n", "- NG_marble1/obj_pos\n", "- NG_marble1/power\n", "- NG_marble1/scale\n", "- NG_marble1/scale_noise\n", "- NG_marble1/scale_pos\n", "- NG_marble1/scale_xyz\n", "- NG_marble1/sin\n", "- NG_marble1/sum\n" ] } ], "source": [ "def updateGraphDictionaryPath(key, value, graphDictionary):\n", " \"\"\"\n", " Add a parent / child to the GraphElement dictionary\n", " \"\"\"\n", " if key in graphDictionary:\n", " graphDictionary[key].add(value)\n", " else:\n", " graphDictionary[key] = {value}\n", "\n", "\n", "def updateGraphDictionaryItem(item, graphDictionary):\n", " \"\"\"\n", " Add a Element to the GraphElement dictionary, where the keys are the GraphElement's path, and the value\n", " is a list of child Element paths\n", " \"\"\"\n", " if not item:\n", " return\n", "\n", " parentElem = item.getParent()\n", " if not parentElem or not parentElem.isA(mx.GraphElement):\n", " return\n", "\n", " key = parentElem.getNamePath()\n", " value = item.getNamePath()\n", " updateGraphDictionaryPath(key, value, graphDictionary)\n", "\n", "def updateGraphDictionary(edge, graphDictionary):\n", " \"\"\"\n", " Add nodes from either end of the connection to a GraphElement dictionary\n", " \"\"\"\n", " ends = [edge.getUpstreamElement(), edge.getDownstreamElement()]\n", " for end in ends:\n", " updateGraphDictionaryItem(end, graphDictionary)\n", "\n", "def printGraphDictionary(graphDictionary):\n", " \"\"\"\n", " Print out the sub-graph dictionary\n", " \"\"\"\n", " for graphPath in graphDictionary:\n", " # Top level document has not path, so just output some identifier string\n", " if graphPath == '':\n", " print('Root Document:')\n", " else:\n", " print(graphPath + ':')\n", " for node in sorted(graphDictionary[graphPath], key=str.lower):\n", " print('- ', node)\n", "\n", "# Travse all edges and add up and downstream nodes to\n", "# the graph dictionary\n", "graphDictionary = {}\n", "processedEdges = set()\n", "for root in roots:\n", " for edge in root.traverseGraph():\n", " if not findEdge(edge,processedEdges):\n", " processedEdges.add(edge)\n", " updateGraphDictionary(edge, graphDictionary)\n", "\n", "# Examine the dictionary.\n", "printGraphDictionary(graphDictionary)\n" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Querying for the children of a `GraphElement` can be done using something like `getChildren()` as discussed in the \"*Basics*\" book. \n", "\n", "However, the purpose of using a traverser is to limit what is found in connected paths instead of just finding all children. This restrictive or filtered list is more optimal for workflows that involve finding exactly what affects the evaluation of a value and for sub-graph comparisons. \n", "A workflow where both are useful is to determine what children in a graph are not used for evaluation (not encountered during traversal)." ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Root Document\n", " - Marble_3D\n", " - SR_marble1\n", " \n", "NG_marble1\n", " - NG_marble1/add_xyz\n", " - NG_marble1/bias\n", " - NG_marble1/color_mix\n", " - NG_marble1/noise\n", " - NG_marble1/obj_pos\n", " - NG_marble1/power\n", " - NG_marble1/scale\n", " - NG_marble1/scale_noise\n", " - NG_marble1/scale_pos\n", " - NG_marble1/scale_xyz\n", " - NG_marble1/sin\n", " - NG_marble1/sum\n", " \n" ] } ], "source": [ "# Examine the entire contents of each graph element\n", "for graphPath in graphDictionary:\n", " graph = doc.getDescendant(graphPath)\n", " graphName = graph.getNamePath()\n", " print(graphName if graphName else \"Root Document\")\n", " children = graph.getNodes()\n", " for child in sorted(children, key=lambda x: x.getNamePath()):\n", " print(' -', child.getNamePath())\n", " print(' ')" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Viewing a Graph\n", "\n", "As an example application, traversal information can be used to create diagrams\n", "of graphs. In this case we will create Mermaid graphs." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "The generic print function `printEdge()` is replaced by logic to output in Mermaid format. The additional logic added is to handle syntax restrictions for node naming, and to allow for a node name and a \"UI\" label. The former requires a sanitized string and the latter is the MaterialX path string.\n", "\n", "Note that this same syntax is used for all Mermaid diagrams used for the node library reference.\n", "```\n", "(upstream node path) --[downstream node input name]--> (downstream node path)\n", "```" ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [], "source": [ "def emitMermaidEdge_nointerfaces(indent, edge):\n", " \"\"\"\n", " Sample utility to print out edge information in Mermaid format\n", " Returns a string of form: `(upstream node path) --[downstream node input name]--> (downstream node path)`\n", " which represents a connection from an upstream node to a downstream one via a given input port.\n", " \"\"\"\n", " outVal = ''\n", "\n", " upstreamElem = edge.getUpstreamElement()\n", " downstreamElem = edge.getDownstreamElement()\n", " connectingElem = edge.getConnectingElement()\n", "\n", " downstreamPath = ''\n", " connectionString = ''\n", " if connectingElem:\n", " connectionString = ' --\".' + connectingElem.getName() + '\"--> '\n", " else:\n", " connectionString = ' --> '\n", " downstreamPath = downstreamElem.getNamePath()\n", "\n", " upstreamPath = upstreamElem.getNamePath()\n", "\n", " # Sanitize names for Mermaid output\n", " upstreamPathM = mx.createValidName(upstreamPath)\n", " downstreamPathM = mx.createValidName(downstreamPath)\n", "\n", " # Print out information about the edge with an \"arrow\" to show direction\n", " # of data flow \n", " outVal = indent + upstreamPathM + '[' + upstreamPath + ']' + connectionString + downstreamPathM + '[' + downstreamPath + ']'\n", " return outVal\n" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "Mermaid supports output of children graphs via the use of the `subgraph` group declaration. The `emitMermaidSubgraphs()` variant \n", "queries the node graph dictionary to output each `GraphElement` item as a `subgraph`." ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [], "source": [ "def emitMermaidSubgraphs(subgraphs):\n", " \"\"\"\n", " Emit GraphElement dictionary in Mermaid format\n", " \"\"\"\n", " subGraphOutput = []\n", "\n", " for subgraph in subgraphs:\n", " if subgraph == '':\n", " continue\n", " \n", " subgraphM = mx.createValidName(subgraph) \n", " subGraphOutput.append('subgraph ' + subgraphM + ':')\n", " for node in subgraphs[subgraph]:\n", " subGraphOutput.append(' ' + mx.createValidName(node))\n", " subGraphOutput.append('end')\n", "\n", " return subGraphOutput" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "These new utilities are used in a wrapper utility `generateMermaidGraph` which takes in the set of roots\n", "to output and generates a string list containing the text for the Mermaid graph. " ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " graph TB;\n", " NG_marble1_obj_pos[NG_marble1/obj_pos] --\".in1\"--> NG_marble1_scale_pos[NG_marble1/scale_pos]\n", " NG_marble1_noise[NG_marble1/noise] --\".in1\"--> NG_marble1_scale_noise[NG_marble1/scale_noise]\n", " NG_marble1_sin[NG_marble1/sin] --\".in1\"--> NG_marble1_scale[NG_marble1/scale]\n", " NG_marble1_sum[NG_marble1/sum] --\".in\"--> NG_marble1_sin[NG_marble1/sin]\n", " SR_marble1[SR_marble1] --\".surfaceshader\"--> Marble_3D[Marble_3D]\n", " NG_marble1_bias[NG_marble1/bias] --\".in1\"--> NG_marble1_power[NG_marble1/power]\n", " NG_marble1_add_xyz[NG_marble1/add_xyz] --\".in1\"--> NG_marble1_scale_xyz[NG_marble1/scale_xyz]\n", " NG_marble1_scale_xyz[NG_marble1/scale_xyz] --\".in1\"--> NG_marble1_sum[NG_marble1/sum]\n", " NG_marble1_scale_pos[NG_marble1/scale_pos] --\".position\"--> NG_marble1_noise[NG_marble1/noise]\n", " NG_marble1_color_mix[NG_marble1/color_mix] --\".base_color\"--> SR_marble1[SR_marble1]\n", " NG_marble1_scale_noise[NG_marble1/scale_noise] --\".in2\"--> NG_marble1_sum[NG_marble1/sum]\n", " NG_marble1_power[NG_marble1/power] --\".mix\"--> NG_marble1_color_mix[NG_marble1/color_mix]\n", " NG_marble1_scale[NG_marble1/scale] --\".in1\"--> NG_marble1_bias[NG_marble1/bias]\n", " NG_marble1_obj_pos[NG_marble1/obj_pos] --\".in1\"--> NG_marble1_add_xyz[NG_marble1/add_xyz]\n", " NG_marble1_color_mix[NG_marble1/color_mix] --\".subsurface_color\"--> SR_marble1[SR_marble1]\n", "subgraph NG_marble1:\n", " NG_marble1_scale\n", " NG_marble1_bias\n", " NG_marble1_obj_pos\n", " NG_marble1_power\n", " NG_marble1_scale_noise\n", " NG_marble1_noise\n", " NG_marble1_scale_pos\n", " NG_marble1_color_mix\n", " NG_marble1_scale_xyz\n", " NG_marble1_sum\n", " NG_marble1_add_xyz\n", " NG_marble1_sin\n", "end\n" ] } ], "source": [ "def generateMermaidGraph_nointerfaces(roots, orientation):\n", " \"\"\"\n", " Output a Mermaid graph diagram given a set of root nodes\n", " \"\"\" \n", " subgraphs = {}\n", " processedEdges = set()\n", "\n", " # Find all edges, and build up the GraphElement dictionary\n", " for root in roots:\n", " for edge in root.traverseGraph():\n", " if not findEdge(edge,processedEdges):\n", " processedEdges.add(edge)\n", " updateGraphDictionary(edge, subgraphs)\n", "\n", " # Get string output for each edge in Mermaid format\n", " edgeOutput = set()\n", " for edge in processedEdges:\n", " outVal = emitMermaidEdge_nointerfaces(' ', edge)\n", " if outVal not in edgeOutput:\n", " edgeOutput.add(outVal)\n", "\n", " # Print graph header, edges, and sub-graphs\n", " outputGraph = []\n", " outputGraph.append(' graph %s;' % orientation)\n", " for outVal in edgeOutput:\n", " outputGraph.append(outVal)\n", " for line in emitMermaidSubgraphs(subgraphs):\n", " outputGraph.append(line)\n", "\n", " return outputGraph\n", "\n", "graph = generateMermaidGraph_nointerfaces(roots, 'TB')\n", "for line in graph:\n", " if line:\n", " print(line)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "The resulting diagram looks like this:\n", "\n", "" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Handling Graph Interfaces\n", "\n", "Traversal logic only return connections between nodes and **hides** any logic which is required to traverse through interface elements on\n", "`GraphElements` (`` and ``). This includes connections to unconnected leaf level `` interfaces .\n", "\n", "Specifically, the `GraphIterator` does not supply this information directly on the `Edge` structure.\n", "An additional gap in information is that any upstream node's `` is not provided. This is important **missing** information if\n", "the upstream node has multiple outputs, and would be useful to be addressed in a future release.\n", "\n", "To extract out interface information additional logic is required. For this example: \n", "\n", "* For interface inputs: `emitInterfaceInputs()` checks the upstream node for any interface connections checking each of it's inputs for an interface input using the `Input` interface `getInterfaceInput()`. If the input is found then a call is made to add it to the appropriate graph list. " ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [], "source": [ "def emitInterfaceInputs(indent, edge, subgraphs, edgeOutput):\n", " outVal = ''\n", "\n", " # Look for upstream interface inputs\n", " upstreamElem = edge.getUpstreamElement()\n", " for input in upstreamElem.getInputs():\n", " # getInterfaceInput() will find the interface input if it exists\n", " interfaceInput = input.getInterfaceInput()\n", " if interfaceInput:\n", "\n", " # Emit connection from interface input to node input\n", " interfaceName = interfaceInput.getName()\n", " interfaceNameM = mx.createValidName(interfaceInput.getNamePath())\n", " nodeName = mx.createValidName(upstreamElem.getNamePath())\n", " outVal = indent + interfaceNameM + '([' + interfaceName + ']) --\".' + input.getName() + '\"--> ' + nodeName\n", " if outVal not in edgeOutput:\n", " edgeOutput.add(outVal)\n", "\n", " # Update subgraphs to include this input\n", " updateGraphDictionaryItem(interfaceInput, subgraphs)\n", "\n", " return outVal" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "* For interface outputs: `emitMermaidEdge()` is a variation on `emitMermaidEdge_nointerfaces()` such that the downstream input is checked for any upstream output connection using the `Input` interface `getConnectedOutput()`. If an output is found then the a connection between this output to the input is emitted.\n", "\n", "Note that during traversal the `Port` interface `getConnectedOutput()` is used to perform input to output traversal, however only the upstream node is returned as part of an `Edge`. Thus the need for extra logic after the fact to find out if an output interface has been traversed. " ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [], "source": [ "def emitMermaidEdge(indent, edge, subgraphs, edgeOutput):\n", " \"Sample utility to print out edge information in Mermaid format\"\n", " \"The interface getConnectedOuput() is used to determine what output the dowstream input is connected to\"\n", "\n", " outVal = ''\n", "\n", " upstreamElem = edge.getUpstreamElement()\n", " downstreamElem = edge.getDownstreamElement()\n", " connectingElem = edge.getConnectingElement()\n", "\n", " downstreamPath = downstreamElem.getNamePath()\n", " upstreamPath = upstreamElem.getNamePath()\n", " upstreamPathM = mx.createValidName(upstreamPath)\n", "\n", " # Add a connection from the upstream output to the downstream \n", " upstreamOutput = None\n", " if connectingElem:\n", " outputString = connectingElem.getAttribute(\"output\")\n", " if outputString:\n", " upstreamOutput = downstreamElem.getConnectedOutput(connectingElem.getName())\n", " if upstreamOutput:\n", " upstreamOutputName = upstreamOutput.getName()\n", " upstreamOutputNameM = mx.createValidName(upstreamOutput.getNamePath())\n", " outConnectionString = upstreamOutputNameM + '[' + upstreamOutputName + ']'\n", "\n", " outVal = indent + upstreamPathM + '[' + upstreamPath + '] --> ' + outConnectionString\n", " if outVal not in edgeOutput:\n", " edgeOutput.add(outVal)\n", "\n", " updateGraphDictionaryItem(upstreamOutput, subgraphs)\n", "\n", " # The upstream output is the upstream path instead of the node.\n", " upstreamPath = upstreamOutput.getNamePath()\n", "\n", " # is not explicitly specified. This occurs for Node outputs\n", " else:\n", " upstreamOutputName = outputString\n", " graphElementPath = upstreamElem.getParent().getNamePath()\n", " upstreamOutputPath = graphElementPath + '/' + outputString\n", " upstreamOutputNameM = mx.createValidName(upstreamOutputPath)\n", " outConnectionString = upstreamOutputNameM + '[' + upstreamOutputName + ']'\n", "\n", " outVal = indent + upstreamPathM + '[' + upstreamPath + '] --> ' + outConnectionString\n", " if outVal not in edgeOutput:\n", " edgeOutput.add(outVal)\n", "\n", " updateGraphDictionaryPath(graphElementPath, upstreamOutputPath, subgraphs)\n", "\n", " # The upstream output is the upstream path instead of the node.\n", " upstreamPath = upstreamOutputPath\n", "\n", " inputConnectionString = ''\n", " if connectingElem:\n", " inputConnectionString = ' --\".' + connectingElem.getName() + '\"--> '\n", " else:\n", " inputConnectionString = ' --> '\n", "\n", " # Sanitize names for Mermaid output\n", " upstreamPathM = mx.createValidName(upstreamPath)\n", " downstreamPathM = mx.createValidName(downstreamPath)\n", "\n", " # Print out information about the edge with an \"arrow\" to show direction\n", " # of data flow \n", " outVal = indent + upstreamPathM + '[' + upstreamPath + ']' + inputConnectionString + downstreamPathM + '[' + downstreamPath + ']'\n", " if outVal not in edgeOutput:\n", " edgeOutput.add(outVal)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "The following code is the same as the previous example, except additional logic to call into the interface utilities." ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "```mermaid\n", " graph TB;\n", " NG_marble1_out[NG_marble1/out] --\".base_color\"--> SR_marble1[SR_marble1]\n", " NG_marble1_noise_octaves([noise_octaves]) --\".octaves\"--> NG_marble1_noise\n", " NG_marble1_noise[NG_marble1/noise] --\".in1\"--> NG_marble1_scale_noise[NG_marble1/scale_noise]\n", " NG_marble1_sin[NG_marble1/sin] --\".in1\"--> NG_marble1_scale[NG_marble1/scale]\n", " NG_marble1_sum[NG_marble1/sum] --\".in\"--> NG_marble1_sin[NG_marble1/sin]\n", " NG_marble1_bias[NG_marble1/bias] --\".in1\"--> NG_marble1_power[NG_marble1/power]\n", " NG_marble1_add_xyz[NG_marble1/add_xyz] --\".in1\"--> NG_marble1_scale_xyz[NG_marble1/scale_xyz]\n", " NG_marble1_scale_pos[NG_marble1/scale_pos] --\".position\"--> NG_marble1_noise[NG_marble1/noise]\n", " NG_marble1_scale_noise[NG_marble1/scale_noise] --\".in2\"--> NG_marble1_sum[NG_marble1/sum]\n", " NG_marble1_power[NG_marble1/power] --\".mix\"--> NG_marble1_color_mix[NG_marble1/color_mix]\n", " NG_marble1_noise_power([noise_power]) --\".in2\"--> NG_marble1_power\n", " NG_marble1_noise_scale_2([noise_scale_2]) --\".in2\"--> NG_marble1_scale_pos\n", " NG_marble1_base_color_2([base_color_2]) --\".fg\"--> NG_marble1_color_mix\n", " NG_marble1_obj_pos[NG_marble1/obj_pos] --\".in1\"--> NG_marble1_scale_pos[NG_marble1/scale_pos]\n", " NG_marble1_base_color_1([base_color_1]) --\".bg\"--> NG_marble1_color_mix\n", " NG_marble1_scale_xyz[NG_marble1/scale_xyz] --\".in1\"--> NG_marble1_sum[NG_marble1/sum]\n", " NG_marble1_noise_scale_1([noise_scale_1]) --\".in2\"--> NG_marble1_scale_xyz\n", " NG_marble1_obj_pos[NG_marble1/obj_pos] --\".in1\"--> NG_marble1_add_xyz[NG_marble1/add_xyz]\n", " NG_marble1_color_mix[NG_marble1/color_mix] --> NG_marble1_out[out]\n", " SR_marble1[SR_marble1] --\".surfaceshader\"--> Marble_3D[Marble_3D]\n", " NG_marble1_out[NG_marble1/out] --\".subsurface_color\"--> SR_marble1[SR_marble1]\n", " NG_marble1_scale[NG_marble1/scale] --\".in1\"--> NG_marble1_bias[NG_marble1/bias]\n", "subgraph NG_marble1:\n", " NG_marble1_obj_pos\n", " NG_marble1_noise_power\n", " NG_marble1_power\n", " NG_marble1_base_color_1\n", " NG_marble1_scale_xyz\n", " NG_marble1_sin\n", " NG_marble1_noise_scale_2\n", " NG_marble1_noise\n", " NG_marble1_scale_pos\n", " NG_marble1_base_color_2\n", " NG_marble1_sum\n", " NG_marble1_noise_scale_1\n", " NG_marble1_scale_noise\n", " NG_marble1_color_mix\n", " NG_marble1_add_xyz\n", " NG_marble1_scale\n", " NG_marble1_bias\n", " NG_marble1_noise_octaves\n", " NG_marble1_out\n", "end\n", "```\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "def generateMermaidGraph(roots, orientation):\n", " \"\"\"\n", " Output a Mermaid graph diagram given a set of root nodes\n", " \"\"\" \n", " subgraphs = {}\n", " processedEdges = set()\n", "\n", " # Find all edges, and build up the GraphElement dictionary\n", " for root in roots:\n", " for edge in root.traverseGraph():\n", " if not findEdge(edge,processedEdges):\n", " processedEdges.add(edge)\n", " updateGraphDictionary(edge, subgraphs)\n", "\n", " # Get string output for each edge in Mermaid format\n", " edgeOutput = set()\n", " for edge in processedEdges:\n", " outVal = emitMermaidEdge(' ', edge, subgraphs, edgeOutput)\n", " if outVal not in edgeOutput:\n", " edgeOutput.add(outVal)\n", "\n", " # Include interface input edges\n", " for edge in processedEdges:\n", " emitInterfaceInputs(' ', edge, subgraphs, edgeOutput) \n", "\n", " # Print graph header, edges, and sub-graphs\n", " outputGraph = []\n", " outputGraph.append(' graph %s;' % orientation)\n", " for outVal in edgeOutput:\n", " outputGraph.append(outVal)\n", " for line in emitMermaidSubgraphs(subgraphs):\n", " outputGraph.append(line)\n", "\n", " return outputGraph\n", "\n", "from IPython.display import display_markdown\n", "graph = generateMermaidGraph(roots, 'TB')\n", "strgraph = '```mermaid\\n'\n", "for line in graph:\n", " if line:\n", " strgraph = strgraph + line + '\\n'\n", "strgraph = strgraph + '```\\n' \n", "display_markdown(strgraph, raw=True)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "The resulting diagram looks like this:\n", "\n", "\n", "\n", "with the same graph as seen in the graph editor:\n", "\n", "\n" ] } ], "metadata": { "kernelspec": { "display_name": ".venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.10" }, "orig_nbformat": 4, "vscode": { "interpreter": { "hash": "5a1e4ae72c28e5dda2aa2f0312b2bc3d95bc8d852de19efe605de92732ab19fa" } } }, "nbformat": 4, "nbformat_minor": 2 }