Example #1
0
        private void WriteSingleDistributionSummary(HeuristicSolutionDistribution distribution, TimeSpan elapsedTime)
        {
            Heuristic?bestHeuristic = distribution.HighestSolution;

            if (bestHeuristic == null)
            {
                throw new ArgumentOutOfRangeException(nameof(distribution));
            }

            int   movesAccepted             = 1;
            int   movesRejected             = 0;
            float previousObjectiveFunction = bestHeuristic.AcceptedObjectiveFunctionByMove[0];

            for (int index = 1; index < bestHeuristic.AcceptedObjectiveFunctionByMove.Count; ++index)
            {
                float currentObjectiveFunction = bestHeuristic.AcceptedObjectiveFunctionByMove[index];
                if (currentObjectiveFunction != previousObjectiveFunction)
                {
                    ++movesAccepted;
                }
                else
                {
                    ++movesRejected;
                }
                previousObjectiveFunction = currentObjectiveFunction;
            }

            float maximumHarvest      = Single.MinValue;
            float minimumHarvest      = Single.MaxValue;
            float harvestSum          = 0.0F;
            float harvestSumOfSquares = 0.0F;

            for (int periodIndex = 1; periodIndex < bestHeuristic.BestTrajectory.PlanningPeriods; ++periodIndex)
            {
                float harvestVolumeScribner = bestHeuristic.BestTrajectory.ThinningVolume.ScribnerTotal[periodIndex];
                maximumHarvest       = Math.Max(harvestVolumeScribner, maximumHarvest);
                harvestSum          += harvestVolumeScribner;
                harvestSumOfSquares += harvestVolumeScribner * harvestVolumeScribner;
                minimumHarvest       = Math.Min(harvestVolumeScribner, minimumHarvest);
            }
            float periods           = (float)(bestHeuristic.BestTrajectory.PlanningPeriods - 1);
            float meanHarvest       = harvestSum / periods;
            float variance          = harvestSumOfSquares / periods - meanHarvest * meanHarvest;
            float standardDeviation = MathF.Sqrt(variance);
            float flowEvenness      = Math.Max(maximumHarvest - meanHarvest, meanHarvest - minimumHarvest) / meanHarvest;

            base.WriteVerbose(String.Empty); // Visual Studio code workaround
            int totalMoves = movesAccepted + movesRejected;

            this.WriteVerbose("{0}: {1} moves, {2} changing ({3:0%}), {4} unchanging ({5:0%})", bestHeuristic.GetName(), totalMoves, movesAccepted, (float)movesAccepted / (float)totalMoves, movesRejected, (float)movesRejected / (float)totalMoves);
            this.WriteVerbose("objective: best {0:0.00#}, mean {1:0.00#} ending {2:0.00#}.", bestHeuristic.BestObjectiveFunction, distribution.BestObjectiveFunctionBySolution.Average(), bestHeuristic.AcceptedObjectiveFunctionByMove.Last());
            this.WriteVerbose("flow: {0:0.0#} mean, {1:0.000} σ, {2:0.000}% even, {3:0.0#}-{4:0.0#} = range {5:0.0}.", meanHarvest, standardDeviation, 1E2 * flowEvenness, minimumHarvest, maximumHarvest, maximumHarvest - minimumHarvest);

            double iterationsPerSecond           = distribution.TotalMoves / distribution.TotalCoreSeconds.TotalSeconds;
            double iterationsPerSecondMultiplier = iterationsPerSecond > 1E3 ? 1E-3 : 1.0;
            string iterationsPerSecondScale      = iterationsPerSecond > 1E3 ? "k" : String.Empty;

            this.WriteVerbose("{0} iterations in {1:0.000} core-s and {2:0.000}s clock time ({3:0.00} {4}iterations/core-s).", distribution.TotalMoves, distribution.TotalCoreSeconds.TotalSeconds, elapsedTime.TotalSeconds, iterationsPerSecondMultiplier * iterationsPerSecond, iterationsPerSecondScale);
        }
Example #2
0
        protected override void ProcessRecord()
        {
            if (this.Runs !.Count < 1)
            {
                throw new ArgumentOutOfRangeException(nameof(this.Runs));
            }

            using StreamWriter writer = this.GetWriter();

            StringBuilder line = new StringBuilder();

            if (this.ShouldWriteHeader())
            {
                HeuristicParameters?highestParameters = this.Runs[0].HighestHeuristicParameters;
                if (highestParameters == null)
                {
                    throw new NotSupportedException("Cannot generate header because first run is missing highest solution parameters.");
                }

                line.Append("stand,heuristic," + highestParameters.GetCsvHeader() + ",thin age,rotation,solution,objective,runtime");
                writer.WriteLine(line);
            }

            for (int runIndex = 0; runIndex < this.Runs.Count; ++runIndex)
            {
                HeuristicSolutionDistribution distribution = this.Runs[runIndex];
                Heuristic?highestHeuristic = distribution.HighestSolution;
                if ((highestHeuristic == null) || (distribution.HighestHeuristicParameters == null))
                {
                    throw new NotSupportedException("Run " + runIndex + " is missing a highest solution or highest solution parameters.");
                }
                OrganonStandTrajectory highestTrajectory = highestHeuristic.BestTrajectory;
                string linePrefix = highestTrajectory.Name + "," + highestHeuristic.GetName() + "," + distribution.HighestHeuristicParameters.GetCsvValues() + "," + highestTrajectory.GetFirstHarvestAge() + "," + highestTrajectory.GetRotationLength();

                List <float> bestSolutions = distribution.BestObjectiveFunctionBySolution;
                for (int solutionIndex = 0; solutionIndex < bestSolutions.Count; ++solutionIndex)
                {
                    line.Clear();

                    string objectiveFunction = bestSolutions[solutionIndex].ToString(CultureInfo.InvariantCulture);
                    string runtime           = distribution.RuntimeBySolution[solutionIndex].TotalSeconds.ToString("0.000", CultureInfo.InvariantCulture);
                    line.Append(linePrefix + "," + solutionIndex + "," + objectiveFunction + "," + runtime);
                    writer.WriteLine(line);
                }
            }
        }
Example #3
0
        protected override void ProcessRecord()
        {
            if (this.Runs !.Count < 1)
            {
                throw new ArgumentOutOfRangeException(nameof(this.Runs));
            }

            using StreamWriter writer = this.GetWriter();

            // for now, perform no reduction when Object.ReferenceEquals(lowestSolution, highestSolution) is true
            StringBuilder line = new StringBuilder();

            if (this.ShouldWriteHeader())
            {
                if ((this.Runs[0].HighestSolution == null) || (this.Runs[0].HighestHeuristicParameters == null) || (this.Runs[0].LowestSolution == null))
                {
                    throw new NotSupportedException("Cannot generate header because first run is missing a highest solution, lowest solution, or highest solution parameters.");
                }

                line.Append("stand,heuristic,thin age,rotation," + this.Runs[0].HighestHeuristicParameters !.GetCsvHeader() + ",iteration,count");

                string            lowestMoveLogHeader = "lowest move log";
                IHeuristicMoveLog?lowestMoveLog       = this.Runs[0].LowestSolution !.GetMoveLog();
                if (lowestMoveLog != null)
                {
                    lowestMoveLogHeader = lowestMoveLog.GetCsvHeader("lowest ");
                }
                line.Append("," + lowestMoveLogHeader);

                line.Append(",lowest,lowest candidate,min,percentile 2.5,percentile 5,lower quartile,median,mean,upper quartile,percentile 95,percentile 97.5,max");

                string            highestMoveLogHeader = "highest move log";
                IHeuristicMoveLog?highestMoveLog       = this.Runs[0].HighestSolution !.GetMoveLog();
                if (highestMoveLog != null)
                {
                    highestMoveLogHeader = highestMoveLog.GetCsvHeader("highest ");
                }
                line.Append("," + highestMoveLogHeader);

                line.Append(",highest,highest candidate");
                writer.WriteLine(line);
            }

            for (int runIndex = 0; runIndex < this.Runs.Count; ++runIndex)
            {
                HeuristicSolutionDistribution distribution = this.Runs[runIndex];
                Heuristic?highestHeuristic = distribution.HighestSolution;
                Heuristic?lowestHeuristic  = distribution.LowestSolution;
                if ((distribution.HighestHeuristicParameters == null) || (highestHeuristic == null) || (lowestHeuristic == null))
                {
                    throw new NotSupportedException("Solution distribution for run " + runIndex + " is missing a highest or lowest solution or highest solution parameters.");
                }
                IHeuristicMoveLog?highestMoveLog = highestHeuristic.GetMoveLog();
                IHeuristicMoveLog?lowestMoveLog  = lowestHeuristic.GetMoveLog();
                // for now, assume highest and lowest solutions used the same parameters
                OrganonStandTrajectory highestTrajectory = highestHeuristic.BestTrajectory;
                string runPrefix = highestTrajectory.Name + "," + highestHeuristic.GetName() + "," + highestTrajectory.GetFirstHarvestAge() + "," + highestTrajectory.GetRotationLength() + "," + distribution.HighestHeuristicParameters.GetCsvValues();

                Debug.Assert(distribution.CountByMove.Count >= lowestHeuristic.AcceptedObjectiveFunctionByMove.Count);
                Debug.Assert(distribution.CountByMove.Count == distribution.MinimumObjectiveFunctionByMove.Count);
                Debug.Assert(distribution.CountByMove.Count >= distribution.TwoPointFivePercentileByMove.Count);
                Debug.Assert(distribution.CountByMove.Count >= distribution.FifthPercentileByMove.Count);
                Debug.Assert(distribution.CountByMove.Count >= distribution.LowerQuartileByMove.Count);
                Debug.Assert(distribution.CountByMove.Count >= distribution.MedianObjectiveFunctionByMove.Count);
                Debug.Assert(distribution.CountByMove.Count == distribution.MeanObjectiveFunctionByMove.Count);
                Debug.Assert(distribution.CountByMove.Count >= distribution.NinetyFifthPercentileByMove.Count);
                Debug.Assert(distribution.CountByMove.Count >= distribution.NinetySevenPointFivePercentileByMove.Count);
                Debug.Assert(distribution.CountByMove.Count >= distribution.UpperQuartileByMove.Count);
                Debug.Assert(distribution.CountByMove.Count == distribution.MaximumObjectiveFunctionByMove.Count);
                Debug.Assert(distribution.CountByMove.Count >= highestHeuristic.AcceptedObjectiveFunctionByMove.Count);
                for (int moveIndex = 0; moveIndex < distribution.CountByMove.Count; moveIndex += this.Step)
                {
                    line.Clear();

                    string runsWithMoveAtIndex = distribution.CountByMove[moveIndex].ToString(CultureInfo.InvariantCulture);

                    string?lowestMove = null;
                    if ((lowestMoveLog != null) && (lowestMoveLog.Count > moveIndex))
                    {
                        lowestMove = lowestMoveLog.GetCsvValues(moveIndex);
                    }
                    string?lowestObjectiveFunction = null;
                    if (lowestHeuristic.AcceptedObjectiveFunctionByMove.Count > moveIndex)
                    {
                        lowestObjectiveFunction = lowestHeuristic.AcceptedObjectiveFunctionByMove[moveIndex].ToString(CultureInfo.InvariantCulture);
                    }
                    string?lowestObjectiveFunctionForMove = null;
                    if (lowestHeuristic.CandidateObjectiveFunctionByMove.Count > moveIndex)
                    {
                        lowestObjectiveFunctionForMove = lowestHeuristic.CandidateObjectiveFunctionByMove[moveIndex].ToString(CultureInfo.InvariantCulture);
                    }

                    string minObjectiveFunction           = distribution.MinimumObjectiveFunctionByMove[moveIndex].ToString(CultureInfo.InvariantCulture);
                    string?lowerQuartileObjectiveFunction = null;
                    if (moveIndex < distribution.LowerQuartileByMove.Count)
                    {
                        lowerQuartileObjectiveFunction = distribution.LowerQuartileByMove[moveIndex].ToString(CultureInfo.InvariantCulture);
                    }
                    string?twoPointFivePercentileObjectiveFunction = null;
                    if (moveIndex < distribution.TwoPointFivePercentileByMove.Count)
                    {
                        twoPointFivePercentileObjectiveFunction = distribution.TwoPointFivePercentileByMove[moveIndex].ToString(CultureInfo.InvariantCulture);
                    }
                    string?fifthPercentileObjectiveFunction = null;
                    if (moveIndex < distribution.FifthPercentileByMove.Count)
                    {
                        fifthPercentileObjectiveFunction = distribution.FifthPercentileByMove[moveIndex].ToString(CultureInfo.InvariantCulture);
                    }
                    string?medianObjectiveFunction = null;
                    if (moveIndex < distribution.MedianObjectiveFunctionByMove.Count)
                    {
                        medianObjectiveFunction = distribution.MedianObjectiveFunctionByMove[moveIndex].ToString(CultureInfo.InvariantCulture);
                    }
                    string meanObjectiveFunction          = distribution.MeanObjectiveFunctionByMove[moveIndex].ToString(CultureInfo.InvariantCulture);
                    string?upperQuartileObjectiveFunction = null;
                    if (moveIndex < distribution.UpperQuartileByMove.Count)
                    {
                        upperQuartileObjectiveFunction = distribution.UpperQuartileByMove[moveIndex].ToString(CultureInfo.InvariantCulture);
                    }
                    string?ninetyFifthPercentileObjectiveFunction = null;
                    if (moveIndex < distribution.NinetyFifthPercentileByMove.Count)
                    {
                        ninetyFifthPercentileObjectiveFunction = distribution.NinetyFifthPercentileByMove[moveIndex].ToString(CultureInfo.InvariantCulture);
                    }
                    string?ninetySevenPointFivePercentileObjectiveFunction = null;
                    if (moveIndex < distribution.NinetySevenPointFivePercentileByMove.Count)
                    {
                        ninetySevenPointFivePercentileObjectiveFunction = distribution.NinetySevenPointFivePercentileByMove[moveIndex].ToString(CultureInfo.InvariantCulture);
                    }
                    string maxObjectiveFunction = distribution.MaximumObjectiveFunctionByMove[moveIndex].ToString(CultureInfo.InvariantCulture);

                    string?highestMove = null;
                    if ((highestMoveLog != null) && (highestMoveLog.Count > moveIndex))
                    {
                        highestMove = highestMoveLog.GetCsvValues(moveIndex);
                    }
                    string highestObjectiveFunction = String.Empty;
                    if (highestHeuristic.AcceptedObjectiveFunctionByMove.Count > moveIndex)
                    {
                        highestObjectiveFunction = highestHeuristic.AcceptedObjectiveFunctionByMove[moveIndex].ToString(CultureInfo.InvariantCulture);
                    }
                    string?highestObjectiveFunctionForMove = null;
                    if (highestHeuristic.CandidateObjectiveFunctionByMove.Count > moveIndex)
                    {
                        highestObjectiveFunctionForMove = highestHeuristic.CandidateObjectiveFunctionByMove[moveIndex].ToString(CultureInfo.InvariantCulture);
                    }

                    line.Append(runPrefix + "," +
                                moveIndex + "," +
                                runsWithMoveAtIndex + "," +
                                lowestMove + "," +
                                lowestObjectiveFunction + "," +
                                lowestObjectiveFunctionForMove + "," +
                                minObjectiveFunction + "," +
                                twoPointFivePercentileObjectiveFunction + "," +
                                fifthPercentileObjectiveFunction + "," +
                                lowerQuartileObjectiveFunction + "," +
                                medianObjectiveFunction + "," +
                                meanObjectiveFunction + "," +
                                upperQuartileObjectiveFunction + "," +
                                ninetyFifthPercentileObjectiveFunction + "," +
                                ninetySevenPointFivePercentileObjectiveFunction + "," +
                                maxObjectiveFunction + "," +
                                highestMove + "," +
                                highestObjectiveFunction + "," +
                                highestObjectiveFunctionForMove);
                    writer.WriteLine(line);
                }
            }
        }
Example #4
0
        protected override void ProcessRecord()
        {
            if (this.Runs !.Count < 1)
            {
                throw new ArgumentOutOfRangeException(nameof(this.Runs));
            }

            using StreamWriter writer = this.GetWriter();

            StringBuilder line = new StringBuilder();

            if (this.ShouldWriteHeader())
            {
                if (this.Runs[0].HighestHeuristicParameters == null)
                {
                    throw new NotSupportedException("Cannot generate header because first run is missing highest solution parameters");
                }

                line.Append("stand,heuristic," + this.Runs[0].HighestHeuristicParameters !.GetCsvHeader() + ",thin age,rotation,generation,highest min,highest mean,highest max,highest cov,highest alleles,highest heterozygosity,highest individuals,highest polymorphism,lowest min,lowest mean,lowest max,lowest cov,lowest alleles,lowest heterozygosity,lowest individuals,lowest polymorphism");
                writer.WriteLine(line);
            }

            for (int runIndex = 0; runIndex < this.Runs.Count; ++runIndex)
            {
                HeuristicSolutionDistribution distribution = this.Runs[runIndex];
                if ((distribution.HighestSolution == null) || (distribution.LowestSolution == null) || (distribution.HighestHeuristicParameters == null))
                {
                    throw new NotSupportedException("Run " + runIndex + " is missing a highest solution, lowest solution, or highest solution parameters");
                }
                GeneticAlgorithm highestHeuristic  = (GeneticAlgorithm)distribution.HighestSolution;
                GeneticAlgorithm lowestHeuristic   = (GeneticAlgorithm)distribution.LowestSolution;
                StandTrajectory  highestTrajectory = highestHeuristic.BestTrajectory;
                string           linePrefix        = highestTrajectory.Name + "," + highestHeuristic.GetName() + "," + distribution.HighestHeuristicParameters.GetCsvValues() + "," + highestTrajectory.GetFirstHarvestAge() + "," + highestTrajectory.GetRotationLength();

                PopulationStatistics highestStatistics = highestHeuristic.PopulationStatistics;
                PopulationStatistics lowestStatistics  = lowestHeuristic.PopulationStatistics;
                int maxGenerations = Math.Max(highestStatistics.Generations, lowestStatistics.Generations);
                for (int generationIndex = 0; generationIndex < maxGenerations; ++generationIndex)
                {
                    line.Clear();

                    string?highestMinimumFitness        = null;
                    string?highestMeanFitness           = null;
                    string?highestMaximumFitness        = null;
                    string?highestCoefficientOfVariance = null;
                    string?highestMeanAlleles           = null;
                    string?highestMeanHeterozygosity    = null;
                    string?highestNewIndividuals        = null;
                    string?highestPolymorphism          = null;
                    if (highestStatistics.Generations > generationIndex)
                    {
                        highestMinimumFitness        = highestStatistics.MinimumFitnessByGeneration[generationIndex].ToString(CultureInfo.InvariantCulture);
                        highestMeanFitness           = highestStatistics.MeanFitnessByGeneration[generationIndex].ToString(CultureInfo.InvariantCulture);
                        highestMaximumFitness        = highestStatistics.MaximumFitnessByGeneration[generationIndex].ToString(CultureInfo.InvariantCulture);
                        highestCoefficientOfVariance = highestStatistics.CoefficientOfVarianceByGeneration[generationIndex].ToString(CultureInfo.InvariantCulture);
                        highestMeanAlleles           = highestStatistics.MeanAllelesPerLocusByGeneration[generationIndex].ToString(CultureInfo.InvariantCulture);
                        highestMeanHeterozygosity    = highestStatistics.MeanHeterozygosityByGeneration[generationIndex].ToString(CultureInfo.InvariantCulture);
                        highestNewIndividuals        = highestStatistics.NewIndividualsByGeneration[generationIndex].ToString(CultureInfo.InvariantCulture);
                        highestPolymorphism          = highestStatistics.PolymorphismByGeneration[generationIndex].ToString(CultureInfo.InvariantCulture);
                    }

                    string?lowestMinimumFitness        = null;
                    string?lowestMeanFitness           = null;
                    string?lowestMaximumFitness        = null;
                    string?lowestCoefficientOfVariance = null;
                    string?lowestMeanAlleles           = null;
                    string?lowestMeanHeterozygosity    = null;
                    string?lowestNewIndividuals        = null;
                    string?lowestPolymorphism          = null;
                    if (lowestStatistics.Generations > generationIndex)
                    {
                        lowestMinimumFitness        = lowestStatistics.MinimumFitnessByGeneration[generationIndex].ToString(CultureInfo.InvariantCulture);
                        lowestMeanFitness           = lowestStatistics.MeanFitnessByGeneration[generationIndex].ToString(CultureInfo.InvariantCulture);
                        lowestMaximumFitness        = lowestStatistics.MaximumFitnessByGeneration[generationIndex].ToString(CultureInfo.InvariantCulture);
                        lowestCoefficientOfVariance = lowestStatistics.CoefficientOfVarianceByGeneration[generationIndex].ToString(CultureInfo.InvariantCulture);
                        lowestMeanAlleles           = lowestStatistics.MeanAllelesPerLocusByGeneration[generationIndex].ToString(CultureInfo.InvariantCulture);
                        lowestMeanHeterozygosity    = lowestStatistics.MeanHeterozygosityByGeneration[generationIndex].ToString(CultureInfo.InvariantCulture);
                        lowestNewIndividuals        = lowestStatistics.NewIndividualsByGeneration[generationIndex].ToString(CultureInfo.InvariantCulture);
                        lowestPolymorphism          = lowestStatistics.PolymorphismByGeneration[generationIndex].ToString(CultureInfo.InvariantCulture);
                    }

                    line.Append(linePrefix + "," +
                                generationIndex + "," +
                                highestMinimumFitness + "," +
                                highestMeanFitness + "," +
                                highestMaximumFitness + "," +
                                highestCoefficientOfVariance + "," +
                                highestMeanAlleles + "," +
                                highestMeanHeterozygosity + "," +
                                highestNewIndividuals + "," +
                                highestPolymorphism + "," +
                                lowestMinimumFitness + "," +
                                lowestMeanFitness + "," +
                                lowestMaximumFitness + "," +
                                lowestCoefficientOfVariance + "," +
                                lowestMeanAlleles + "," +
                                lowestMeanHeterozygosity + "," +
                                lowestNewIndividuals + "," +
                                lowestPolymorphism);
                    writer.WriteLine(line);
                }
            }
        }
Example #5
0
        protected override void ProcessRecord()
        {
            if (this.HarvestPeriods.Count < 1)
            {
                throw new ArgumentOutOfRangeException(nameof(this.HarvestPeriods));
            }
            if ((this.PerturbBy < 0.0F) || (this.PerturbBy > 1.0F))
            {
                throw new ArgumentOutOfRangeException(nameof(this.PerturbBy));
            }
            if (this.PlanningPeriods.Count < 1)
            {
                throw new ArgumentOutOfRangeException(nameof(this.PlanningPeriods));
            }
            if (this.ProportionalPercentage.Count < 1)
            {
                throw new ArgumentOutOfRangeException(nameof(this.ProportionalPercentage));
            }

            Stopwatch stopwatch = new Stopwatch();

            stopwatch.Start();

            IList <TParameters> parameterCombinations = this.GetParameterCombinations();
            int treeCount = this.Stand !.GetTreeRecordCount();
            List <HeuristicSolutionDistribution> distributions = new List <HeuristicSolutionDistribution>(parameterCombinations.Count * this.HarvestPeriods.Count * this.PlanningPeriods.Count);

            for (int planningPeriodIndex = 0; planningPeriodIndex < this.PlanningPeriods.Count; ++planningPeriodIndex)
            {
                for (int harvestPeriodIndex = 0; harvestPeriodIndex < this.HarvestPeriods.Count; ++harvestPeriodIndex)
                {
                    int planningPeriods = this.PlanningPeriods[planningPeriodIndex];
                    int harvestPeriods  = this.HarvestPeriods[harvestPeriodIndex];
                    if (harvestPeriods >= planningPeriods) // minimum 10 years between thinning and final harvest (if five year time step)
                    {
                        continue;
                    }

                    for (int parameterIndex = 0; parameterIndex < parameterCombinations.Count; ++parameterIndex)
                    {
                        distributions.Add(new HeuristicSolutionDistribution(1, harvestPeriods, treeCount)
                        {
                            HarvestPeriodIndex  = harvestPeriodIndex,
                            ParameterIndex      = parameterIndex,
                            PlanningPeriodIndex = planningPeriodIndex
                        });
                    }
                }
            }

            ParallelOptions parallelOptions = new ParallelOptions()
            {
                MaxDegreeOfParallelism = this.Threads
            };
            int  totalRuns     = distributions.Count * this.BestOf;
            int  runsCompleted = 0;
            Task runs          = Task.Run(() =>
            {
                Parallel.For(0, totalRuns, parallelOptions, (int iteration, ParallelLoopState loopState) =>
                {
                    if (loopState.ShouldExitCurrentIteration)
                    {
                        return;
                    }

                    int distributionIndex = iteration / this.BestOf;
                    HeuristicSolutionDistribution distribution = distributions[distributionIndex];
                    OrganonConfiguration organonConfiguration  = new OrganonConfiguration(OrganonVariant.Create(this.TreeModel));
                    organonConfiguration.Treatments.Harvests.Add(this.CreateHarvest(distribution.HarvestPeriodIndex));

                    Objective objective = new Objective()
                    {
                        IsLandExpectationValue = this.LandExpectationValue,
                        PlanningPeriods        = this.PlanningPeriods[distribution.PlanningPeriodIndex]
                    };
                    TParameters runParameters  = parameterCombinations[distribution.ParameterIndex];
                    Heuristic currentHeuristic = this.CreateHeuristic(organonConfiguration, objective, runParameters);
                    if (runParameters.PerturbBy > 0.0F)
                    {
                        if ((runParameters.PerturbBy == 1.0F) || (distribution.EliteSolutions.NewIndividuals == 0))
                        {
                            // minor optimization point: save a few time steps by by re-using pre-thin results
                            // minor optimization point: save one loop over stand by skipping this for genetic algorithms
                            currentHeuristic.RandomizeTreeSelection(runParameters.ProportionalPercentage);
                        }
                        else
                        {
                            // TODO: support initialization from unperturbed elite solutions
                            // TODO: intialize genetic algorithm population from elite solutions?
                            // TODO: how to define generation statistics?
                            // TODO: more granular locking?
                            lock (distributions)
                            {
                                currentHeuristic.RandomizeTreeSelectionFrom(runParameters.PerturbBy, distribution.EliteSolutions);
                            }
                        }
                    }
                    TimeSpan runTime = currentHeuristic.Run();

                    lock (distributions)
                    {
                        distribution.AddRun(currentHeuristic, runTime, runParameters);
                        ++runsCompleted;
                    }

                    if (this.Stopping)
                    {
                        loopState.Stop();
                    }
                });
            });

            string name = this.GetName();
            int    sleepsSinceLastStatusUpdate = 0;

            while (runs.IsCompleted == false)
            {
                Thread.Sleep(TimeSpan.FromSeconds(1.0));
                ++sleepsSinceLastStatusUpdate;

                if (runs.IsFaulted)
                {
                    Debug.Assert(runs.Exception != null && runs.Exception.InnerException != null);
                    // per https://stackoverflow.com/questions/20170527/how-to-correctly-rethrow-an-exception-of-task-already-in-faulted-state
                    ExceptionDispatchInfo.Capture(runs.Exception.InnerException).Throw();
                }
                if (sleepsSinceLastStatusUpdate > 30)
                {
                    double fractionComplete = (double)runsCompleted / (double)totalRuns;
                    double secondsElapsed   = stopwatch.Elapsed.TotalSeconds;
                    double secondsRemaining = secondsElapsed * (1.0 / fractionComplete - 1.0);
                    this.WriteProgress(new ProgressRecord(0, name, String.Format(runsCompleted + " of " + totalRuns + " runs completed by " + this.Threads + " threads."))
                    {
                        PercentComplete  = (int)(100.0 * fractionComplete),
                        SecondsRemaining = (int)Math.Round(secondsRemaining)
                    });
                    sleepsSinceLastStatusUpdate = 0;
                }
            }
            runs.GetAwaiter().GetResult(); // propagate any exceptions since last IsFaulted check

            foreach (HeuristicSolutionDistribution distribution in distributions)
            {
                distribution.OnRunsComplete();
            }
            stopwatch.Stop();

            this.WriteObject(distributions);
            if (distributions.Count == 1)
            {
                this.WriteSingleDistributionSummary(distributions[0], stopwatch.Elapsed);
            }
            else
            {
                this.WriteMultipleDistributionSummary(distributions, stopwatch.Elapsed);
            }
        }