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); }
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); } } }
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); } } }
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); } } }
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); } }