Beispiel #1
0
        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]);
        }
Beispiel #2
0
    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));
        }
Beispiel #4
0
    /// <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));
    }
Beispiel #5
0
    /// <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);
        }
Beispiel #7
0
    /// <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++;
                }
            }
        }
Beispiel #9
0
        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)
                        );
                }
            }
        }
Beispiel #10
0
    /// <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));
    }
Beispiel #11
0
    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);
                }
            }
        }