public void ConnectThroughInput() { // Simple acyclic graph. var connList = new List <DirectedConnection> { new DirectedConnection(0, 3), new DirectedConnection(1, 3), new DirectedConnection(2, 3), new DirectedConnection(2, 4), new DirectedConnection(4, 1) }; // Create graph. connList.Sort(); var digraph = DirectedGraphBuilder.Create(connList, 3, 2); // Assert is acyclic. var cyclicGraphAnalysis = new CyclicGraphAnalysis(); Assert.IsTrue(!cyclicGraphAnalysis.IsCyclic(digraph)); // Depth analysis. GraphDepthInfo depthInfo = AcyclicGraphDepthAnalysis.CalculateNodeDepths(digraph); // Assertions. Assert.AreEqual(4, depthInfo._networkDepth); Assert.AreEqual(5, depthInfo._nodeDepthArr.Length); // Node depths. Assert.AreEqual(0, depthInfo._nodeDepthArr[0]); Assert.AreEqual(2, depthInfo._nodeDepthArr[1]); Assert.AreEqual(0, depthInfo._nodeDepthArr[2]); Assert.AreEqual(3, depthInfo._nodeDepthArr[3]); Assert.AreEqual(1, depthInfo._nodeDepthArr[4]); }
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.ConnectionIds, acyclicDigraph.LayerArray, acyclicDigraph.OutputNodeIdxArr, weightArr)); }
/// <summary> /// Create a NeatGenome with the given meta data, connection genes and supplementary data. /// </summary> /// <param name="id">Genome ID.</param> /// <param name="birthGeneration">Birth generation.</param> /// <param name="connGenes">Connection genes.</param> /// <param name="hiddenNodeIdArr">An array of the hidden node IDs in the genome, in ascending order.</param> /// <returns>A new NeatGenome instance.</returns> public NeatGenome <T> Create( int id, int birthGeneration, ConnectionGenes <T> connGenes, int[] hiddenNodeIdArr) { int inputCount = _metaNeatGenome.InputNodeCount; int outputCount = _metaNeatGenome.OutputNodeCount; int inputOutputCount = _metaNeatGenome.InputOutputNodeCount; // Create a mapping from node IDs to node indexes. Dictionary <int, int> nodeIdxById = BuildNodeIndexById(hiddenNodeIdArr); // Create a DictionaryNodeIdMap. DictionaryNodeIdMap nodeIndexByIdMap = new DictionaryNodeIdMap(inputCount, nodeIdxById); // Create a digraph from the genome. DirectedGraph digraph = NeatGenomeBuilderUtils.CreateDirectedGraph( _metaNeatGenome, connGenes, nodeIndexByIdMap); // Calc the depth of each node in the digraph. GraphDepthInfo depthInfo = _graphDepthAnalysis.CalculateNodeDepths(digraph); // Create a weighted acyclic digraph. // Note. This also outputs connectionIndexMap. For each connection in the acyclic graph this gives // the index of the same connection in the genome; this is because connections are re-ordered based // on node depth in the acyclic graph. AcyclicDirectedGraph acyclicDigraph = AcyclicDirectedGraphBuilderUtils.CreateAcyclicDirectedGraph( digraph, depthInfo, out int[] newIdByOldId, out int[] connectionIndexMap, ref _timesortWorkArr, ref _timesortWorkVArr); // TODO: Write unit tests to cover this! // Update nodeIdxById with the new depth based node index allocations. // Notes. // The current nodeIndexByIdMap maps node IDs (also know as innovation IDs in NEAT) to a compact // ID space in which any gaps have been removed, i.e. a compacted set of IDs that can be used as indexes, // i.e. if there are N nodes in total then the highest node ID will be N-1. // // Here we map the new compact IDs to an alternative ID space that is also compact, but ensures that nodeIDs // reflect the depth of a node in the acyclic graph. UpdateNodeIndexById(nodeIdxById, hiddenNodeIdArr, newIdByOldId); // Create the neat genome. return(new NeatGenome <T>( _metaNeatGenome, id, birthGeneration, connGenes, hiddenNodeIdArr, nodeIndexByIdMap, acyclicDigraph, connectionIndexMap)); }
/// <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)); }
/// <summary> /// Creates a new directed acyclic graph instance from the provided graph structure, accompanying graph /// layer information, and node ID mappings. /// </summary> /// <param name="digraph">A directed graph structure.</param> /// <param name="depthInfo">Depth/layer information, describing what layer each node of the graph is within.</param> /// <param name="newIdByOldId">Returns a set of node ID mappings. These describe a mapping from the /// non-contiguous node ID space of <paramref name="digraph"/>, to the contiguous node ID space of the /// returned <see cref="DirectedGraphAcyclic"/> /// contiguous space.</param> /// <param name="connectionIndexMap">Returns a set of connection index mappings. The connections of /// <paramref name="digraph"/> are re-ordered based on the layer/depth of each connection's source node; /// this structure conveys the new index of each connection given its original/old index.</param> /// <returns>A new instance of <see cref="DirectedGraphAcyclic"/>.</returns> 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)); }
private static bool IsValid_Acyclic( MetaNeatGenome <T> metaNeatGenome, GraphDepthInfo depthInfo) { // DepthInfo is mandatory for acyclic graphs. if (null == depthInfo) { return(false); } // Test 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. if (!ArrayUtils.Equals(depthInfo._nodeDepthArr, 0, 0, metaNeatGenome.InputNodeCount)) { return(false); } // TODO: More acyclic graph validation. return(true); }
/// <summary> /// Create an array that gives the node layer for each node, keyed by node index. /// </summary> /// <param name="digraph">The directed cyclic graph.</param> /// <returns>A new integer array.</returns> private static int[] BuildNodeLayerByIdx_Cyclic(DirectedGraph digraph) { // Perform an analysis on the cyclic graph to assign a depth to each node. // TODO: Re-use these instances, by maintaining a pool of them that can be 'rented from' and 'returned to' the pool. CyclicGraphDepthAnalysis cyclicDepthAnalysis = new(); GraphDepthInfo depthInfo = cyclicDepthAnalysis.CalculateNodeDepths(digraph); int[] nodeLayerByIdx = depthInfo._nodeDepthArr; // Move all nodes up one layer, such that layer 1 is the first/top layer; layer zero will be // used later to represent input nodes only. for (int i = digraph.InputCount; i < nodeLayerByIdx.Length; i++) { nodeLayerByIdx[i]++; } // Assign input nodes to their own layer (layer zero). for (int i = 0; i < digraph.InputCount; i++) { nodeLayerByIdx[i] = 0; } // Assign output nodes to their own layer. int outputLayerIdx = depthInfo._graphDepth + 1; for (int i = 0; i < digraph.OutputCount; i++) { // Note. For cyclic networks the output node indexes occur in a contiguous segment following the input nodes. int outputNodeIdx = digraph.InputCount + i; nodeLayerByIdx[outputNodeIdx] = outputLayerIdx; } // Remove empty layers (if any), by adjusting the depth values in nodeLayerByIdx. RemoveEmptyLayers(nodeLayerByIdx, outputLayerIdx + 1); // Return the constructed node layer lookup table. return(nodeLayerByIdx); }
public void TestAddAcyclicConnection_CumulativeAdditions() { var pop = CreateNeatPopulation(); var genomeBuilder = NeatGenomeBuilderFactory <double> .Create(pop.MetaNeatGenome); var rootGenome = pop.GenomeList[0]; var strategy = new AddAcyclicConnectionStrategy <double>( pop.MetaNeatGenome, genomeBuilder, pop.GenomeIdSeq, pop.InnovationIdSeq, pop.GenerationSeq); IRandomSource rng = RandomDefaults.CreateRandomSource(); var nodeIdSet = GetNodeIdSet(rootGenome); CyclicGraphAnalysis cyclicGraphAnalysis = new CyclicGraphAnalysis(); AcyclicGraphDepthAnalysis graphDepthAnalysis = new AcyclicGraphDepthAnalysis(); // Run the inner loop test multiple times. // Note. The add-connection mutations are random, thus each loop accumulates a different set of mutations. for (int i = 0; i < 50; i++) { var parentGenome = rootGenome; // Accumulate random mutations for some number of loops. for (int j = 0; j < 20;) { var childGenome = strategy.CreateChildGenome(rootGenome, rng); // Note. the strategy will return a null if it cannot find an acyclic connection to add; // test for this and try again. The test will be for N successful mutations rather than N attempts. if (null == childGenome) { continue; } // The child genome should have one more connection than parent. Assert.AreEqual(rootGenome.ConnectionGenes.Length + 1, childGenome.ConnectionGenes.Length); // The child genome's new connection should not be a duplicate of any of the existing/parent connections. var connSet = GetDirectedConnectionSet(rootGenome); var childConnSet = GetDirectedConnectionSet(childGenome); var newConnList = new List <DirectedConnection>(childConnSet.Except(connSet)); Assert.AreEqual(1, newConnList.Count); // The connection genes should be sorted. Assert.IsTrue(SortUtils.IsSortedAscending(childGenome.ConnectionGenes._connArr)); // The child genome should have the same set of node IDs as the parent. var childNodeIdSet = GetNodeIdSet(childGenome); Assert.IsTrue(nodeIdSet.SetEquals(childNodeIdSet)); // The child genome should describe an acyclic graph, i.e. the new connection should not have // formed a cycle in the graph. var digraph = childGenome.DirectedGraph; Assert.IsFalse(cyclicGraphAnalysis.IsCyclic(digraph)); // Run the acyclic graph depth analysis algorithm. GraphDepthInfo depthInfo = graphDepthAnalysis.CalculateNodeDepths(childGenome.DirectedGraph); // Run again with the alternative algorithm (that uses function recursion). GraphDepthInfo depthInfo2 = AcyclicGraphDepthAnalysisByRecursion.CalculateNodeDepths(childGenome.DirectedGraph); Assert.AreEqual(nodeIdSet.Count, depthInfo._nodeDepthArr.Length); Assert.AreEqual(nodeIdSet.Count, depthInfo2._nodeDepthArr.Length); TestUtils.Compare(depthInfo2._nodeDepthArr, depthInfo._nodeDepthArr); // Set the child genome to be the new parent, thus we accumulate random new connections over time. parentGenome = childGenome; // Increment for successful tests only. j++; } } }
private static void AssertAcyclicGraph( MetaNeatGenome <T> metaNeatGenome, DirectedGraph digraph, int[]?connectionIndexMap) { Debug.Assert(digraph is DirectedGraphAcyclic); Debug.Assert(connectionIndexMap is object); // Cast to an acyclic digraph. var acyclicDigraph = (DirectedGraphAcyclic)digraph; // Layer info checks. LayerInfo[] layerArr = acyclicDigraph.LayerArray; Debug.Assert(layerArr is object && layerArr.Length > 0); // Layer zero is the input layer, thus the number of nodes in this layer should be at least the number of input nodes. // Note. Any node with no incoming connections is also assigned to layer zero, therefore there can be non-input nodes in // this layer too. Debug.Assert(layerArr[0].EndNodeIdx >= metaNeatGenome.InputNodeCount); // EndNodeIdx is strictly increasing, as is EndConnectionIdx. // Note. There is always at least one node in a layer (otherwise the layer would not exist). for (int i = 1; i < layerArr.Length; i++) { Debug.Assert(layerArr[i - 1].EndNodeIdx < layerArr[i].EndNodeIdx); } // EndConnectionIdx is strictly increasing, except for the last layer which has no connections by definition (if // there was a connection it would result in one more layer!). for (int i = 1; i < layerArr.Length - 1; i++) { Debug.Assert(layerArr[i - 1].EndConnectionIdx < layerArr[i].EndConnectionIdx); } // The last layer has no connections, by definition. // Note. In principle there can be a single layer, i.e. a bunch of nodes with no connections between them; // it's nonsensical, but it's not disallowed. int lastLayerIdx = layerArr.Length - 1; if (lastLayerIdx > 0) { Debug.Assert(layerArr[lastLayerIdx].EndConnectionIdx == layerArr[lastLayerIdx - 1].EndConnectionIdx); } // Reconstruct a GraphDepthInfo from the layer info. GraphDepthInfo depthInfo = BuildGraphDepthInfo(layerArr, digraph.TotalNodeCount); int[] nodeDepthArr = depthInfo._nodeDepthArr; // Connection tests. int connIdx = 0; int[] srcIdArr = digraph.ConnectionIdArrays._sourceIdArr; int[] tgtIdArr = digraph.ConnectionIdArrays._targetIdArr; // Loop the layer infos. for (int layerIdx = 0; layerIdx < layerArr.Length; layerIdx++) { LayerInfo layerInfo = layerArr[layerIdx]; // EndNodeIdx should never be negative or higher than the total node count. Debug.Assert(layerInfo.EndNodeIdx >= 0 && layerInfo.EndNodeIdx <= digraph.TotalNodeCount); // Loop the connections in the current layer. for (; connIdx < layerInfo.EndConnectionIdx; connIdx++) { int srcId = srcIdArr[connIdx]; int tgtId = tgtIdArr[connIdx]; // The connections in the current layer should all have a source node in this layer. Debug.Assert(nodeDepthArr[srcId] == layerIdx); // The target node should normally be in a higher layer. However, layer zero is a special case because it // contains not only the input nodes, but can also contain hidden nodes that are not reachable from an input node. // // Thus, the connections in layer zero should have either: // a) an input source node in this layer, and a target node in a layer with a higher layer index, or, // b) a hidden source node in this layer, and a target node that can be in any layer, including layer zero // if the target node is also unreachable from an input node (i.e. via another connectivity path). Debug.Assert( (layerIdx == 0 && (srcId >= digraph.InputCount || nodeDepthArr[tgtId] > 0)) || (layerIdx > 0 && nodeDepthArr[tgtId] > layerIdx) ); } } }
/// <summary> /// Creates a new directed acyclic graph instance from the provided graph structure, accompanying graph /// layer information, and node ID mappings. /// </summary> /// <param name="digraph">A directed graph structure.</param> /// <param name="depthInfo">Depth/layer information, describing what layer each node of the graph is within.</param> /// <param name="newIdByOldId">Returns a set of node ID mappings. These describe a mapping from the /// non-contiguous node ID space of <paramref name="digraph"/>, to the contiguous node ID space of the /// returned <see cref="DirectedGraphAcyclic"/> /// contiguous space.</param> /// <param name="connectionIndexMap">Returns a set of connection index mappings. The connections of /// <paramref name="digraph"/> are re-ordered based on the layer/depth of each connection's source node; /// this structure conveys the new index of each connection given its original/old index.</param> /// <param name="timsortWorkArr">A re-usable working array for use as temporary storage by the timsort algorithm.</param> /// <param name="timsortWorkVArr">A secondary re-usable working array for use as temporary storage by the timsort algorithm.</param> /// <returns>A new instance of <see cref="DirectedGraphAcyclic"/>.</returns> 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.Equal(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. ConnectionIds connIds = digraph.ConnectionIds; MapIds(connIds, newIdByOldId); // Init connection index map. int connCount = connIds.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(connIds, connectionIndexMap); // Make a copy of the sub-range of newIdByOldId 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]; newIdByOldId.AsSpan(inputCount, outputCount).CopyTo(outputNodeIdxArr); // 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; ReadOnlySpan <int> srcIds = connIds.GetSourceIdSpan(); 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 < srcIds.Length && nodeDepthArr[srcIds[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, connIds, 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 = ArrayPool <int> .Shared.Rent(nodeCount); try { var nodeIds = nodeIdArr.AsSpan(0, nodeCount); for (int i = 0; i < nodeCount; i++) { nodeIds[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. // 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.AsSpan(inputCount), nodeIds.Slice(inputCount), ref timsortWorkArr, ref 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[nodeIds[i]] = i; } return(newIdByOldId); } finally { ArrayPool <int> .Shared.Return(nodeIdArr); } }
private static void AssertAcyclicGraph( MetaNeatGenome <T> metaNeatGenome, DirectedGraph digraph, int[] connectionIndexMap) { Debug.Assert(digraph is AcyclicDirectedGraph); Debug.Assert(null != connectionIndexMap); // Cast to an acyclic digraph. var acyclicDigraph = (AcyclicDirectedGraph)digraph; // Layer info checks. LayerInfo[] layerArr = acyclicDigraph.LayerArray; Debug.Assert(null != layerArr && layerArr.Length > 0); // Layer 0 is the input layer, thus the number of nodes in this layer should be at least the number of input nodes. // Note. Any node with no incoming connections is assigned to layer 0, therefore there can be non input nodes in // this layer too. Debug.Assert(layerArr[0].EndNodeIdx >= metaNeatGenome.InputNodeCount); // EndNodeIdx is strictly increasing, as is EndConnectionIdx. // Note. There is always at least one node in a layer (otherwise the layer would not exist). for (int i = 1; i < layerArr.Length; i++) { Debug.Assert(layerArr[i - 1].EndNodeIdx < layerArr[i].EndNodeIdx); } // EndConnectionIdx is strictly increasing, except for the last layer which has no connections by definition (if // there was a connection it would result in one more layer!). for (int i = 1; i < layerArr.Length - 1; i++) { Debug.Assert(layerArr[i - 1].EndConnectionIdx < layerArr[i].EndConnectionIdx); } // The last layer has no connections, by definition. // Note. In principle there can be a single layer, i.e. a bunch of nodes with no connections between them; // it's nonsensical, but it's not disallowed. int lastLayerIdx = layerArr.Length - 1; if (lastLayerIdx > 0) { Debug.Assert(layerArr[lastLayerIdx].EndConnectionIdx == layerArr[lastLayerIdx - 1].EndConnectionIdx); } // Reconstruct a GraphDepthInfo from the layer info. GraphDepthInfo depthInfo = BuildGraphDepthInfo(layerArr, digraph.TotalNodeCount); int[] nodeDepthArr = depthInfo._nodeDepthArr; // Connection tests. int connIdx = 0; int[] srcIdArr = digraph.ConnectionIdArrays._sourceIdArr; int[] tgtIdArr = digraph.ConnectionIdArrays._targetIdArr; // Loop the layer infos. for (int layerIdx = 0; layerIdx < layerArr.Length; layerIdx++) { LayerInfo layerInfo = layerArr[layerIdx]; // EndNodeIdx should never be negative or higher than the total node count. Debug.Assert(layerInfo.EndNodeIdx >= 0 && layerInfo.EndNodeIdx <= digraph.TotalNodeCount); // The connections in this layer should all have a source node in this layer, and a target node in a layer // with a higher layer index. for (; connIdx < layerInfo.EndConnectionIdx; connIdx++) { int srcId = srcIdArr[connIdx]; int tgtId = tgtIdArr[connIdx]; Debug.Assert(nodeDepthArr[srcId] == layerIdx); Debug.Assert(nodeDepthArr[tgtId] > layerIdx); } } }