From f30592994ce87c0fb57e4259fc82ccbe1f407529 Mon Sep 17 00:00:00 2001 From: mikasoukhov Date: Sun, 8 Feb 2026 01:30:12 +0300 Subject: [PATCH 1/3] Added IAsyncFitness support - IAsyncFitness interface with EvaluateAsync(chromosome, cancellationToken) - AsyncFuncFitness for lambda-based async fitness evaluation - ITaskExecutor.Add changed from Action to Func - CancellationToken on ITaskExecutor, passed through constructor - Executors natively support async tasks with linked cancellation - GeneticAlgorithm auto-detects IAsyncFitness and calls async method - Sync IFitness.Evaluate throws NotSupportedException in async implementations --- .../GeneticAlgorithmTest.cs | 97 +++++++++++++++++++ .../Populations/AsyncFitnessStub.cs | 41 ++++++++ .../Fitnesses/AsyncFuncFitness.cs | 43 ++++++++ .../Fitnesses/IAsyncFitness.cs | 19 ++++ src/GeneticSharp.Domain/GeneticAlgorithm.cs | 30 ++++-- .../Threading/StubTaskExecutor.cs | 12 ++- .../Threading/ITaskExecutor.cs | 11 ++- .../Threading/LinearTaskExecutor.cs | 21 +++- .../Threading/ParallelTaskExecutor.cs | 20 +++- .../Threading/TaskExecutorBase.cs | 25 ++++- .../Threading/TaskExecutorExtensions.cs | 20 ++++ .../Threading/TplTaskExecutor.cs | 42 +++++--- 12 files changed, 342 insertions(+), 39 deletions(-) create mode 100644 src/GeneticSharp.Domain.UnitTests/Populations/AsyncFitnessStub.cs create mode 100644 src/GeneticSharp.Domain/Fitnesses/AsyncFuncFitness.cs create mode 100644 src/GeneticSharp.Domain/Fitnesses/IAsyncFitness.cs create mode 100644 src/GeneticSharp.Infrastructure.Framework/Threading/TaskExecutorExtensions.cs diff --git a/src/GeneticSharp.Domain.UnitTests/GeneticAlgorithmTest.cs b/src/GeneticSharp.Domain.UnitTests/GeneticAlgorithmTest.cs index 8510393d..fad8d80f 100644 --- a/src/GeneticSharp.Domain.UnitTests/GeneticAlgorithmTest.cs +++ b/src/GeneticSharp.Domain.UnitTests/GeneticAlgorithmTest.cs @@ -682,6 +682,103 @@ public void Start_FloatingPoingChromosome_Evolved() Assert.GreaterOrEqual(ga.GenerationsNumber, 100); } + [Test()] + public void Start_AsyncFitness_Optimization() + { + var selection = new EliteSelection(); + var crossover = new OnePointCrossover(2); + var mutation = new UniformMutation(); + var chromosome = new ChromosomeStub(); + var target = new GeneticAlgorithm(new Population(50, 50, chromosome), + new AsyncFitnessStub() { SupportsParallel = false }, selection, crossover, mutation); + + target.Population.GenerationStrategy = new TrackingGenerationStrategy(); + target.Termination = new GenerationNumberTermination(25); + + target.Start(); + + Assert.AreEqual(GeneticAlgorithmState.TerminationReached, target.State); + Assert.IsFalse(target.IsRunning); + Assert.AreEqual(25, target.Population.Generations.Count); + + var lastFitness = 0.0; + + foreach (var g in target.Population.Generations) + { + Assert.GreaterOrEqual(g.BestChromosome.Fitness.Value, lastFitness); + lastFitness = g.BestChromosome.Fitness.Value; + } + + Assert.GreaterOrEqual(lastFitness, 0.8); + } + + [Test()] + public void Start_AsyncFitnessParallel_Optimization() + { + var taskExecutor = new ParallelTaskExecutor(); + taskExecutor.MinThreads = 100; + taskExecutor.MaxThreads = 100; + + var selection = new EliteSelection(); + var crossover = new OnePointCrossover(1); + var mutation = new UniformMutation(); + var chromosome = new ChromosomeStub(); + + FlowAssert.IsAtLeastOneAttemptOk(20, () => + { + var target = new GeneticAlgorithm(new Population(100, 150, chromosome), + new AsyncFitnessStub() { SupportsParallel = true }, selection, crossover, mutation); + target.TaskExecutor = taskExecutor; + + target.Start(); + + Assert.AreEqual(GeneticAlgorithmState.TerminationReached, target.State); + Assert.IsFalse(target.IsRunning); + Assert.IsNotNull(target.Population.BestChromosome); + Assert.IsTrue(target.Population.BestChromosome.Fitness >= 0.9, $"Fitness should be >= 0.9, but is {target.Population.BestChromosome.Fitness}"); + }); + } + + [Test()] + public void Start_AsyncFitnessCancellation_OperationCanceled() + { + var cts = new CancellationTokenSource(); + var taskExecutor = new LinearTaskExecutor(cts.Token); + + var selection = new EliteSelection(); + var crossover = new OnePointCrossover(2); + var mutation = new UniformMutation(); + var chromosome = new ChromosomeStub(); + var target = new GeneticAlgorithm(new Population(50, 50, chromosome), + new AsyncFitnessStub() { SupportsParallel = true, ParallelSleep = 5000 }, selection, crossover, mutation); + target.TaskExecutor = taskExecutor; + target.Termination = new GenerationNumberTermination(25); + + cts.CancelAfter(100); + + Assert.Catch(() => + { + target.Start(); + }); + } + + [Test()] + public void Start_AsyncFitnessEvaluationFailed_FitnessException() + { + var selection = new RouletteWheelSelection(); + var crossover = new OnePointCrossover(1); + var mutation = new UniformMutation(); + var chromosome = new ChromosomeStub(); + var fitness = new AsyncFuncFitness((c, ct) => throw new Exception("TEST")); + + var target = new GeneticAlgorithm(new Population(100, 150, chromosome), fitness, selection, crossover, mutation); + + Assert.Catch(target.Start); + + Assert.IsFalse(target.IsRunning); + Assert.AreEqual(GeneticAlgorithmState.Stopped, target.State); + } + [Test()] public void Stop_NotStarted_Exception() { diff --git a/src/GeneticSharp.Domain.UnitTests/Populations/AsyncFitnessStub.cs b/src/GeneticSharp.Domain.UnitTests/Populations/AsyncFitnessStub.cs new file mode 100644 index 00000000..af9e3317 --- /dev/null +++ b/src/GeneticSharp.Domain.UnitTests/Populations/AsyncFitnessStub.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace GeneticSharp.Domain.UnitTests +{ + public class AsyncFitnessStub : IFitness, IAsyncFitness + { + public AsyncFitnessStub() + { + ParallelSleep = 500; + } + + public bool SupportsParallel { get; set; } + public int ParallelSleep { get; set; } + + public double Evaluate(IChromosome chromosome) + { + throw new NotSupportedException("Use EvaluateAsync instead."); + } + + public async Task EvaluateAsync(IChromosome chromosome, CancellationToken cancellationToken) + { + if (SupportsParallel) + { + await Task.Delay(ParallelSleep, cancellationToken); + } + + var genes = chromosome.GetGenes(); + double f = genes.Sum(g => (int)g.Value) / 20f; + + if (f > 1) + { + f = 0; + } + + return f; + } + } +} diff --git a/src/GeneticSharp.Domain/Fitnesses/AsyncFuncFitness.cs b/src/GeneticSharp.Domain/Fitnesses/AsyncFuncFitness.cs new file mode 100644 index 00000000..f5432a4f --- /dev/null +++ b/src/GeneticSharp.Domain/Fitnesses/AsyncFuncFitness.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace GeneticSharp +{ + /// + /// An IAsyncFitness implementation that defers the fitness evaluation to an async Func. + /// + public class AsyncFuncFitness : IFitness, IAsyncFitness + { + private readonly Func> m_func; + + /// + /// Initializes a new instance of the class. + /// + /// The async fitness evaluation Func. + public AsyncFuncFitness(Func> func) + { + ExceptionHelper.ThrowIfNull("func", func); + m_func = func; + } + + /// + /// Evaluate the specified chromosome. + /// + /// Chromosome. + public double Evaluate(IChromosome chromosome) + { + throw new NotSupportedException("Use EvaluateAsync instead."); + } + + /// + /// Evaluate the specified chromosome asynchronously. + /// + /// Chromosome. + /// The cancellation token. + public Task EvaluateAsync(IChromosome chromosome, CancellationToken cancellationToken) + { + return m_func(chromosome, cancellationToken); + } + } +} diff --git a/src/GeneticSharp.Domain/Fitnesses/IAsyncFitness.cs b/src/GeneticSharp.Domain/Fitnesses/IAsyncFitness.cs new file mode 100644 index 00000000..225f070f --- /dev/null +++ b/src/GeneticSharp.Domain/Fitnesses/IAsyncFitness.cs @@ -0,0 +1,19 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace GeneticSharp +{ + /// + /// Defines an interface for asynchronous fitness function. + /// + public interface IAsyncFitness + { + /// + /// Performs the evaluation against the specified chromosome asynchronously. + /// + /// The chromosome to be evaluated. + /// The cancellation token. + /// The fitness of the chromosome. + Task EvaluateAsync(IChromosome chromosome, CancellationToken cancellationToken); + } +} diff --git a/src/GeneticSharp.Domain/GeneticAlgorithm.cs b/src/GeneticSharp.Domain/GeneticAlgorithm.cs index 9c3ac3ed..7f9cd26f 100644 --- a/src/GeneticSharp.Domain/GeneticAlgorithm.cs +++ b/src/GeneticSharp.Domain/GeneticAlgorithm.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace GeneticSharp { @@ -399,10 +401,7 @@ private void EvaluateFitness() { var c = chromosomesWithoutFitness[i]; - TaskExecutor.Add(() => - { - RunEvaluateFitness(c); - }); + TaskExecutor.Add(ct => RunEvaluateFitness(c, ct)); } if (!TaskExecutor.Start()) @@ -419,13 +418,10 @@ private void EvaluateFitness() Population.CurrentGeneration.Chromosomes = Population.CurrentGeneration.Chromosomes.OrderByDescending(c => c.Fitness.Value).ToList(); } - /// - /// Runs the evaluate fitness. - /// - /// The chromosome. - private void RunEvaluateFitness(object chromosome) + private ValueTask RunEvaluateFitness(IChromosome c, CancellationToken ct) { - var c = chromosome as IChromosome; + if (Fitness is IAsyncFitness asyncFitness) + return RunEvaluateFitnessAsync(c, asyncFitness, ct); try { @@ -435,6 +431,20 @@ private void RunEvaluateFitness(object chromosome) { throw new FitnessException(Fitness, "Error executing Fitness.Evaluate for chromosome: {0}".With(ex.Message), ex); } + + return default; + } + + private async ValueTask RunEvaluateFitnessAsync(IChromosome c, IAsyncFitness asyncFitness, CancellationToken ct) + { + try + { + c.Fitness = await asyncFitness.EvaluateAsync(c, ct); + } + catch (Exception ex) + { + throw new FitnessException(Fitness, "Error executing Fitness.Evaluate for chromosome: {0}".With(ex.Message), ex); + } } /// diff --git a/src/GeneticSharp.Infrastructure.Framework.UnitTests/Threading/StubTaskExecutor.cs b/src/GeneticSharp.Infrastructure.Framework.UnitTests/Threading/StubTaskExecutor.cs index a541a5ce..c77456fe 100644 --- a/src/GeneticSharp.Infrastructure.Framework.UnitTests/Threading/StubTaskExecutor.cs +++ b/src/GeneticSharp.Infrastructure.Framework.UnitTests/Threading/StubTaskExecutor.cs @@ -1,18 +1,20 @@ using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace GeneticSharp.Infrastructure.Framework.UnitTests.Threading { - public class StubTaskExecutor : TaskExecutorBase + public class StubTaskExecutor : TaskExecutorBase { - public IList GetTasks() - { + public IList> GetTasks() + { return Tasks; } public bool GetStopRequested() - { + { return StopRequested; - } + } } } \ No newline at end of file diff --git a/src/GeneticSharp.Infrastructure.Framework/Threading/ITaskExecutor.cs b/src/GeneticSharp.Infrastructure.Framework/Threading/ITaskExecutor.cs index 4d46321f..2b198627 100644 --- a/src/GeneticSharp.Infrastructure.Framework/Threading/ITaskExecutor.cs +++ b/src/GeneticSharp.Infrastructure.Framework/Threading/ITaskExecutor.cs @@ -1,7 +1,9 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; -namespace GeneticSharp +namespace GeneticSharp { /// /// Defines a interface to a task executor. @@ -21,6 +23,11 @@ public interface ITaskExecutor /// true if this instance is running; otherwise, false. /// bool IsRunning { get; } + + /// + /// Gets the cancellation token. + /// + CancellationToken CancellationToken { get; } #endregion #region Methods @@ -28,7 +35,7 @@ public interface ITaskExecutor /// Add the specified task to be executed. /// /// The task. - void Add(Action task); + void Add(Func task); /// /// Clear all the tasks. diff --git a/src/GeneticSharp.Infrastructure.Framework/Threading/LinearTaskExecutor.cs b/src/GeneticSharp.Infrastructure.Framework/Threading/LinearTaskExecutor.cs index 0345234d..19a09e62 100644 --- a/src/GeneticSharp.Infrastructure.Framework/Threading/LinearTaskExecutor.cs +++ b/src/GeneticSharp.Infrastructure.Framework/Threading/LinearTaskExecutor.cs @@ -1,12 +1,29 @@ using System; +using System.Threading; -namespace GeneticSharp +namespace GeneticSharp { /// /// An ITaskExecutor's implementation that executes the tasks in a linear fashion. /// public class LinearTaskExecutor : TaskExecutorBase { + /// + /// Initializes a new instance of the class. + /// + public LinearTaskExecutor() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The cancellation token. + public LinearTaskExecutor(CancellationToken cancellationToken) + : base(cancellationToken) + { + } + #region implemented abstract members of TaskExecutorBase /// /// Starts the tasks execution. @@ -27,7 +44,7 @@ public override bool Start() return true; } - Tasks[i](); + Tasks[i](CancellationToken).GetAwaiter().GetResult(); // If take more time expected on Timeout property, // tehn stop thre running. diff --git a/src/GeneticSharp.Infrastructure.Framework/Threading/ParallelTaskExecutor.cs b/src/GeneticSharp.Infrastructure.Framework/Threading/ParallelTaskExecutor.cs index c766852c..5a13f472 100644 --- a/src/GeneticSharp.Infrastructure.Framework/Threading/ParallelTaskExecutor.cs +++ b/src/GeneticSharp.Infrastructure.Framework/Threading/ParallelTaskExecutor.cs @@ -2,7 +2,7 @@ using System.Threading; using System.Threading.Tasks; -namespace GeneticSharp +namespace GeneticSharp { /// /// An ITaskExecutor's implementation that executes the tasks in a parallel fashion. @@ -18,6 +18,18 @@ public ParallelTaskExecutor() MinThreads = 200; MaxThreads = 200; } + + /// + /// Initializes a new instance of the + /// class. + /// + /// The cancellation token. + public ParallelTaskExecutor(CancellationToken cancellationToken) + : base(cancellationToken) + { + MinThreads = 200; + MaxThreads = 200; + } /// /// Gets or sets the minimum threads. @@ -47,12 +59,14 @@ public override bool Start() try { base.Start(); - CancellationTokenSource = new CancellationTokenSource(); + CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken); + var token = CancellationTokenSource.Token; var parallelTasks = new Task[Tasks.Count]; for (int i = 0; i < Tasks.Count; i++) { - parallelTasks[i] = Task.Run(Tasks[i], CancellationTokenSource.Token); + var task = Tasks[i]; + parallelTasks[i] = Task.Run(async () => await task(token), token); } // Need to verify, because TimeSpan.MaxValue passed to Task.WaitAll throws a System.ArgumentOutOfRangeException. diff --git a/src/GeneticSharp.Infrastructure.Framework/Threading/TaskExecutorBase.cs b/src/GeneticSharp.Infrastructure.Framework/Threading/TaskExecutorBase.cs index 67f951b3..7e16971e 100644 --- a/src/GeneticSharp.Infrastructure.Framework/Threading/TaskExecutorBase.cs +++ b/src/GeneticSharp.Infrastructure.Framework/Threading/TaskExecutorBase.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace GeneticSharp { @@ -14,9 +16,19 @@ public abstract class TaskExecutorBase : ITaskExecutor /// Initializes a new instance of the class. /// protected TaskExecutorBase() + : this(CancellationToken.None) { - Tasks = new List(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The cancellation token. + protected TaskExecutorBase(CancellationToken cancellationToken) + { + Tasks = new List>(); Timeout = TimeSpan.MaxValue; + CancellationToken = cancellationToken; } /// @@ -36,7 +48,12 @@ protected TaskExecutorBase() /// /// Gets the tasks. /// - protected IList Tasks { get; private set; } + protected IList> Tasks { get; private set; } + + /// + /// Gets the cancellation token. + /// + public CancellationToken CancellationToken { get; } /// /// Gets a value indicating whether this @@ -44,12 +61,12 @@ protected TaskExecutorBase() /// /// true if stop requested; otherwise, false. protected bool StopRequested { get; private set; } - + /// /// Add the specified task to be executed. /// /// The task. - public void Add(Action task) + public void Add(Func task) { Tasks.Add(task); } diff --git a/src/GeneticSharp.Infrastructure.Framework/Threading/TaskExecutorExtensions.cs b/src/GeneticSharp.Infrastructure.Framework/Threading/TaskExecutorExtensions.cs new file mode 100644 index 00000000..90387bcf --- /dev/null +++ b/src/GeneticSharp.Infrastructure.Framework/Threading/TaskExecutorExtensions.cs @@ -0,0 +1,20 @@ +using System; + +namespace GeneticSharp +{ + /// + /// Extension methods for . + /// + public static class TaskExecutorExtensions + { + /// + /// Add the specified synchronous task to be executed. + /// + /// The task executor. + /// The synchronous task. + public static void Add(this ITaskExecutor executor, Action task) + { + executor.Add(ct => { task(); return default; }); + } + } +} diff --git a/src/GeneticSharp.Infrastructure.Framework/Threading/TplTaskExecutor.cs b/src/GeneticSharp.Infrastructure.Framework/Threading/TplTaskExecutor.cs index 876e1756..0ce461e9 100644 --- a/src/GeneticSharp.Infrastructure.Framework/Threading/TplTaskExecutor.cs +++ b/src/GeneticSharp.Infrastructure.Framework/Threading/TplTaskExecutor.cs @@ -10,6 +10,22 @@ namespace GeneticSharp /// public class TplTaskExecutor : ParallelTaskExecutor { + /// + /// Initializes a new instance of the class. + /// + public TplTaskExecutor() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The cancellation token. + public TplTaskExecutor(CancellationToken cancellationToken) + : base(cancellationToken) + { + } + /// /// Starts the tasks execution. /// @@ -19,28 +35,28 @@ public override bool Start() try { var startTime = DateTime.Now; - CancellationTokenSource = new CancellationTokenSource(); - var result = new ParallelLoopResult(); + CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken); + var token = CancellationTokenSource.Token; try { - result = Parallel.For(0, Tasks.Count, new ParallelOptions() { CancellationToken = CancellationTokenSource.Token }, (i, state) => - { - // Execute the target function (fitness). - Tasks[i](); + Parallel.ForEachAsync( + System.Linq.Enumerable.Range(0, Tasks.Count), + new ParallelOptions() { CancellationToken = token }, + async (i, ct) => + { + await Tasks[i](ct); - // If cancellation token was requested OR take more time expected on Timeout property, - // then stop the running. - if (CancellationTokenSource.IsCancellationRequested || (DateTime.Now - startTime) > Timeout) - state.Break(); - }); + if ((DateTime.Now - startTime) > Timeout) + CancellationTokenSource.Cancel(); + }).GetAwaiter().GetResult(); } catch (OperationCanceledException) { - // Mute cancellation exception. + return false; } - return result.IsCompleted; + return true; } finally { From 34f969026e8991512cf3290a790f62bacf33c3a0 Mon Sep 17 00:00:00 2001 From: mikasoukhov Date: Sun, 8 Feb 2026 03:05:27 +0300 Subject: [PATCH 2/3] Added IMutation.MinChromosomeLength property --- .../Mutations/MinChromosomeLengthTest.cs | 25 +++++++++++++++++++ .../Mutations/IMutation.cs | 7 +++++- .../Mutations/MutationBase.cs | 7 +++++- .../Mutations/SequenceMutationBase.cs | 7 +++++- 4 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 src/GeneticSharp.Domain.UnitTests/Mutations/MinChromosomeLengthTest.cs diff --git a/src/GeneticSharp.Domain.UnitTests/Mutations/MinChromosomeLengthTest.cs b/src/GeneticSharp.Domain.UnitTests/Mutations/MinChromosomeLengthTest.cs new file mode 100644 index 00000000..3afe47b8 --- /dev/null +++ b/src/GeneticSharp.Domain.UnitTests/Mutations/MinChromosomeLengthTest.cs @@ -0,0 +1,25 @@ +using NUnit.Framework; + +namespace GeneticSharp.Domain.UnitTests.Mutations +{ + [TestFixture()] + [Category("Mutations")] + public class MinChromosomeLengthTest + { + [Test()] + public void MinChromosomeLength_SequenceMutations_Three() + { + Assert.AreEqual(3, new DisplacementMutation().MinChromosomeLength); + Assert.AreEqual(3, new InsertionMutation().MinChromosomeLength); + Assert.AreEqual(3, new PartialShuffleMutation().MinChromosomeLength); + Assert.AreEqual(3, new ReverseSequenceMutation().MinChromosomeLength); + } + + [Test()] + public void MinChromosomeLength_NonSequenceMutations_Zero() + { + Assert.AreEqual(0, new TworsMutation().MinChromosomeLength); + Assert.AreEqual(0, new UniformMutation().MinChromosomeLength); + } + } +} diff --git a/src/GeneticSharp.Domain/Mutations/IMutation.cs b/src/GeneticSharp.Domain/Mutations/IMutation.cs index 088bfda2..0aa4d537 100644 --- a/src/GeneticSharp.Domain/Mutations/IMutation.cs +++ b/src/GeneticSharp.Domain/Mutations/IMutation.cs @@ -1,4 +1,4 @@ -namespace GeneticSharp +namespace GeneticSharp { /// /// Mutation is a genetic operator used to maintain genetic diversity from one generation of a population of genetic algorithm @@ -23,6 +23,11 @@ namespace GeneticSharp /// public interface IMutation : IChromosomeOperator { + /// + /// Gets the minimum chromosome length required by this mutation. + /// + int MinChromosomeLength { get; } + /// /// Mutate the specified chromosome. /// diff --git a/src/GeneticSharp.Domain/Mutations/MutationBase.cs b/src/GeneticSharp.Domain/Mutations/MutationBase.cs index 4415995d..7d405674 100644 --- a/src/GeneticSharp.Domain/Mutations/MutationBase.cs +++ b/src/GeneticSharp.Domain/Mutations/MutationBase.cs @@ -1,4 +1,4 @@ -namespace GeneticSharp +namespace GeneticSharp { /// /// Base class for IMutation's implementation. @@ -10,6 +10,11 @@ public abstract class MutationBase : IMutation /// Gets or sets a value indicating whether the operator is ordered (if can keep the chromosome order). /// public bool IsOrdered { get; protected set; } + + /// + /// Gets the minimum chromosome length required by this mutation. + /// + public virtual int MinChromosomeLength => 0; #endregion #region Methods diff --git a/src/GeneticSharp.Domain/Mutations/SequenceMutationBase.cs b/src/GeneticSharp.Domain/Mutations/SequenceMutationBase.cs index a3dad098..0bf57545 100644 --- a/src/GeneticSharp.Domain/Mutations/SequenceMutationBase.cs +++ b/src/GeneticSharp.Domain/Mutations/SequenceMutationBase.cs @@ -8,6 +8,11 @@ namespace GeneticSharp /// public abstract class SequenceMutationBase : MutationBase { + /// + /// Gets the minimum chromosome length required by this mutation. + /// + public override int MinChromosomeLength => 3; + #region Methods /// /// Mutate the specified chromosome. @@ -37,7 +42,7 @@ protected override void PerformMutate(IChromosome chromosome, float probability) /// The chromosome. protected virtual void ValidateLength(IChromosome chromosome) { - if (chromosome.Length < 3) + if (chromosome.Length < MinChromosomeLength) { throw new MutationException(this, "A chromosome should have, at least, 3 genes. {0} has only {1} gene.".With(chromosome.GetType().Name, chromosome.Length)); } From 45bbd8eefd1c0a8f4dfc1423c163dbe7921332dc Mon Sep 17 00:00:00 2001 From: mikasoukhov Date: Tue, 3 Mar 2026 20:02:41 +0300 Subject: [PATCH 3/3] Fixed issues from PR review: dispose linked CTS, proper OperationCanceledException handling, dynamic error messages --- src/GeneticSharp.Domain.UnitTests/GeneticAlgorithmTest.cs | 2 +- src/GeneticSharp.Domain/GeneticAlgorithm.cs | 6 +++++- src/GeneticSharp.Domain/Mutations/SequenceMutationBase.cs | 2 +- .../Threading/ParallelTaskExecutor.cs | 2 ++ .../Threading/TplTaskExecutor.cs | 2 ++ 5 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/GeneticSharp.Domain.UnitTests/GeneticAlgorithmTest.cs b/src/GeneticSharp.Domain.UnitTests/GeneticAlgorithmTest.cs index fad8d80f..077570f8 100644 --- a/src/GeneticSharp.Domain.UnitTests/GeneticAlgorithmTest.cs +++ b/src/GeneticSharp.Domain.UnitTests/GeneticAlgorithmTest.cs @@ -756,7 +756,7 @@ public void Start_AsyncFitnessCancellation_OperationCanceled() cts.CancelAfter(100); - Assert.Catch(() => + Assert.Catch(() => { target.Start(); }); diff --git a/src/GeneticSharp.Domain/GeneticAlgorithm.cs b/src/GeneticSharp.Domain/GeneticAlgorithm.cs index 7f9cd26f..0486c34b 100644 --- a/src/GeneticSharp.Domain/GeneticAlgorithm.cs +++ b/src/GeneticSharp.Domain/GeneticAlgorithm.cs @@ -441,9 +441,13 @@ private async ValueTask RunEvaluateFitnessAsync(IChromosome c, IAsyncFitness asy { c.Fitness = await asyncFitness.EvaluateAsync(c, ct); } + catch (OperationCanceledException) + { + throw; + } catch (Exception ex) { - throw new FitnessException(Fitness, "Error executing Fitness.Evaluate for chromosome: {0}".With(ex.Message), ex); + throw new FitnessException(Fitness, "Error executing Fitness.EvaluateAsync for chromosome: {0}".With(ex.Message), ex); } } diff --git a/src/GeneticSharp.Domain/Mutations/SequenceMutationBase.cs b/src/GeneticSharp.Domain/Mutations/SequenceMutationBase.cs index 0bf57545..e7e6ff21 100644 --- a/src/GeneticSharp.Domain/Mutations/SequenceMutationBase.cs +++ b/src/GeneticSharp.Domain/Mutations/SequenceMutationBase.cs @@ -44,7 +44,7 @@ protected virtual void ValidateLength(IChromosome chromosome) { if (chromosome.Length < MinChromosomeLength) { - throw new MutationException(this, "A chromosome should have, at least, 3 genes. {0} has only {1} gene.".With(chromosome.GetType().Name, chromosome.Length)); + throw new MutationException(this, "A chromosome should have, at least, {0} genes. {1} has only {2} gene.".With(MinChromosomeLength, chromosome.GetType().Name, chromosome.Length)); } } diff --git a/src/GeneticSharp.Infrastructure.Framework/Threading/ParallelTaskExecutor.cs b/src/GeneticSharp.Infrastructure.Framework/Threading/ParallelTaskExecutor.cs index 5a13f472..77c6f3b8 100644 --- a/src/GeneticSharp.Infrastructure.Framework/Threading/ParallelTaskExecutor.cs +++ b/src/GeneticSharp.Infrastructure.Framework/Threading/ParallelTaskExecutor.cs @@ -81,6 +81,8 @@ public override bool Start() finally { ResetThreadPoolConfig(minWorker, minIOC, maxWorker, maxIOC); + CancellationTokenSource?.Dispose(); + CancellationTokenSource = null; IsRunning = false; } } diff --git a/src/GeneticSharp.Infrastructure.Framework/Threading/TplTaskExecutor.cs b/src/GeneticSharp.Infrastructure.Framework/Threading/TplTaskExecutor.cs index 0ce461e9..4d5433dd 100644 --- a/src/GeneticSharp.Infrastructure.Framework/Threading/TplTaskExecutor.cs +++ b/src/GeneticSharp.Infrastructure.Framework/Threading/TplTaskExecutor.cs @@ -60,6 +60,8 @@ public override bool Start() } finally { + CancellationTokenSource?.Dispose(); + CancellationTokenSource = null; IsRunning = false; } }