Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions src/GeneticSharp.Domain.UnitTests/GeneticAlgorithmTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<OperationCanceledException>(() =>
{
target.Start();
});
Comment on lines +745 to +762

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disposable 'CancellationTokenSource' is created but not disposed.

Suggested change
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<FitnessException>(() =>
{
target.Start();
});
using (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<FitnessException>(() =>
{
target.Start();
});
}

Copilot uses AI. Check for mistakes.
}

[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<FitnessException>(target.Start);

Assert.IsFalse(target.IsRunning);
Assert.AreEqual(GeneticAlgorithmState.Stopped, target.State);
}

[Test()]
public void Stop_NotStarted_Exception()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
41 changes: 41 additions & 0 deletions src/GeneticSharp.Domain.UnitTests/Populations/AsyncFitnessStub.cs
Original file line number Diff line number Diff line change
@@ -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<double> 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;
}
}
}
43 changes: 43 additions & 0 deletions src/GeneticSharp.Domain/Fitnesses/AsyncFuncFitness.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace GeneticSharp
{
/// <summary>
/// An IAsyncFitness implementation that defers the fitness evaluation to an async Func.
/// </summary>
public class AsyncFuncFitness : IFitness, IAsyncFitness
{
private readonly Func<IChromosome, CancellationToken, Task<double>> m_func;

/// <summary>
/// Initializes a new instance of the <see cref="AsyncFuncFitness"/> class.
/// </summary>
/// <param name="func">The async fitness evaluation Func.</param>
public AsyncFuncFitness(Func<IChromosome, CancellationToken, Task<double>> func)
{
ExceptionHelper.ThrowIfNull("func", func);
m_func = func;
}

/// <summary>
/// Evaluate the specified chromosome.
/// </summary>
/// <param name="chromosome">Chromosome.</param>
public double Evaluate(IChromosome chromosome)
{
throw new NotSupportedException("Use EvaluateAsync instead.");
}

/// <summary>
/// Evaluate the specified chromosome asynchronously.
/// </summary>
/// <param name="chromosome">Chromosome.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public Task<double> EvaluateAsync(IChromosome chromosome, CancellationToken cancellationToken)
{
return m_func(chromosome, cancellationToken);
}
}
}
19 changes: 19 additions & 0 deletions src/GeneticSharp.Domain/Fitnesses/IAsyncFitness.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Threading;
using System.Threading.Tasks;

namespace GeneticSharp
{
/// <summary>
/// Defines an interface for asynchronous fitness function.
/// </summary>
public interface IAsyncFitness
{
/// <summary>
/// Performs the evaluation against the specified chromosome asynchronously.
/// </summary>
/// <param name="chromosome">The chromosome to be evaluated.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The fitness of the chromosome.</returns>
Task<double> EvaluateAsync(IChromosome chromosome, CancellationToken cancellationToken);
}
}
34 changes: 24 additions & 10 deletions src/GeneticSharp.Domain/GeneticAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace GeneticSharp
{
Expand Down Expand Up @@ -399,10 +401,7 @@ private void EvaluateFitness()
{
var c = chromosomesWithoutFitness[i];

TaskExecutor.Add(() =>
{
RunEvaluateFitness(c);
});
TaskExecutor.Add(ct => RunEvaluateFitness(c, ct));
}

if (!TaskExecutor.Start())
Expand All @@ -419,13 +418,10 @@ private void EvaluateFitness()
Population.CurrentGeneration.Chromosomes = Population.CurrentGeneration.Chromosomes.OrderByDescending(c => c.Fitness.Value).ToList();
}

/// <summary>
/// Runs the evaluate fitness.
/// </summary>
/// <param name="chromosome">The chromosome.</param>
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
{
Expand All @@ -435,6 +431,24 @@ 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 (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
throw new FitnessException(Fitness, "Error executing Fitness.EvaluateAsync for chromosome: {0}".With(ex.Message), ex);
}
}

/// <summary>
Expand Down
7 changes: 6 additions & 1 deletion src/GeneticSharp.Domain/Mutations/IMutation.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace GeneticSharp
namespace GeneticSharp
{
/// <summary>
/// Mutation is a genetic operator used to maintain genetic diversity from one generation of a population of genetic algorithm
Expand All @@ -23,6 +23,11 @@ namespace GeneticSharp
/// </summary>
public interface IMutation : IChromosomeOperator
{
/// <summary>
/// Gets the minimum chromosome length required by this mutation.
/// </summary>
int MinChromosomeLength { get; }

/// <summary>
/// Mutate the specified chromosome.
/// </summary>
Expand Down
7 changes: 6 additions & 1 deletion src/GeneticSharp.Domain/Mutations/MutationBase.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace GeneticSharp
namespace GeneticSharp
{
/// <summary>
/// Base class for IMutation's implementation.
Expand All @@ -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).
/// </summary>
public bool IsOrdered { get; protected set; }

/// <summary>
/// Gets the minimum chromosome length required by this mutation.
/// </summary>
public virtual int MinChromosomeLength => 0;
#endregion

#region Methods
Expand Down
9 changes: 7 additions & 2 deletions src/GeneticSharp.Domain/Mutations/SequenceMutationBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ namespace GeneticSharp
/// </summary>
public abstract class SequenceMutationBase : MutationBase
{
/// <summary>
/// Gets the minimum chromosome length required by this mutation.
/// </summary>
public override int MinChromosomeLength => 3;

#region Methods
/// <summary>
/// Mutate the specified chromosome.
Expand Down Expand Up @@ -37,9 +42,9 @@ protected override void PerformMutate(IChromosome chromosome, float probability)
/// <param name="chromosome">The chromosome.</param>
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));
throw new MutationException(this, "A chromosome should have, at least, {0} genes. {1} has only {2} gene.".With(MinChromosomeLength, chromosome.GetType().Name, chromosome.Length));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Action> GetTasks()
{
public IList<Func<CancellationToken, ValueTask>> GetTasks()
{
return Tasks;
}

public bool GetStopRequested()
{
{
return StopRequested;
}
}
}
}
Loading