public CandidateSolution <T, S> FindBestSolution()
        {
            // keep track of best overall, average fitness scores
            float bestFitnessScoreAllTime = (IsLowerFitnessBetter ? float.MaxValue : float.MinValue);
            float bestAverageFitnessScore = (IsLowerFitnessBetter ? float.MaxValue : float.MinValue);
            CandidateSolution <T, S> bestTreeAllTime = null;
            int bestSolutionGenerationNumber = 0, bestAverageFitnessGenerationNumber = 0;

            // elitism
            int numElitesToAdd = (int)(ElitismRate * PopulationSize);

            // depending on whether elitism is used, or the selection type, we may need to sort candidates by fitness (which is slower)
            bool needToSortByFitness =
                SelectionStyle == SelectionStyle.RouletteWheel ||
                SelectionStyle == SelectionStyle.Ranked ||
                ElitismRate > 0;

            Stopwatch timer = new Stopwatch();

            // create an initial population of random trees, passing the possible functions, consts, and variables
            for (int p = 0; p < PopulationSize; p++)
            {
                CandidateSolution <T, S> tree = new CandidateSolution <T, S>(primitiveSet, RandomTreeMinDepth, RandomTreeMaxDepth);
                tree.CreateRandom();
                currentGeneration.Add(tree);
            }

            // loop over generations
            int currentGenerationNumber = 0;

            while (true)
            {
                timer.Restart();

                // for each tree, find and store the fitness score
                // multithread the fitness evaluation
                Parallel.ForEach(currentGeneration, (candidate) =>
                {
                    // calc the fitness by calling the user-supplied function via the delegate
                    float fitness     = myFitnessFunction(candidate);
                    candidate.Fitness = fitness;
                });

                // single-threaded approach, for debugging
                //foreach (var candidate in currentGeneration)
                //    candidate.Fitness = myFitnessFunction(candidate);

                // now check if we have a new best
                float bestFitnessScoreThisGeneration            = (IsLowerFitnessBetter ? float.MaxValue : float.MinValue);
                CandidateSolution <T, S> bestTreeThisGeneration = null;
                float totalFitness = 0;
                foreach (var tree in currentGeneration)
                {
                    totalFitness += tree.Fitness;

                    // find best of this generation, update best all-time if needed
                    bool isBestThisGeneration = false;
                    if (IsLowerFitnessBetter)
                    {
                        isBestThisGeneration = tree.Fitness < bestFitnessScoreThisGeneration;
                    }
                    else
                    {
                        isBestThisGeneration = tree.Fitness > bestFitnessScoreThisGeneration;
                    }

                    if (isBestThisGeneration)
                    {
                        bestFitnessScoreThisGeneration = tree.Fitness;
                        bestTreeThisGeneration         = tree;

                        bool isBestEver = false;
                        if (IsLowerFitnessBetter)
                        {
                            isBestEver = bestFitnessScoreThisGeneration < bestFitnessScoreAllTime;
                        }
                        else
                        {
                            isBestEver = bestFitnessScoreThisGeneration > bestFitnessScoreAllTime;
                        }

                        if (isBestEver)
                        {
                            bestFitnessScoreAllTime      = bestFitnessScoreThisGeneration;
                            bestTreeAllTime              = tree.Clone();
                            bestSolutionGenerationNumber = currentGenerationNumber;
                        }
                    }
                }

                // determine average fitness and store if it's all-time best
                float averageFitness = totalFitness / PopulationSize;
                if (IsLowerFitnessBetter)
                {
                    if (averageFitness < bestAverageFitnessScore)
                    {
                        bestAverageFitnessGenerationNumber = currentGenerationNumber;
                        bestAverageFitnessScore            = averageFitness;
                    }
                }
                else
                {
                    if (averageFitness > bestAverageFitnessScore)
                    {
                        bestAverageFitnessGenerationNumber = currentGenerationNumber;
                        bestAverageFitnessScore            = averageFitness;
                    }
                }

                // report progress back to the user, and allow them to terminate the loop
                EngineProgress progress = new EngineProgress()
                {
                    GenerationNumber   = currentGenerationNumber,
                    AvgFitnessThisGen  = averageFitness,
                    BestFitnessThisGen = bestFitnessScoreThisGeneration,
                    BestFitnessSoFar   = bestFitnessScoreAllTime,
                    TimeForGeneration  = timer.Elapsed
                };

                // there are two forms here, and if they aren't defined, this code just drops through
                bool keepGoing = true;
                if (myProgressFunction != null)
                {
                    myProgressFunction(progress);
                }
                if (myProgressWithCandidateFunction != null)
                {
                    keepGoing = myProgressWithCandidateFunction(progress, bestTreeThisGeneration);
                }
                if (!keepGoing)
                {
                    break;              // user signalled to end looping
                }
                // termination conditions
                if (currentGenerationNumber >= MinGenerations)
                {
                    // exit the loop if we're not making any progress in our average fitness score or our overall best score
                    if (((currentGenerationNumber - bestAverageFitnessGenerationNumber) >= StagnantGenerationLimit) &&
                        ((currentGenerationNumber - bestSolutionGenerationNumber) >= StagnantGenerationLimit))
                    {
                        break;
                    }

                    // maxed out?
                    if (currentGenerationNumber >= MaxGenerations)
                    {
                        break;
                    }
                }

                // we may need to sort the current generation by fitness, depending on SelectionStyle
                if (needToSortByFitness)
                {
                    if (IsLowerFitnessBetter)
                    {
                        currentGeneration = currentGeneration.OrderBy(c => c.Fitness).ToList();
                    }
                    else
                    {
                        currentGeneration = currentGeneration.OrderByDescending(c => c.Fitness).ToList();
                    }
                }

                // depending on the SelectionStyle, we may need to adjust all candidate's fitness scores
                AdjustFitnessScores(currentGeneration);

                // Start building the next generation
                List <CandidateSolution <T, S> > nextGeneration = new List <CandidateSolution <T, S> >();

                // Elitism
                var theBest = currentGeneration.Take(numElitesToAdd);
                foreach (var peakPerformer in theBest)
                {
                    nextGeneration.Add(peakPerformer);
                }

                // now create a new generation using fitness scores for selection, and crossover and mutation
                while (nextGeneration.Count < PopulationSize)
                {
                    // select parents
                    CandidateSolution <T, S> parent1 = null, parent2 = null;
                    switch (SelectionStyle)
                    {
                    case SelectionStyle.Tourney:
                        parent1 = TournamentSelectParent();
                        parent2 = TournamentSelectParent();
                        break;

                    case SelectionStyle.RouletteWheel:
                    case SelectionStyle.Ranked:
                        parent1 = RouletteSelectParent();
                        parent2 = RouletteSelectParent();
                        break;
                    }

                    // cross them over to generate two new children
                    CandidateSolution <T, S> child1, child2;
                    CrossOverParents(parent1, parent2, out child1, out child2);

                    // Mutation
                    if (Randomizer.GetFloatFromZeroToOne() < MutationRate)
                    {
                        child1.Mutate();
                    }
                    if (Randomizer.GetFloatFromZeroToOne() < MutationRate)
                    {
                        child2.Mutate();
                    }

                    // then add to the new generation
                    nextGeneration.Add(child1);
                    nextGeneration.Add(child2);
                }

                // move to the next generation
                currentGeneration = nextGeneration;
                currentGenerationNumber++;
            }

            bestTreeAllTime.Root.SetCandidateRef(bestTreeAllTime);
            return(bestTreeAllTime);
        }