Skip to content

Added IAsyncFitness and IMutation.MinChromosomeLength#144

Open
mikasoukhov wants to merge 3 commits intogiacomelli:masterfrom
StockSharp:master
Open

Added IAsyncFitness and IMutation.MinChromosomeLength#144
mikasoukhov wants to merge 3 commits intogiacomelli:masterfrom
StockSharp:master

Conversation

@mikasoukhov
Copy link

  • Added IAsyncFitness interface for async fitness evaluation with CancellationToken support
  • Changed ITaskExecutor.Add to accept Func<CancellationToken, ValueTask> for native async execution
  • Added IMutation.MinChromosomeLength property (IMutation. RequiredLength property #109)

- IAsyncFitness interface with EvaluateAsync(chromosome, cancellationToken)
- AsyncFuncFitness for lambda-based async fitness evaluation
- ITaskExecutor.Add changed from Action to Func<CancellationToken, ValueTask>
- 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
Copilot AI review requested due to automatic review settings February 8, 2026 07:24
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR extends GeneticSharp to support asynchronous fitness evaluation (with cancellation support) and exposes a minimum chromosome length requirement on mutations (per #109). It also updates task executors to accept native async delegates (Func<CancellationToken, ValueTask>) while preserving a sync Add(Action) convenience via an extension method.

Changes:

  • Added IAsyncFitness (+ helper implementations/stubs) and updated GeneticAlgorithm fitness evaluation to use async fitness when available.
  • Updated ITaskExecutor + executor implementations to run Func<CancellationToken, ValueTask> tasks and propagate cancellation tokens.
  • Added IMutation.MinChromosomeLength (defaulted in MutationBase, overridden for sequence mutations) and tests validating the new contract.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/GeneticSharp.Infrastructure.Framework/Threading/TplTaskExecutor.cs Switches TPL executor to Parallel.ForEachAsync and introduces cancellation-token-aware execution.
src/GeneticSharp.Infrastructure.Framework/Threading/TaskExecutorExtensions.cs Adds Add(Action) convenience overload via extension method.
src/GeneticSharp.Infrastructure.Framework/Threading/TaskExecutorBase.cs Changes stored task type to async delegate and introduces a constructor-provided cancellation token.
src/GeneticSharp.Infrastructure.Framework/Threading/ParallelTaskExecutor.cs Updates parallel execution to run async delegates and link cancellation tokens.
src/GeneticSharp.Infrastructure.Framework/Threading/LinearTaskExecutor.cs Updates linear execution to run async delegates synchronously.
src/GeneticSharp.Infrastructure.Framework/Threading/ITaskExecutor.cs Updates task signature and adds CancellationToken to the interface.
src/GeneticSharp.Infrastructure.Framework.UnitTests/Threading/StubTaskExecutor.cs Adapts unit test stub to new task delegate type.
src/GeneticSharp.Domain/Mutations/SequenceMutationBase.cs Introduces MinChromosomeLength override and uses it in validation.
src/GeneticSharp.Domain/Mutations/MutationBase.cs Adds default MinChromosomeLength implementation.
src/GeneticSharp.Domain/Mutations/IMutation.cs Adds MinChromosomeLength to the mutation contract.
src/GeneticSharp.Domain/GeneticAlgorithm.cs Uses async fitness when available; passes cancellation token through task executor.
src/GeneticSharp.Domain/Fitnesses/IAsyncFitness.cs Adds async fitness interface with cancellation support.
src/GeneticSharp.Domain/Fitnesses/AsyncFuncFitness.cs Adds a Func-based async fitness implementation.
src/GeneticSharp.Domain.UnitTests/Populations/AsyncFitnessStub.cs Adds async fitness stub used for GA tests.
src/GeneticSharp.Domain.UnitTests/Mutations/MinChromosomeLengthTest.cs Adds tests for MinChromosomeLength on several mutations.
src/GeneticSharp.Domain.UnitTests/GeneticAlgorithmTest.cs Adds tests for async fitness, parallel async fitness, cancellation, and failure wrapping.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 33 to +39
public override bool Start()
{
try
{
var startTime = DateTime.Now;
CancellationTokenSource = new CancellationTokenSource();
var result = new ParallelLoopResult();
CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken);
var token = CancellationTokenSource.Token;
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

TplTaskExecutor.Start() never calls base.Start(), so StopRequested is not reset and IsRunning is never set to true (but is set to false in finally). This makes IsRunning/Stop semantics inconsistent with LinearTaskExecutor/ParallelTaskExecutor. Call base.Start() at the beginning of Start(), and ensure IsRunning is set/reset in a consistent place (ideally by letting TaskExecutorBase/ParallelTaskExecutor handle it).

Copilot uses AI. Check for mistakes.
Comment on lines 37 to +45
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 },
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

TplTaskExecutor inherits MinThreads/MaxThreads and SetThreadPoolConfig/ResetThreadPoolConfig behavior from ParallelTaskExecutor, but Start() bypasses all of that. As a result, MinThreads/MaxThreads currently have no effect for this executor. Consider mirroring ParallelTaskExecutor.Start() by calling SetThreadPoolConfig before starting and ResetThreadPoolConfig in finally (or delegating to base.Start implementation) so the thread pool configuration is applied consistently.

Copilot uses AI. Check for mistakes.
Comment on lines +38 to 40
CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken);
var token = CancellationTokenSource.Token;

Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

CreateLinkedTokenSource allocates registrations that should be disposed. Neither the previous CancellationTokenSource (if any) nor the newly-created linked source is disposed here, which can leak registrations across repeated Start/Stop cycles. Dispose the linked CancellationTokenSource when Start finishes (and consider disposing any prior instance before overwriting the field).

Copilot uses AI. Check for mistakes.
Comment on lines 61 to 64
base.Start();
CancellationTokenSource = new CancellationTokenSource();
CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken);
var token = CancellationTokenSource.Token;
var parallelTasks = new Task[Tasks.Count];
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

ParallelTaskExecutor creates a linked CancellationTokenSource but never disposes it. Linked CTS instances hold onto token registrations; dispose it when Start completes (and also dispose any previous instance before overwriting the property) to avoid leaks when the executor is reused.

Copilot uses AI. Check for mistakes.
Comment on lines +62 to +63
CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken);
var token = CancellationTokenSource.Token;
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

ParallelTaskExecutor.Start returns false on timeout (Task.WaitAll(..., Timeout)), but it does not cancel its CancellationTokenSource when that happens. That can leave work running in the background until the caller later invokes Stop(). Consider canceling the linked CancellationTokenSource when a timeout is detected so tasks get a timely cancellation signal.

Copilot uses AI. Check for mistakes.
Comment on lines +444 to +446
catch (Exception ex)
{
throw new FitnessException(Fitness, "Error executing Fitness.Evaluate for chromosome: {0}".With(ex.Message), ex);
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

The async fitness path wraps exceptions using a message that still references Fitness.Evaluate. This is misleading for IAsyncFitness implementations and makes diagnosing async failures harder. Update the message to reference EvaluateAsync (and consider special-casing OperationCanceledException if you want cancellations to be distinguishable from evaluation failures).

Suggested change
catch (Exception ex)
{
throw new FitnessException(Fitness, "Error executing Fitness.Evaluate for chromosome: {0}".With(ex.Message), ex);
catch (OperationCanceledException)
{
// Preserve cancellation semantics without wrapping in FitnessException.
throw;
}
catch (Exception ex)
{
throw new FitnessException(Fitness, "Error executing Fitness.EvaluateAsync for chromosome: {0}".With(ex.Message), ex);

Copilot uses AI. Check for mistakes.
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));
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

ValidateLength now uses MinChromosomeLength, but the exception message still hardcodes "at least, 3 genes". If a derived mutation overrides MinChromosomeLength, this message becomes incorrect. Build the message using MinChromosomeLength (and consider pluralization for "gene(s)") so the error stays accurate.

Suggested change
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} {1}. {2} has only {3} {4}."
.With(
MinChromosomeLength,
MinChromosomeLength == 1 ? "gene" : "genes",
chromosome.GetType().Name,
chromosome.Length,
chromosome.Length == 1 ? "gene" : "genes"));

Copilot uses AI. Check for mistakes.
Comment on lines +745 to +762
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();
});
Copy link

Copilot AI Feb 8, 2026

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.
…eledException handling, dynamic error messages
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants