From f39943e2fe9f5ad738bff23bd4c52930e48b3070 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 19:22:27 +0000 Subject: [PATCH 1/3] Modernise to .NET 8 and compile bots in memory with expression trees - Convert to SDK-style projects targeting net8.0; Common.Logging 3.4.1; MSTest 3. - Replace CodeDom source/DLL generation with System.Linq.Expressions delegates. Nothing is written to disk and no assemblies leak. TBot must now be concrete with a parameterless constructor; RunBotLogic is virtual and runs the delegate. - Remove dead BotBreeder, legacy test settings and VS accessor files. - Fix SimulateGenerations returning no bots when info logging was disabled. - Add end-to-end tests that compile, run and evolve a CounterBot. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01PfG6MnHg74EM7ndCo5atss --- GeneCodeCS.Test/BotGeneratorTest.cs | 10 +- GeneCodeCS.Test/GeneCodeCS.Test.csproj | 89 +---- GeneCodeCS.Test/Helpers/CounterBot.cs | 36 ++ GeneCodeCS.Test/PopulationTests.cs | 42 +++ GeneCodeCS.Test/Properties/AssemblyInfo.cs | 35 -- .../Test References/GeneCodeCS.accessor | 2 - GeneCodeCS.Test/packages.config | 4 - GeneCodeCS.sln | 66 ++-- GeneCodeCS.vsmdi | 6 - GeneCodeCS/BaseBot.cs | 31 +- GeneCodeCS/BotBreeder.cs | 134 ------- GeneCodeCS/BotCompileException.cs | 16 +- GeneCodeCS/Compilation.cs | 326 ++++-------------- GeneCodeCS/Execution.cs | 2 +- GeneCodeCS/GeneCodeCS.csproj | 113 +----- GeneCodeCS/Population.cs | 9 +- GeneCodeCS/Properties/AssemblyInfo.cs | 37 -- GeneCodeCS/Reproduction.cs | 4 +- GeneCodeCS/UnknownGenotypeException.cs | 14 - GeneCodeCS/packages.config | 7 - Local.testsettings | 10 - README.md | 10 +- TraceAndTestImpact.testsettings | 9 - 23 files changed, 260 insertions(+), 752 deletions(-) create mode 100644 GeneCodeCS.Test/Helpers/CounterBot.cs create mode 100644 GeneCodeCS.Test/PopulationTests.cs delete mode 100644 GeneCodeCS.Test/Properties/AssemblyInfo.cs delete mode 100644 GeneCodeCS.Test/Test References/GeneCodeCS.accessor delete mode 100644 GeneCodeCS.Test/packages.config delete mode 100644 GeneCodeCS.vsmdi delete mode 100644 GeneCodeCS/BotBreeder.cs delete mode 100644 GeneCodeCS/Properties/AssemblyInfo.cs delete mode 100644 GeneCodeCS/packages.config delete mode 100644 Local.testsettings delete mode 100644 TraceAndTestImpact.testsettings diff --git a/GeneCodeCS.Test/BotGeneratorTest.cs b/GeneCodeCS.Test/BotGeneratorTest.cs index 3af83cd..226d162 100644 --- a/GeneCodeCS.Test/BotGeneratorTest.cs +++ b/GeneCodeCS.Test/BotGeneratorTest.cs @@ -26,13 +26,11 @@ namespace GeneCodeCS.Test public class BotGeneratorTest { [TestMethod] - [DeploymentItem("GeneCodeCS.dll")] public void BotGeneratorCreateBotNameCreatesExpectedNameGivenParameters1() { BotGeneratorCreateBotNameCreatesExpectedNameGivenParameters1Helper(); } [TestMethod] - [DeploymentItem("GeneCodeCS.dll")] public void BotGeneratorCreateBotNameCreatesExpectedNameGivenParameters2() { BotGeneratorCreateBotNameCreatesExpectedNameGivenParameters2Helper(); } @@ -42,19 +40,19 @@ public void BotGeneratorCreateRandomBotReturnsNonNullBotTree() { BotGeneratorCreateRandomBotReturnsNonNullBotTreeHelper(); } - private static void BotGeneratorCreateBotNameCreatesExpectedNameGivenParameters1Helper() { + private static void BotGeneratorCreateBotNameCreatesExpectedNameGivenParameters1Helper() where TBot : BaseBot { const int generation = 0; const int populus = 0; const string expected = "BaseBot_Gen0_Bot0"; - var actual = Reproduction_Accessor.CreateBotName(generation, populus); + var actual = Reproduction.CreateBotName(generation, populus); Assert.AreEqual(expected, actual); } - private static void BotGeneratorCreateBotNameCreatesExpectedNameGivenParameters2Helper() { + private static void BotGeneratorCreateBotNameCreatesExpectedNameGivenParameters2Helper() where TBot : BaseBot { const int generation = 4; const int populus = 5; const string expected = "BaseBot_Gen4_Bot5"; - var actual = Reproduction_Accessor.CreateBotName(generation, populus); + var actual = Reproduction.CreateBotName(generation, populus); Assert.AreEqual(expected, actual); } diff --git a/GeneCodeCS.Test/GeneCodeCS.Test.csproj b/GeneCodeCS.Test/GeneCodeCS.Test.csproj index 9eed16d..654ccba 100644 --- a/GeneCodeCS.Test/GeneCodeCS.Test.csproj +++ b/GeneCodeCS.Test/GeneCodeCS.Test.csproj @@ -1,82 +1,21 @@ - - + + - Debug - AnyCPU - - - 2.0 - {70CC27C4-4073-4C9F-8822-A7B3313038BC} - Library - Properties + net8.0 GeneCodeCS.Test - GeneCodeCS.Test - v4.0 - 512 - {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + latest + false + $(NoWarn);CS8981 - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\packages\Common.Logging.2.1.2\lib\net40\Common.Logging.dll - - - - - - 3.5 - - - - - False - - - - - - - - - - - - - - - + - - {0A7CCAFD-4FB8-4739-A260-7B384E093469} - GeneCodeCS - + + + + - - + - - - \ No newline at end of file + + diff --git a/GeneCodeCS.Test/Helpers/CounterBot.cs b/GeneCodeCS.Test/Helpers/CounterBot.cs new file mode 100644 index 0000000..81ed12e --- /dev/null +++ b/GeneCodeCS.Test/Helpers/CounterBot.cs @@ -0,0 +1,36 @@ +using System; + +namespace GeneCodeCS.Test.Helpers +{ + /// + /// A tiny bot for end-to-end tests: it has to reach a target number by choosing between increments and decrements. + /// + public class CounterBot : BaseBot + { + private const int Target = 10; + private int _steps; + + public int Counter { get; private set; } + + public void Increment(StepParameter step) { Counter += step.Value; } + + public void Decrement(StepParameter step) { Counter -= step.Value; } + + public bool BelowTarget() { return Counter < Target; } + + protected override bool NotFinished() { return ++_steps <= 20; } + + protected override void Evaluate() { Fitness = 1000 - Math.Abs(Target - Counter); } + + public class StepParameter : Genetics.IParameter + { + private readonly int _value; + + public StepParameter(Random random) { _value = random.Next(1, 4); } + + public StepParameter(int value) { _value = value; } + + public int Value { get { return _value; } } + } + } +} diff --git a/GeneCodeCS.Test/PopulationTests.cs b/GeneCodeCS.Test/PopulationTests.cs new file mode 100644 index 0000000..0393f37 --- /dev/null +++ b/GeneCodeCS.Test/PopulationTests.cs @@ -0,0 +1,42 @@ +using System.Linq; +using GeneCodeCS.Test.Helpers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GeneCodeCS.Test +{ + [TestClass] + public class PopulationTests + { + [TestMethod] + public void SimulateGenerations_compiles_runs_and_returns_best_bots_without_writing_files() { + var before = System.IO.Directory.GetFiles(System.IO.Directory.GetCurrentDirectory(), "*.cs"); + var population = new Population(); + + var best = population.SimulateGenerations(null, generationLimit: 5, botsPerGeneration: 20, maxTreeDepth: 3); + + Assert.IsTrue(best.Count > 0); + Assert.IsTrue(best.All(b => b.Bot != null)); + Assert.IsTrue(best.All(b => b.Bot.Fitness > int.MinValue), "Bots should run without throwing."); + CollectionAssert.AreEquivalent(before, System.IO.Directory.GetFiles(System.IO.Directory.GetCurrentDirectory(), "*.cs")); + } + + [TestMethod] + public void SimulateGenerations_evolves_a_good_solution_to_the_counter_problem() { + var population = new Population(); + + var best = population.SimulateGenerations(null, generationLimit: 30, botsPerGeneration: 60, maxTreeDepth: 3, + fitnessThreshold: 999); + + Assert.AreEqual(1000, best.First().Bot.Fitness); + } + + [TestMethod] + public void SimulateIndividuals_returns_a_bot_exceeding_the_threshold() { + var population = new Population(); + + var bot = population.SimulateIndividuals(null, maxTreeDepth: 3, fitnessThreshold: 995); + + Assert.IsTrue(bot.Bot.Fitness > 995); + } + } +} diff --git a/GeneCodeCS.Test/Properties/AssemblyInfo.cs b/GeneCodeCS.Test/Properties/AssemblyInfo.cs deleted file mode 100644 index 72950dd..0000000 --- a/GeneCodeCS.Test/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -// -// GeneCodeCS - Genetic programming library for code bot natural selection. -// Copyright (C) 2013 Edd Porter -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see {http://www.gnu.org/licenses/}. -// - -using System.Reflection; -using System.Resources; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("GeneCodeCS.Test")] -[assembly: AssemblyDescription("Test for the genetic programming library for code bot natural selection.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Edd Porter")] -[assembly: AssemblyProduct("GeneCodeCS.Test")] -[assembly: AssemblyCopyright("Copyright © Edd Porter 2013")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: ComVisible(false)] -[assembly: Guid("6bcff89f-5aa3-4eb1-9810-8c62939780db")] -[assembly: AssemblyVersion("0.1.0.0")] -[assembly: AssemblyFileVersion("0.1.0.0")] -[assembly: NeutralResourcesLanguageAttribute("en-GB")] \ No newline at end of file diff --git a/GeneCodeCS.Test/Test References/GeneCodeCS.accessor b/GeneCodeCS.Test/Test References/GeneCodeCS.accessor deleted file mode 100644 index faa0e68..0000000 --- a/GeneCodeCS.Test/Test References/GeneCodeCS.accessor +++ /dev/null @@ -1,2 +0,0 @@ -GeneCodeCS.dll -Desktop diff --git a/GeneCodeCS.Test/packages.config b/GeneCodeCS.Test/packages.config deleted file mode 100644 index 78ac149..0000000 --- a/GeneCodeCS.Test/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/GeneCodeCS.sln b/GeneCodeCS.sln index 03313b6..4531972 100644 --- a/GeneCodeCS.sln +++ b/GeneCodeCS.sln @@ -1,38 +1,28 @@ - -Microsoft Visual Studio Solution File, Format Version 11.00 -# Visual Studio 2010 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeneCodeCS", "GeneCodeCS\GeneCodeCS.csproj", "{0A7CCAFD-4FB8-4739-A260-7B384E093469}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeneCodeCS.Test", "GeneCodeCS.Test\GeneCodeCS.Test.csproj", "{70CC27C4-4073-4C9F-8822-A7B3313038BC}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DA875293-158A-47BB-AA48-1D64F41B6DAA}" - ProjectSection(SolutionItems) = preProject - GeneCodeCS.vsmdi = GeneCodeCS.vsmdi - LICENSE = LICENSE - Local.testsettings = Local.testsettings - README.md = README.md - TraceAndTestImpact.testsettings = TraceAndTestImpact.testsettings - EndProjectSection -EndProject -Global - GlobalSection(TestCaseManagementSettings) = postSolution - CategoryFile = GeneCodeCS.vsmdi - EndGlobalSection - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {0A7CCAFD-4FB8-4739-A260-7B384E093469}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0A7CCAFD-4FB8-4739-A260-7B384E093469}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0A7CCAFD-4FB8-4739-A260-7B384E093469}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0A7CCAFD-4FB8-4739-A260-7B384E093469}.Release|Any CPU.Build.0 = Release|Any CPU - {70CC27C4-4073-4C9F-8822-A7B3313038BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {70CC27C4-4073-4C9F-8822-A7B3313038BC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {70CC27C4-4073-4C9F-8822-A7B3313038BC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {70CC27C4-4073-4C9F-8822-A7B3313038BC}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeneCodeCS", "GeneCodeCS\GeneCodeCS.csproj", "{C5674E3C-5E3B-47E2-98A0-79F40D6E3C53}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeneCodeCS.Test", "GeneCodeCS.Test\GeneCodeCS.Test.csproj", "{9A4746C8-5664-418A-B09F-356D38BEA082}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C5674E3C-5E3B-47E2-98A0-79F40D6E3C53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5674E3C-5E3B-47E2-98A0-79F40D6E3C53}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5674E3C-5E3B-47E2-98A0-79F40D6E3C53}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5674E3C-5E3B-47E2-98A0-79F40D6E3C53}.Release|Any CPU.Build.0 = Release|Any CPU + {9A4746C8-5664-418A-B09F-356D38BEA082}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A4746C8-5664-418A-B09F-356D38BEA082}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A4746C8-5664-418A-B09F-356D38BEA082}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A4746C8-5664-418A-B09F-356D38BEA082}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/GeneCodeCS.vsmdi b/GeneCodeCS.vsmdi deleted file mode 100644 index aab22b1..0000000 --- a/GeneCodeCS.vsmdi +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/GeneCodeCS/BaseBot.cs b/GeneCodeCS/BaseBot.cs index 8e1a5f1..071eaaf 100644 --- a/GeneCodeCS/BaseBot.cs +++ b/GeneCodeCS/BaseBot.cs @@ -16,6 +16,8 @@ // along with this program. If not, see {http://www.gnu.org/licenses/}. // +using System; +using System.Reflection; using Common.Logging; using GeneCodeCS.Genetics; @@ -32,6 +34,18 @@ namespace GeneCodeCS /// public abstract class BaseBot { + /// + /// The method, for use by the generated code. + /// + internal static readonly MethodInfo NotFinishedMethod = typeof(BaseBot).GetMethod("IsNotFinished", + BindingFlags.Instance | + BindingFlags.NonPublic); + + /// + /// The compiled chromosome, set when the bot is created from its expression tree. + /// + private Action _logic; + /// /// Gets custom information about the bot as configured by the bot's implementation of the method. /// @@ -73,10 +87,25 @@ public virtual void Initialise(object parameters) { } /// Whether the execution cycle is complete. protected abstract bool NotFinished(); + /// + /// Allows the generated code to call without it being public. + /// + internal bool IsNotFinished() { return NotFinished(); } + + /// + /// Sets the compiled chromosome that will run. + /// + internal void SetLogic(Action logic) { _logic = logic; } + /// /// Contains the bot logic. Implemented by running or . /// - protected abstract void RunBotLogic(); + protected virtual void RunBotLogic() { + if (_logic == null) { + throw new InvalidOperationException("The bot has not been compiled."); + } + _logic(); + } } } \ No newline at end of file diff --git a/GeneCodeCS/BotBreeder.cs b/GeneCodeCS/BotBreeder.cs deleted file mode 100644 index 096f5ee..0000000 --- a/GeneCodeCS/BotBreeder.cs +++ /dev/null @@ -1,134 +0,0 @@ -// -// GeneCodeCS - Genetic programming library for code bot natural selection. -// Copyright (C) 2013 Edd Porter -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see {http://www.gnu.org/licenses/}. -// - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using Common.Logging; -using GeneCodeCS.Genetics; - -namespace GeneCodeCS -{ - /// - /// This class generates a population of randomised bot code, runs the bots through a set of tests, and then breeds the best bots together to create a new generation. The process repeats for a set number of generations or until a fitness threshold is reached. - /// - /// The bot class type to breed. Must inherit from . - public sealed class BotBreeder where TBot : BaseBot - { - // TODO: implement fitness threshold termination. - - /// Not null. - private readonly BotCodeCreator _codeCreator; - - /// Not null. - private readonly BotGenerator _generator; - - /// Not null. - private readonly ILog _log; - - /// Can be null. - private readonly Func _optimise; - - /// Not null. - private readonly BotRunner _runner; - - public BotBreeder(ILog log, Func optimise = null) { - if (log == null) { - throw new ArgumentNullException("log"); - } - - _log = log; - _optimise = optimise; - - _runner = new BotRunner(log); - _codeCreator = new BotCodeCreator(log); - _generator = new BotGenerator(_log); - } - - public List Run(int generations = 20, int maxTreeDepth = 3, int population = 30, T parameters = default(T), - List starterBots = null) { - var previousGenerations = new List>(); - if (starterBots != null) { - previousGenerations.Add(starterBots); - } - - var generator = new BotGenerator(_log); - var bestBots = new List(); - for (var generation = 0; generation < generations; ++generation) { - _log.Info(string.Format("Breeding generation {0:N}", generation)); - - // 1. Generate expression trees for generation. - var lastGeneration = previousGenerations.LastOrDefault(); - var lastGenerationReports = lastGeneration != null - ? lastGeneration.Select(g => g.TerminationReport).ToList() - : null; - var thisGeneration = generator.CreateNewGeneration(generation, lastGenerationReports, population, maxTreeDepth, - _optimise); - - // 2. Convert expression trees to code. - var bots = _codeCreator.CreateBotCode(thisGeneration, generation); - - // 3. Run code to generate fitness. - var newGeneration = - _runner.RunGeneration(bots, parameters).OrderByDescending(report => report.TerminationReport.Fitness).ToList(); - - // 4. Evaluate bot fitness. - var bestFitness = newGeneration.First().TerminationReport.Fitness; - var worstFitness = newGeneration.Last().TerminationReport.Fitness; - var averageFitness = newGeneration.Average(fp => fp.TerminationReport.Fitness); - _log.Info(string.Format("Generation {0} report: Best {1:N} / Average {2:N2} / Worst {3:N}", generation, - bestFitness, averageFitness, worstFitness)); - - bestBots = newGeneration.Where(b => b.TerminationReport.Fitness == bestFitness).ToList(); - _log.Info(string.Format("Best bots: {0}", string.Join(", ", bestBots.Select(b => b.TerminationReport.Bot.Name)))); - - previousGenerations.Add(newGeneration); - } - - _log.Info("Bot creation complete."); - foreach (var bot in bestBots) { - _log.Info(string.Format("Bot {0}: Fitness = {1}", bot.TerminationReport.Bot.Name, bot.TerminationReport.Fitness)); - } - return bestBots; - } - - public TBot RunSingle(int maxTreeDepth = 3, T parameters = default(T), int fitnessThreshold = 0) { - while (true) { - // 1. Generate single bot. - var bot = _generator.CreateRandomBot(maxTreeDepth, _optimise); - var thisGeneration = new List {bot}; - - // 2. Convert expression tree to code. - const int generation = 0; - var bots = _codeCreator.CreateBotCode(thisGeneration, generation); - - // 3. Run code to generate fitness. - var newBot = _runner.RunGeneration(bots, parameters).FirstOrDefault(); - - Debug.Assert(newBot != null, "newBot != null"); - - // 4. Evaluate bot fitness. - _log.Info(string.Format("{0} report: {1:N}", newBot.TerminationReport.Bot.Name, newBot.TerminationReport.Fitness)); - if (newBot.TerminationReport.Fitness > fitnessThreshold) { - return newBot; - } - } - } - } -} \ No newline at end of file diff --git a/GeneCodeCS/BotCompileException.cs b/GeneCodeCS/BotCompileException.cs index 20f3c8b..2eb41ea 100644 --- a/GeneCodeCS/BotCompileException.cs +++ b/GeneCodeCS/BotCompileException.cs @@ -1,4 +1,4 @@ -// +// // GeneCodeCS - Genetic programming library for code bot natural selection. // Copyright (C) 2013 Edd Porter // @@ -17,14 +17,12 @@ // using System; -using System.Runtime.Serialization; namespace GeneCodeCS { /// /// Thrown when the compilation phase of bot generation fails. /// - [Serializable] public class BotCompileException : Exception { /// @@ -45,17 +43,5 @@ public BotCompileException(string message) : base(message) { } /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if no inner exception is specified. public BotCompileException(string message, Exception innerException) : base(message, innerException) { } - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - /// The - /// - /// parameter is null. - /// The class name is null or - /// - /// is zero (0). - protected BotCompileException(SerializationInfo info, StreamingContext context) : base(info, context) { } } } \ No newline at end of file diff --git a/GeneCodeCS/Compilation.cs b/GeneCodeCS/Compilation.cs index 2e152aa..1a5afd6 100644 --- a/GeneCodeCS/Compilation.cs +++ b/GeneCodeCS/Compilation.cs @@ -1,4 +1,4 @@ -// +// // GeneCodeCS - Genetic programming library for code bot natural selection. // Copyright (C) 2013 Edd Porter // @@ -17,37 +17,31 @@ // using System; -using System.CodeDom; -using System.CodeDom.Compiler; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; +using System.Linq.Expressions; using System.Reflection; -using System.Text; using Common.Logging; using GeneCodeCS.Genetics; using GeneCodeCS.Properties; -using Microsoft.CSharp; namespace GeneCodeCS { /// - /// This class converts Chromosomes into C# code and then compiles them into an assembly. + /// This class converts Chromosomes into executable delegates by building and compiling expression trees in memory. /// - /// The bot class type to breed. It must inherit from . Its public void-type (actions) and bool-type (decision points) methods will be used to construct the genes for the bot; their parameters must derive from The bot class type to breed. It must inherit from and have a public parameterless constructor. Its public void-type (actions) and bool-type (decision points) methods will be used to construct the genes for the bot; their parameters must derive from . See the documentation for restrictions and requirements of implementing this class type. internal sealed class Compilation where TBot : BaseBot { - // TODO: Allow specification of source code and DLL output destinations. - /// /// Used to log status information. /// /// /// Not null. /// - private readonly ILog _log = LogManager.GetCurrentClassLogger(); + private readonly ILog _log = LogManager.GetLogger(typeof(Compilation)); /// /// Initialises a new instance of the class. @@ -55,292 +49,124 @@ internal sealed class Compilation where TBot : BaseBot public Compilation() { _log.Trace("ctor: Constructing class."); } /// - /// Converts a bot into C# code and compiles it into an assembly. The code and assembly file names are constructed from the class name and the . + /// Compiles a bot's chromosome and creates the bot instance that will run it. /// - /// The bot to compile. The compiled bot type is added in-place to this object. - /// The generation number of the bot. This is used to create the file name and namespace for the compiled bot. + /// The bot to compile. The bot instance is added in-place to this object. /// If the compilation was successful, returns true ; otherwise returns false . - public bool CompileBot(BotInformation bot, int generationNumber) { + public bool CompileBot(BotInformation bot) { if (bot == null || string.IsNullOrWhiteSpace(bot.Name) || bot.Tree == null) { throw new ArgumentNullException("bot", Resources.ValidBotRequired); } - return CompileBots(new List> { bot }, generationNumber); + return CompileBots(new List> { bot }); } /// - /// Takes a list of bots, converts them into C# code, and compiles into an assembly. The code and assembly file names are constructed from the class name and the . + /// Compiles the chromosome of each bot into a delegate and creates the bot instances that will run them. /// - /// The bots to compile. The compiled bot types are added in-place to the bot objects in this list. - /// The generation number of the bots. This is used to create the file names and namespace for the compiled bots. + /// The bots to compile. The bot instances are added in-place to the bot objects in this list. /// If the compilation was successful, returns true ; otherwise returns false . - public bool CompileBots(IList> bots, int generationNumber) { + public bool CompileBots(IList> bots) { if (bots == null || bots.Count == 0 || bots.Any(b => string.IsNullOrWhiteSpace(b.Name) || b.Tree == null)) { throw new ArgumentNullException("bots", Resources.ValidBotCollectionRequired); } - if (generationNumber < 0) { - throw new ArgumentNullException("generationNumber", Resources.GenerationNumberValidRange); - } - - _log.TraceFormat("CompileBots: Compiling {0} bots in generation {1}.", bots.Count, generationNumber); - - var namespaceName = string.Format("{0}{1}", typeof(TBot).Namespace, generationNumber); - _log.InfoFormat("Compiling into namespace {0}.", namespaceName); - - _log.Trace("CompileBots: Generating code."); - var codeCompileUnit = BuildCompileUnit(bots, namespaceName); - GenerateCode(namespaceName, codeCompileUnit); - - _log.Trace("CompileBots: Compiling code."); - var compiledBots = CompileCode(namespaceName); - if (compiledBots == null) { - return false; - } - _log.Trace("CompileBots: Creating bots from assembly."); - var types = compiledBots.CompiledAssembly.GetTypes(); - foreach (var type in types) { - bots.Single(bot => bot.Name == type.Name).Bot = (TBot)Activator.CreateInstance(type); + _log.TraceFormat("CompileBots: Compiling {0} bots.", bots.Count); + + foreach (var bot in bots) { + try { + var logic = BuildBotLogic(bot.Tree).Compile(); + var instance = Activator.CreateInstance(); + instance.SetLogic(() => logic(instance)); + bot.Bot = instance; + } catch (Exception ex) { + _log.ErrorFormat("Errors compiling bot '{0}': {1}", ex, bot.Name, ex.Message); + return false; + } } return true; } /// - /// Builds the class container for the bot. Essentially this is the bot itself: it contains the constructor and an implementation of the bot's . The type is used as the base class. - /// - /// The name of the class to create. - /// The to encapsulate in the class's RunBotLogic method. - /// The class declaration. - private static CodeTypeDeclaration BuildClass(string name, Chromosome chromosome) { - Debug.Assert(!string.IsNullOrWhiteSpace(name), "A name for the class must be specified."); - Debug.Assert(chromosome != null, "A valid Chromosome must be provided to build into a class."); - - var codeType = new CodeTypeDeclaration(name); - codeType.BaseTypes.Add(typeof(TBot).Name); - codeType.Members.Add(BuildRunBotLogicMethod(chromosome)); - return codeType; - } - - /// - /// Converts bots into code compilation units within a specified . + /// Builds the bot's main loop: while (bot.NotFinished()) { /* execute the Chromosome instructions */ } . /// - /// The bots to compile. - /// The namespace in which to place the code. - /// The compilation unit for the bots. - private static CodeCompileUnit BuildCompileUnit(IEnumerable> bots, string @namespace) { - Debug.Assert(bots != null, Resources.ValidBotCollectionRequired); - Debug.Assert(!string.IsNullOrWhiteSpace(@namespace), "A valid namespace name must be provided."); - - var codeCompileUnit = new CodeCompileUnit(); - codeCompileUnit.ReferencedAssemblies.Add(typeof(TBot).Assembly.FullName); - codeCompileUnit.ReferencedAssemblies.Add(typeof(ILog).Assembly.FullName); - - // namespace Namespace { - var namespaces = new CodeNamespace(@namespace); - // using TBot.Namespace; - namespaces.Imports.Add(new CodeNamespaceImport(typeof(TBot).Namespace)); - // using Common.Logging; - namespaces.Imports.Add(new CodeNamespaceImport("Common.Logging")); - - foreach (var botType in bots.Select(b => BuildClass(b.Name, b.Tree))) { - // public class Bot { - namespaces.Types.Add(botType); - // } - } - // } + /// The instruction tree to convert. + /// A lambda that runs the instructions against a bot instance. + private static Expression> BuildBotLogic(Chromosome chromosome) { + Debug.Assert(chromosome != null, "A valid chromosome must be provided."); - codeCompileUnit.Namespaces.Add(namespaces); - return codeCompileUnit; + var bot = Expression.Parameter(typeof(TBot), "bot"); + var exit = Expression.Label("exit"); + var loop = Expression.Loop(Expression.IfThenElse(Expression.Call(bot, BaseBot.NotFinishedMethod), + BuildGeneExpression(bot, chromosome), Expression.Break(exit)), exit); + return Expression.Lambda>(loop, bot); } /// - /// Converts the genes within the into code statements. + /// Converts the genes within the into an expression. /// + /// The parameter expression holding the bot instance on which to invoke the gene methods. /// The chromosome to interpret. - /// An array of code statements. - private static CodeStatement[] BuildGeneStatements(Chromosome chromosome) { + /// An expression of type void . + private static Expression BuildGeneExpression(ParameterExpression bot, Chromosome chromosome) { Debug.Assert(chromosome != null, "A valid chromosome must be provided."); Debug.Assert(chromosome.Node != null, "The chromosome must contain a valid genotype."); - var codeStatements = new CodeStatementCollection(); var node = chromosome.Node; - // The while loop acts as a control statement that can be broken out of once the correct expression type has - // finished processing. If the bottom of the while loop is reached, then an unknown IGene implementation has been - // encountered, so an exception is thrown. This method, whilst the same as, means a goto statement doesn't need - // to be used, nor does evaluation need to continue once the correct IGene implementation has been processed. - while (true) { - var branch = node as BranchExpression; - if (branch != null) { - // if (this.Method(params)) { - var ifCondition = new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), branch.MethodInfo.Name, - BuildParametersArray(branch.MethodInfo.GetParameters(), - branch.Parameters)); - // // execute true branch - var ifTrue = BuildGeneStatements(branch.TrueBranch); - // } else { - // // execute false branch - var ifFalse = BuildGeneStatements(branch.FalseBranch); - // } - - var ifStatement = new CodeConditionStatement(ifCondition, ifTrue, ifFalse); - codeStatements.Add(ifStatement); - break; - } - - var sequence = node as SequenceExpression; - if (sequence != null) { - foreach (var expression in sequence.Expressions) { - codeStatements.AddRange(BuildGeneStatements(expression)); - } - break; - } - - var terminal = node as TerminalExpression; - if (terminal != null) { - // this.Method(params); - var expression = new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), terminal.MethodInfo.Name, - BuildParametersArray(terminal.MethodInfo.GetParameters(), - terminal.Parameters)); - codeStatements.Add(expression); - break; - } - - throw new UnknownGenotypeException("An unknown Genotype was encountered."); + var branch = node as BranchExpression; + if (branch != null) { + // if (bot.Method(params)) { /* true branch */ } else { /* false branch */ } + var condition = BuildMethodCall(bot, branch.MethodInfo, branch.Parameters); + return Expression.IfThenElse(condition, BuildGeneExpression(bot, branch.TrueBranch), + BuildGeneExpression(bot, branch.FalseBranch)); } - var statementArray = new CodeStatement[codeStatements.Count]; - codeStatements.CopyTo(statementArray, 0); - return statementArray; - } - - /// - /// Converts an array of parameter and corresponding entries in an array of into an array of s that can be passed to a . - /// - /// - /// The arrays and must contain the same number of entries. - /// - /// An array of information describing the parameters. - /// The values that each parameter will take. - /// An array of code expressions. - private static CodeExpression[] BuildParametersArray(IList parameterInfo, IList values) { - Debug.Assert(parameterInfo != null, "A valid list of parameters must be provided."); - Debug.Assert(values != null, "A valid list of parameter values must be provided"); - Debug.Assert(parameterInfo.Count == values.Count, "Array lengths not equal."); - - var parameterCollection = new CodeExpressionCollection(); - for (var index = 0; index < parameterInfo.Count; ++index) { - var expression = new CodeObjectCreateExpression(new CodeTypeReference(parameterInfo[index].ParameterType.Name), - new CodePrimitiveExpression(values[index])); - parameterCollection.Add(expression); + var sequence = node as SequenceExpression; + if (sequence != null) { + var statements = sequence.Expressions.Select(e => BuildGeneExpression(bot, e)).ToList(); + return statements.Count == 0 ? (Expression)Expression.Empty() : Expression.Block(typeof(void), statements); } - var parameters = new CodeExpression[parameterCollection.Count]; - parameterCollection.CopyTo(parameters, 0); - return parameters; - } - - /// - /// Builds the RunBotLogic method that will run the instructions encoded in the specified . - /// - /// The instruction tree to convert to code. - /// The code member for the RunBotLogic method. - private static CodeTypeMember BuildRunBotLogicMethod(Chromosome chromosome) { - Debug.Assert(chromosome != null, "A valid chromosome must be provided."); - - // protected override void RunBotLogic() { - var method = new CodeMemberMethod { - ReturnType = new CodeTypeReference(typeof(void)), - Attributes = (MemberAttributes.Family + (int)MemberAttributes.Override), - Name = "RunBotLogic" - }; - - // for ( ; this.NotFinished(); ) { - // // execute the Chromosome instructions - // } - var noop = new CodeSnippetStatement(""); - var notFinishedCall = new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), "NotFinished"); - var whileLoop = new CodeIterationStatement(noop, notFinishedCall, noop, BuildGeneStatements(chromosome)); - - method.Statements.Add(whileLoop); - return method; + var terminal = node as TerminalExpression; + if (terminal != null) { + // bot.Method(params); + return Expression.Block(typeof(void), BuildMethodCall(bot, terminal.MethodInfo, terminal.Parameters)); + } - // } + throw new UnknownGenotypeException("An unknown Genotype was encountered."); } /// - /// Writes the built namespace to C# code. The file is output in the working directory with the name <filename>.cs . + /// Builds a call of a gene's method on the bot, constructing each from its stored value. /// - /// The name of the - /// The code unit to write to C#. - private static void GenerateCode(string filename, CodeCompileUnit codeUnit) { - Debug.Assert(!string.IsNullOrWhiteSpace(filename), "A valid destination filename must be provided."); - Debug.Assert(codeUnit != null, "A valid code unit must be provieded."); - - const bool noAppend = false; + private static MethodCallExpression BuildMethodCall(Expression bot, MethodInfo method, IList values) { + Debug.Assert(method != null, "A valid method must be provided."); + Debug.Assert(values != null, "A valid list of parameter values must be provided"); - var sw = new StreamWriter(string.Format("{0}.cs", filename), noAppend); - using (var itw = new IndentedTextWriter(sw, " ")) { - using (var codeProvider = new CSharpCodeProvider()) { - codeProvider.GenerateCodeFromCompileUnit(codeUnit, itw, new CodeGeneratorOptions()); - } + var parameters = method.GetParameters(); + var arguments = new Expression[parameters.Length]; + for (var index = 0; index < parameters.Length; ++index) { + arguments[index] = index < values.Count + ? BuildParameter(parameters[index].ParameterType, values[index]) + : Expression.Constant(parameters[index].DefaultValue, parameters[index].ParameterType); } + return Expression.Call(bot, method, arguments); } /// - /// Compiles a generated C# bot code file into a DLL. + /// Builds an expression that creates an of the given type, passing it the stored value. /// - /// The C# bot code file name. - /// The results of the compilation, including a reference to the compiled assembly. - private CompilerResults CompileCode(string filename) { - Debug.Assert(!string.IsNullOrWhiteSpace(filename), "A valid source filename must be provided."); - - var provider = new CSharpCodeProvider(); - var options = new CompilerParameters - { GenerateInMemory = true, GenerateExecutable = false, IncludeDebugInformation = false }; - options.ReferencedAssemblies.Add("System.dll"); - options.ReferencedAssemblies.Add("Common.Logging.dll"); - options.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().ManifestModule.ScopeName); - - // Ensure that the assembly in which the provided TBot type is declared is included, along with any classes from - // which it inherits (which may included generics). - var botType = typeof(TBot); - while (botType != null && botType != typeof(object)) { - options.ReferencedAssemblies.Add(Assembly.GetAssembly(botType).ManifestModule.ScopeName); - if (botType.IsGenericType) { - foreach (var gt in botType.GetGenericArguments()) { - options.ReferencedAssemblies.Add(Assembly.GetAssembly(gt).ManifestModule.ScopeName); - } - } - botType = botType.BaseType; - } - options.OutputAssembly = string.Format("{0}.dll", filename); - - // Invoke compilation. - var sourceFile = string.Format("{0}.cs", filename); - Debug.Assert(File.Exists(sourceFile), "The code file must exist."); - var results = provider.CompileAssemblyFromFile(options, sourceFile); - - if (results.Errors.Count <= 0) { - _log.InfoFormat("Source {0} built successfully.", sourceFile); - return results; - } - - // Display compilation errors. - _log.ErrorFormat("Errors building {0} into {1}", sourceFile, results.PathToAssembly); - var errors = new StringBuilder(); - errors.AppendLine("Build encountered the following errors:"); - foreach (CompilerError ce in results.Errors) { - errors.AppendLine(string.Format(" {0}", ce)); + private static Expression BuildParameter(Type parameterType, object value) { + // The value constructor is the single-argument constructor accepting the stored value (not the Random one). + var constructor = parameterType.GetConstructors() + .Where(c => c.GetParameters().Length == 1) + .FirstOrDefault(c => c.GetParameters()[0].ParameterType.IsInstanceOfType(value)); + if (constructor == null) { + throw new BotCompileException(string.Format("{0} has no public constructor accepting a {1}.", + parameterType.Name, value == null ? "null" : value.GetType().Name)); } - _log.TraceFormat("CompileCode: {0}", errors); - return null; + return Expression.New(constructor, Expression.Constant(value, constructor.GetParameters()[0].ParameterType)); } } -} \ No newline at end of file +} diff --git a/GeneCodeCS/Execution.cs b/GeneCodeCS/Execution.cs index 2f5a8f4..c54aa34 100644 --- a/GeneCodeCS/Execution.cs +++ b/GeneCodeCS/Execution.cs @@ -37,7 +37,7 @@ internal sealed class Execution where TBot : BaseBot /// /// Not null. /// - private readonly ILog _log = LogManager.GetCurrentClassLogger(); + private readonly ILog _log = LogManager.GetLogger(typeof(Execution)); /// /// Initialises a new instance of the class. diff --git a/GeneCodeCS/GeneCodeCS.csproj b/GeneCodeCS/GeneCodeCS.csproj index dddd8ec..f20aa4d 100644 --- a/GeneCodeCS/GeneCodeCS.csproj +++ b/GeneCodeCS/GeneCodeCS.csproj @@ -1,101 +1,24 @@ - - + + - Debug - AnyCPU - 8.0.30703 - 2.0 - {0A7CCAFD-4FB8-4739-A260-7B384E093469} - Library - Properties + net8.0 GeneCodeCS GeneCodeCS - v4.0 - 512 - ..\..\Hound\ - true + latest + disable + true + $(NoWarn);CS1591;CS1573;CS1587;CS1570;CS1572;CS1574;CS1584;CS1658;CS1710;CS1711;CS1712;CS8981 + en-GB + 0.2.0 + Edd Porter + Genetic programming library for code bot natural selection. + Copyright © Edd Porter 2013 + GPL-3.0-or-later - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\GeneCodeCS.XML - false - false - AnyCPU - false - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - False - ..\..\Hound\packages\Common.Logging.2.1.2\lib\net40\Common.Logging.dll - - - - - - - - - - - - - - - - - - - - - True - - - - True - True - Resources.resx - - - - - - - - - - - Designer - - - + - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - + + - - - - \ No newline at end of file + + diff --git a/GeneCodeCS/Population.cs b/GeneCodeCS/Population.cs index c9c641f..42640e3 100644 --- a/GeneCodeCS/Population.cs +++ b/GeneCodeCS/Population.cs @@ -54,7 +54,7 @@ public sealed class Population where TBot : BaseBot /// /// Not null. /// - private readonly ILog _log = LogManager.GetCurrentClassLogger(); + private readonly ILog _log = LogManager.GetLogger(typeof(Population)); /// /// An optional method that optimises a given (gene expression tree). It removes redundant code and consolidates statements. The purpose is to reduce the time taken to evaluate a bot's fitness and to reduce the likelihood that identical bots are created. The resulting optimise = null) { // 2. Convert expression trees to code. _log.Trace("SimulateGenerations`1: Compiling bot code."); - if (!_compiler.CompileBots(generationInformation, generationNumber)) { + if (!_compiler.CompileBots(generationInformation)) { throw new BotCompileException(); } @@ -147,6 +147,7 @@ public Population(Func optimise = null) { // We use First() and Last() since the list is sorted. var bestFitness = latestOrderedEvaluation.First().Bot.Fitness; + bestBots = latestOrderedEvaluation.TakeWhile(b => b.Bot.Fitness == bestFitness).ToList(); if (_log.IsInfoEnabled) { var worstFitness = latestOrderedEvaluation.Last().Bot.Fitness; var meanFitness = latestOrderedEvaluation.Average(fp => fp.Bot.Fitness); @@ -154,7 +155,6 @@ public Population(Func optimise = null) { _log.InfoFormat("Generation {0} report: Best {1:N} / Mean average {2:N2} / Worst {3:N}. Time taken: {4:T}.", generationNumber, bestFitness, meanFitness, worstFitness, generationEnd - generationStart); - bestBots = latestOrderedEvaluation.TakeWhile(b => b.Bot.Fitness == bestFitness).ToList(); _log.InfoFormat("Best bots: '{0}'", string.Join("', '", bestBots.Select(b => b.Name))); } @@ -194,8 +194,7 @@ public Population(Func optimise = null) { // 2. Convert expression tree to code. _log.Trace("SimulateIndividuals`1: Compiling bot code."); - const int generationNumber = 0; - if (!_compiler.CompileBot(botInformation, generationNumber)) { + if (!_compiler.CompileBot(botInformation)) { throw new BotCompileException(); } diff --git a/GeneCodeCS/Properties/AssemblyInfo.cs b/GeneCodeCS/Properties/AssemblyInfo.cs deleted file mode 100644 index 45796d4..0000000 --- a/GeneCodeCS/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,37 +0,0 @@ -// -// GeneCodeCS - Genetic programming library for code bot natural selection. -// Copyright (C) 2013 Edd Porter -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see {http://www.gnu.org/licenses/}. -// - -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Resources; - -[assembly: AssemblyTitle("GeneCodeCS")] -[assembly: AssemblyDescription("Genetic programming library for code bot natural selection.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Edd Porter")] -[assembly: AssemblyProduct("GeneCodeCS")] -[assembly: AssemblyCopyright("Copyright © Edd Porter 2013")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: ComVisible(false)] -[assembly: Guid("ae3176e4-8047-4915-bb4b-0cd49a29b89f")] -[assembly: AssemblyVersion("0.1.0.0")] -[assembly: AssemblyFileVersion("0.1.0.0")] -[assembly: InternalsVisibleTo("GeneCodeCS.Test")] -[assembly: NeutralResourcesLanguageAttribute("en-GB")] diff --git a/GeneCodeCS/Reproduction.cs b/GeneCodeCS/Reproduction.cs index 2c9b38f..bca94f2 100644 --- a/GeneCodeCS/Reproduction.cs +++ b/GeneCodeCS/Reproduction.cs @@ -45,7 +45,7 @@ public sealed class Reproduction where TBot : BaseBot /// /// Not null. /// - private readonly ILog _log = LogManager.GetCurrentClassLogger(); + private readonly ILog _log = LogManager.GetLogger(typeof(Reproduction)); /// /// A list of non-terminal genes, formed from the branch methods found in the type and constructed sequence genes. @@ -281,7 +281,7 @@ public BotInformation CreateBot(int maxTreeDepth, Func The generation number that the bot is in. /// The index of the bot in its generation. /// A name for the bot. - private static string CreateBotName(int generationNumber, int botIndex) { + internal static string CreateBotName(int generationNumber, int botIndex) { Debug.Assert(generationNumber >= 0, Resources.GenerationNumberValidRange); Debug.Assert(botIndex >= 0, Resources.BotIndexValidRange); diff --git a/GeneCodeCS/UnknownGenotypeException.cs b/GeneCodeCS/UnknownGenotypeException.cs index 4e93d63..9e93138 100644 --- a/GeneCodeCS/UnknownGenotypeException.cs +++ b/GeneCodeCS/UnknownGenotypeException.cs @@ -17,14 +17,12 @@ // using System; -using System.Runtime.Serialization; namespace GeneCodeCS { /// /// Thrown when an unknown genotype ( implementation) is encountered. /// - [Serializable] internal class UnknownGenotypeException : Exception { /// @@ -45,17 +43,5 @@ public UnknownGenotypeException(string message) : base(message) { } /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if no inner exception is specified. public UnknownGenotypeException(string message, Exception innerException) : base(message, innerException) { } - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - /// The - /// - /// parameter is null. - /// The class name is null or - /// - /// is zero (0). - protected UnknownGenotypeException(SerializationInfo info, StreamingContext context) : base(info, context) { } } } \ No newline at end of file diff --git a/GeneCodeCS/packages.config b/GeneCodeCS/packages.config deleted file mode 100644 index afb8c66..0000000 --- a/GeneCodeCS/packages.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Local.testsettings b/Local.testsettings deleted file mode 100644 index 91cc830..0000000 --- a/Local.testsettings +++ /dev/null @@ -1,10 +0,0 @@ - - - These are default test settings for a local test run. - - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 0ab3ac9..28cef0b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ A genetic programming library for performing breeding and natural selection on c Usage ----- -Instantiate a new object of `Population<>` passing a custom sub-class of `BaseBot`. Define public methods in the sub-class for: +Requires .NET 8. Build and test with `dotnet test`. + +Instantiate a new object of `Population<>` passing a custom sub-class of `BaseBot`. The sub-class must be concrete and have a public parameterless constructor. Define public methods in the sub-class for: * Functions - these return bool; * Terminals - there return void. An optimisation function can optionally be passed to the `Population<>` constructor. When a new `Chromosome` is created, it can be reduced and optimised (for example, by removing any impossible branches). Care must be taken that this doesn't reduce the possible search space of the tree. @@ -24,8 +26,4 @@ Each bot will have Initialise called, then Execute, and then Evaluate. The bot s To run the genetic program, call `SimulateGenerations`. -TODO ----- - -* The code currently outputs CSharp code and compiles DLLs into the working directory, which thus must be writeable to. This should be made a configuration option. -* The generated assemblies are loaded into the same assembly as the executing program and isn't unloaded. For larger populations, this might cause a problem. \ No newline at end of file +Bots are compiled in memory from expression trees to delegates, so nothing is written to disk and no assemblies are loaded that cannot be collected. diff --git a/TraceAndTestImpact.testsettings b/TraceAndTestImpact.testsettings deleted file mode 100644 index cd93fe4..0000000 --- a/TraceAndTestImpact.testsettings +++ /dev/null @@ -1,9 +0,0 @@ - - - These are test settings for Trace and Test Impact. - - - - - - \ No newline at end of file From b1068824c1fe4e228aeff223aca8fc1502a9c601 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 21:58:36 +0000 Subject: [PATCH 2/3] Add README quick start, NuGet package metadata and CI workflow Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01PfG6MnHg74EM7ndCo5atss --- .github/workflows/ci.yml | 22 +++++++++++++++++++++ GeneCodeCS/GeneCodeCS.csproj | 9 +++++++++ README.md | 38 ++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..203176c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: CI + +on: + push: + branches: [master, main] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - run: dotnet build --configuration Release + - run: dotnet test --configuration Release --no-build + - run: dotnet pack GeneCodeCS/GeneCodeCS.csproj --configuration Release --no-build --output artifacts + - uses: actions/upload-artifact@v4 + with: + name: package + path: artifacts/*.nupkg diff --git a/GeneCodeCS/GeneCodeCS.csproj b/GeneCodeCS/GeneCodeCS.csproj index f20aa4d..5a8b462 100644 --- a/GeneCodeCS/GeneCodeCS.csproj +++ b/GeneCodeCS/GeneCodeCS.csproj @@ -14,8 +14,17 @@ Genetic programming library for code bot natural selection. Copyright © Edd Porter 2013 GPL-3.0-or-later + GeneCodeCS + genetic-programming;evolutionary-computation;machine-learning + README.md + https://github.com/EddPorter/GeneCodeCS + git + + + + diff --git a/README.md b/README.md index 28cef0b..db27c21 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,44 @@ GeneCodeCS A genetic programming library for performing breeding and natural selection on code bots. +Quick start +----------- + +Evolve a bot that reaches a target number using increments and decrements. The public `void` methods are the actions, and the public `bool` methods are the decision points: + +```csharp +public class CounterBot : BaseBot +{ + private int _steps; + public int Counter { get; private set; } + + // Terminals (actions). + public void Increment(StepParameter step) { Counter += step.Value; } + public void Decrement(StepParameter step) { Counter -= step.Value; } + + // Function (decision point). + public bool BelowTarget() { return Counter < 10; } + + protected override bool NotFinished() { return ++_steps <= 20; } + protected override void Evaluate() { Fitness = 1000 - Math.Abs(10 - Counter); } +} + +// An evolvable constant, restricted to the range 1 to 3. +public class StepParameter : IParameter +{ + public StepParameter(Random random) { Value = random.Next(1, 4); } + public StepParameter(int value) { Value = value; } + public int Value { get; private set; } +} + +var population = new Population(); +var best = population.SimulateGenerations(null, generationLimit: 30, botsPerGeneration: 60, + fitnessThreshold: 999); +Console.WriteLine(best.First().Tree); // The evolved program. +``` + +A working version of this is in `GeneCodeCS.Test/Helpers/CounterBot.cs` and `PopulationTests.cs`. + Usage ----- From efe83136642aafa3da220c9410aacba178f120d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 22:14:54 +0000 Subject: [PATCH 3/3] Add GitHub Pages site with live evolution demo; fix rate validation - docs/index.html: self-contained landing page with an interactive, working miniature of the evolutionary loop, annotated code walkthrough, settings reference and fit/limitations guidance. - Fix CrossoverRate, MutationRate and RandomBotPercentage setters, which validated the previous field value instead of the new value. - Link the page from the README. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01PfG6MnHg74EM7ndCo5atss --- GeneCodeCS.Test/ReproductionTests.cs | 40 ++ GeneCodeCS/Reproduction.cs | 6 +- README.md | 2 + docs/.nojekyll | 0 docs/index.html | 733 +++++++++++++++++++++++++++ 5 files changed, 778 insertions(+), 3 deletions(-) create mode 100644 GeneCodeCS.Test/ReproductionTests.cs create mode 100644 docs/.nojekyll create mode 100644 docs/index.html diff --git a/GeneCodeCS.Test/ReproductionTests.cs b/GeneCodeCS.Test/ReproductionTests.cs new file mode 100644 index 0000000..c24467d --- /dev/null +++ b/GeneCodeCS.Test/ReproductionTests.cs @@ -0,0 +1,40 @@ +using System; +using GeneCodeCS.Test.Helpers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GeneCodeCS.Test +{ + [TestClass] + public class ReproductionTests + { + [TestMethod] + public void Rates_accept_values_from_0_to_100() { + var target = new Reproduction { CrossoverRate = 0, MutationRate = 100, RandomBotPercentage = 50 }; + + Assert.AreEqual(0, target.CrossoverRate); + Assert.AreEqual(100, target.MutationRate); + Assert.AreEqual(50, target.RandomBotPercentage); + } + + [TestMethod] + public void CrossoverRate_rejects_out_of_range_values() { + var target = new Reproduction(); + Assert.ThrowsException(() => target.CrossoverRate = 101); + Assert.ThrowsException(() => target.CrossoverRate = -1); + } + + [TestMethod] + public void MutationRate_rejects_out_of_range_values() { + var target = new Reproduction(); + Assert.ThrowsException(() => target.MutationRate = 101); + Assert.ThrowsException(() => target.MutationRate = -1); + } + + [TestMethod] + public void RandomBotPercentage_rejects_out_of_range_values() { + var target = new Reproduction(); + Assert.ThrowsException(() => target.RandomBotPercentage = 101); + Assert.ThrowsException(() => target.RandomBotPercentage = -1); + } + } +} diff --git a/GeneCodeCS/Reproduction.cs b/GeneCodeCS/Reproduction.cs index bca94f2..09ec4ec 100644 --- a/GeneCodeCS/Reproduction.cs +++ b/GeneCodeCS/Reproduction.cs @@ -106,7 +106,7 @@ public Reproduction(int randomSeed = 0) { public int CrossoverRate { get { return _crossoverRate; } set { - if (_crossoverRate < 0 || _crossoverRate > 100) { + if (value < 0 || value > 100) { throw new ArgumentOutOfRangeException("value", value, Resources.CrossoverRateValidRange); } _log.TraceFormat("get_CrossoverRate: Crossover rate set to {0}.", value); @@ -125,7 +125,7 @@ public int CrossoverRate { public int MutationRate { get { return _mutationRate; } set { - if (_mutationRate < 0 || _mutationRate > 100) { + if (value < 0 || value > 100) { throw new ArgumentOutOfRangeException("value", value, Resources.MutationRateValidRange); } _log.TraceFormat("get_MutationRate: Mutation rate set to {0}.", value); @@ -139,7 +139,7 @@ public int MutationRate { public int RandomBotPercentage { get { return _randomBotPercentage; } set { - if (_randomBotPercentage < 0 || _randomBotPercentage > 100) { + if (value < 0 || value > 100) { throw new ArgumentOutOfRangeException("value", value, Resources.RandomBotPercentageValidRange); } _log.TraceFormat("get_RandomBotPercentage: Random bot percentage set to {0}.", value); diff --git a/README.md b/README.md index db27c21..1fa4185 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ GeneCodeCS A genetic programming library for performing breeding and natural selection on code bots. +**[Live demo and walkthrough](https://eddporter.github.io/GeneCodeCS/)** + Quick start ----------- diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..51fba03 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,733 @@ + + + + + +GeneCodeCS – evolve programs in C# + + + + +
+ + +
+
Genetic programming for .NET
+

Evolve programs,
not just parameters.

+

Write the small methods your bot is allowed to call. GeneCodeCS breeds random programs from them, compiles each one in memory, scores it with your fitness function, and lets the best ones reproduce.

+ +
# .NET 8 SDK
+git clone https://github.com/EddPorter/GeneCodeCS
+cd GeneCodeCS && dotnet test
+
.NET 8GPL-3.0-or-laterNo code generation to diskReproducible with a seed
+
+ +
+
Live demo
+

A population, evolving

+

This is a small working model of what the library does, running in your browser. Each card is one bot’s program drawn as a tree. Press play, or step through a generation one stage at a time.

+ +
+
+

The task: a counter starts at 0. Each bot’s program runs 20 times. The bot is fitter the closer the counter finishes to 10 (fitness = 1000 − distance). The building blocks are Increment(n), Decrement(n) and the question BelowTarget(). No bot is told how to combine them.

+
+ + + +
+
+ +

+
+ +
+

Fitness by generation

+

Best bot’s counter over its 20 runs

+

The best bot, as C#

+
+
+

The demo is a miniature written in JavaScript for this page. It uses the same ingredients as the library (tree genomes, tournament selection, subtree crossover, mutation, elitism and random newcomers) but isn’t the library itself. The C# below is.

+
+ +
+
How it works
+

Four steps, one loop

+

You supply the vocabulary and the scoring. The library does the evolving.

+
+

Describe

Public void methods on your bot become actions. Public bool methods become decisions. Parameters are evolvable constants you control the range of.

+

Grow

The first generation is random expression trees built from those parts, with Grow or Full initialisation up to a depth you choose.

+

Compile

Each tree is turned into a delegate with System.Linq.Expressions, in memory. There are no files to write and no assemblies to leak.

+

Select & breed

Your Evaluate sets Fitness. Fitter bots are more likely to be parents; subtrees are swapped, nodes occasionally mutated, and the loop repeats.

+
+
+ +
+
The code
+

This is the whole bot

+

The demo above is this C# class. Nothing else describes the problem.

+
+
public class CounterBot : BaseBot
+{
+  private int _steps;
+  public int Counter { get; private set; }
+
+  // Actions: public void methods1
+  public void Increment(StepParameter s)2
+  { Counter += s.Value; }
+  public void Decrement(StepParameter s)
+  { Counter -= s.Value; }
+
+  // Decisions: public bool methods3
+  public bool BelowTarget() { return Counter < 10; }
+
+  protected override bool NotFinished()4
+  { return ++_steps <= 20; }
+
+  protected override void Evaluate()5
+  { Fitness = 1000 - Math.Abs(10 - Counter); }
+}
+
+// An evolvable constant, kept to the range 1 to 3
+public class StepParameter : IParameter<int>
+{
+  public StepParameter(Random r)6
+  { Value = r.Next(1, 4); }
+  public StepParameter(int value)
+  { Value = value; }
+  public int Value { get; private set; }
+}
+
+// Evolve
+var population = new Population<CounterBot>();
+var best = population.SimulateGenerations<object>(null,
+    generationLimit: 30, botsPerGeneration: 60,
+    fitnessThreshold: 999);7
+
    +
  1. 1

    Your methods are the gene pool. The library finds them by reflection. void methods become the leaves of a program tree.

  2. +
  3. 2

    Parameters are part of the genome. Each Increment in a tree carries its own evolved step size.

  4. +
  5. 3

    Decisions make branches. A bool method becomes an if/else with a subtree on each side.

  6. +
  7. 4

    You say when a bot is done. The evolved program runs in a loop until NotFinished returns false.

  8. +
  9. 5

    You define “good”. Set Fitness from whatever the bot did. Higher is better. A bot that throws is scored int.MinValue.

  10. +
  11. 6

    Two constructors. One takes a Random to pick the starting value, so you control the range. The other restores a stored value when the tree is compiled.

  12. +
  13. 7

    One call evolves it. You get back the best bot or bots, each with its tree, name, parents and compiled instance.

  14. +
+
+ +
+ + + + + + + + +
You writeThe library treats it asShown in the demo as
public void Increment(StepParameter step)A terminal gene (a leaf), with one evolvable parameterA green +n node
public bool BelowTarget()A branch gene: if / elseA blue Below? node with yes and no subtrees
Several genes in a rowA sequence, run top to bottomA grey then node
Evaluate()The fitness functionThe bar on each card
+
+
+ +
+
Control
+

The knobs that matter

+
+ + + + + + + + + + + + + +
SettingWhat it doesDefault
botsPerGeneration, generationLimit, maxTreeDepthPopulation size, how long to run, how large the first random programs can be30, 20, 3
fitnessThresholdStop early once a bot beats this fitnessNever
InitialisationMethodGrow mixes functions and terminals; Full fills every levelGrow
SelectionMethodTournament, or FitnessProportionate (chance of parenthood in proportion to fitness)Tournament
CrossoverRate, MutationRateChance, 0–100, of swapping subtrees or mutating during breeding90, 5
RandomBotPercentageShare of each new generation made of fresh random bots, to keep diversity up20
starterBotsSeed the first generation with programs you already have, or the output of an earlier runNone
Optimise callbackSimplify each new tree (for example, remove impossible branches) before it is compiledNone
Random seedMake a run reproducibleTime-based
+
+
+ +
+
Is it for you?
+

Good at some things, not all

+
+
+

A good fit

+
    +
  • Behaviour-style programs: trading rules, game agents, simulation controllers, scheduling heuristics.
  • +
  • Problems you can score automatically and cheaply, thousands of times.
  • +
  • When you want to read the answer. The result is a small program, not a weight matrix.
  • +
  • Prototyping: if you can write the building blocks, you can start evolving in an afternoon.
  • +
+
+
+

Think twice

+
    +
  • Numeric symbolic regression. The building blocks are bool and void methods, not typed expressions that return numbers.
  • +
  • Noisy or thin data. Evolution will happily overfit; score on held-out data.
  • +
  • Expensive fitness. Each bot in each generation must be run, so slow evaluation multiplies fast.
  • +
  • Anything needing a guarantee. Evolution gives you good candidates, not proofs.
  • +
+
+
+
+
+ + + + + + + +