private static WeightedDirectedGraphAcyclic <T> CreateInner( WeightedDirectedGraph <T> digraph, GraphDepthInfo depthInfo) { // Create acyclic digraph. var acyclicDigraph = DirectedGraphAcyclicBuilderUtils.CreateDirectedGraphAcyclic( digraph, depthInfo, out int[] _, out int[] connectionIndexMap); // Copy weights into a new array and into their correct position. T[] genomeWeightArr = digraph.WeightArray; T[] weightArr = new T[genomeWeightArr.Length]; for (int i = 0; i < weightArr.Length; i++) { weightArr[i] = genomeWeightArr[connectionIndexMap[i]]; } // Construct a new WeightedDirectedGraphAcyclic. return(new WeightedDirectedGraphAcyclic <T>( acyclicDigraph.InputCount, acyclicDigraph.OutputCount, acyclicDigraph.TotalNodeCount, acyclicDigraph.ConnectionIdArrays, acyclicDigraph.LayerArray, acyclicDigraph.OutputNodeIdxArr, weightArr)); }
/// <summary> /// Create from the provided <see cref="WeightedDirectedGraph{T}"/> and associated depth info. /// </summary> /// <param name="digraph">The directed graph.</param> /// <param name="depthInfo">Depth info associated with <paramref name="digraph"/>.</param> /// <returns>A new instance of <see cref="WeightedDirectedGraphAcyclic{T}"/>.</returns> /// <remarks> /// The provided graph is expected to describe an acyclic graph; this method asserts that is the case and builds /// a formal acyclic graph representation. /// </remarks> public static WeightedDirectedGraphAcyclic <T> Create( WeightedDirectedGraph <T> digraph, GraphDepthInfo depthInfo) { // Assert that the passed in depth info is correct. // Note. This test is expensive because it invokes a graph traversal algorithm to determine node depths. // ENHANCEMENT: Use a re-usable instance of AcyclicGraphDepthAnalysis. Debug.Assert(depthInfo.Equals(new AcyclicGraphDepthAnalysis().CalculateNodeDepths(digraph))); return(CreateInner(digraph, depthInfo)); }
public static DirectedGraphAcyclic CreateDirectedGraphAcyclic( DirectedGraph digraph, GraphDepthInfo depthInfo, out int[] newIdByOldId, out int[] connectionIndexMap) { // Timsort working arrays. We only need the variable slot to pass as reference, timsort will allocate them if // necessary and return them, but here we just discard those arrays. To re-use the arrays call the method overload // that accepts the two arrays. int[]? timsortWorkArr = null; int[]? timsortWorkVArr = null; return(CreateDirectedGraphAcyclic( digraph, depthInfo, out newIdByOldId, out connectionIndexMap, ref timsortWorkArr, ref timsortWorkVArr)); }
public static DirectedGraphAcyclic CreateDirectedGraphAcyclic( DirectedGraph digraph, GraphDepthInfo depthInfo, out int[] newIdByOldId, out int[] connectionIndexMap, ref int[]?timsortWorkArr, ref int[]?timsortWorkVArr) { int inputCount = digraph.InputCount; int outputCount = digraph.OutputCount; // Assert that all input nodes are at depth zero. // Any input node with a non-zero depth must have an input connection, and this is not supported. Debug.Assert(SpanUtils.Equals(depthInfo._nodeDepthArr.AsSpan(0, inputCount), 0)); // Compile a mapping from current node IDs to new IDs (based on node depth in the graph). newIdByOldId = CompileNodeIdMap(depthInfo, digraph.TotalNodeCount, inputCount, ref timsortWorkArr, ref timsortWorkVArr); // Map the connection node IDs. ConnectionIdArrays connIdArrays = digraph.ConnectionIdArrays; MapIds(connIdArrays, newIdByOldId); // Init connection index map. int connCount = connIdArrays.Length; connectionIndexMap = new int[connCount]; for (int i = 0; i < connCount; i++) { connectionIndexMap[i] = i; } // Sort the connections based on sourceID, targetId; this will arrange the connections based on the depth // of the source nodes. // Note. This sort routine will also sort a secondary array, i.e. keep the items in both arrays aligned; // here we use this to create connectionIndexMap. ConnectionSorter <int> .Sort(connIdArrays, connectionIndexMap); // Make a copy of the sub-range of newIdMap that represents the output nodes. // This is required later to be able to locate the output nodes now that they have been sorted by depth. int[] outputNodeIdxArr = new int[outputCount]; Array.Copy(newIdByOldId, inputCount, outputNodeIdxArr, 0, outputCount); // Create an array of LayerInfo(s). // Each LayerInfo contains the index + 1 of both the last node and last connection in that layer. // // The array is in order of depth, from layer zero (inputs nodes) to the last layer (usually output nodes, // but not necessarily if there is a dead end pathway with a high number of hops). // // Note. There is guaranteed to be at least one connection with a source at a given depth level, this is // because for there to be a layer N there must necessarily be a connection from a node in layer N-1 // to a node in layer N. int graphDepth = depthInfo._graphDepth; LayerInfo[] layerInfoArr = new LayerInfo[graphDepth]; // Note. Scanning over nodes can start at inputCount instead of zero, because all nodes prior to that index // are input nodes and are therefore at depth zero. (input nodes are never the target of a connection, // therefore are always guaranteed to be at the start of a connectivity graph, and thus at depth zero). int nodeCount = digraph.TotalNodeCount; int nodeIdx = inputCount; int connIdx = 0; int[] nodeDepthArr = depthInfo._nodeDepthArr; int[] srcIdArr = connIdArrays._sourceIdArr; for (int currDepth = 0; currDepth < graphDepth; currDepth++) { // Scan for last node at the current depth. for (; nodeIdx < nodeCount && nodeDepthArr[nodeIdx] == currDepth; nodeIdx++) { ; } // Scan for last connection at the current depth. for (; connIdx < srcIdArr.Length && nodeDepthArr[srcIdArr[connIdx]] == currDepth; connIdx++) { ; } // Store node and connection end indexes for the layer. layerInfoArr[currDepth] = new LayerInfo(nodeIdx, connIdx); } // Construct and return. return(new DirectedGraphAcyclic( inputCount, outputCount, nodeCount, connIdArrays, layerInfoArr, outputNodeIdxArr)); }
private static int[] CompileNodeIdMap( GraphDepthInfo depthInfo, int nodeCount, int inputCount, ref int[]?timsortWorkArr, ref int[]?timsortWorkVArr) { // Create an array of all node IDs in the digraph. int[] nodeIdArr = new int[nodeCount]; for (int i = 0; i < nodeCount; i++) { nodeIdArr[i] = i; } // Sort nodeIdArr based on the depth of the nodes. // Notes. // We skip the input nodes because these all have depth zero and therefore remain at fixed // positions. // // The remaining nodes (output and hidden nodes) are sorted by depth, noting that typically // there will be multiple nodes at a given depth. Here we apply the TimSort algorithm; this // has very good performance when there are pre-sorted sub-spans, either in the correct // direction or in reverse, as is typical of much real world data, and is likely the case here // too. // // Timsort also performs a stable sort, as it is based on a mergesort (note. at time of writing // Array.Sort employs introsort, which is not stable), thus avoids unnecessary shuffling of nodes // that are at the same depth. However the use of a stable sort is not a strict requirement here. // // Regarding timsort temporary working data. // Depending on the data being sorted, timsort may use a temp array with up to N/2 elements. Here // we ensure that the maximum possible size is allocated, and we re-use these arrays in future // calls. If instead we pass null or an array that is too short, then timsort will allocate a new // array internally, per sort, so we want to avoid that cost. // ENHANCEMENT: Modify TimSort class to accept working arrays by ref, so that if a larger array was allocated internally, we receive it back here. // Thus we achieve the same functionality without requiring knowledge of TimSort's internal logic. // Allocate new timsort working arrays, if necessary. int timsortWorkArrLength = nodeCount >> 1; if (timsortWorkArr is null || timsortWorkArr.Length < timsortWorkArrLength) { timsortWorkArr = new int[timsortWorkArrLength]; timsortWorkVArr = new int[timsortWorkArrLength]; } // Sort the node IDs by depth. TimSort <int, int> .Sort( depthInfo._nodeDepthArr, nodeIdArr, inputCount, nodeCount - inputCount, timsortWorkArr, timsortWorkVArr); // Each node is now assigned a new node ID based on its index in nodeIdArr, i.e. we are re-allocating IDs based on node depth. // ENHANCEMENT: This mapping inversion is avoidable if the consumer of the mapping is modified to consume the 'old index to new index' mapping. int[] newIdByOldId = new int[nodeCount]; for (int i = 0; i < nodeCount; i++) { newIdByOldId[nodeIdArr[i]] = i; } return(newIdByOldId); }