@reicek/neataptic-ts 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +27 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +28 -0
  4. package/.github/workflows/ci.yml +41 -0
  5. package/.github/workflows/deploy-pages.yml +29 -0
  6. package/.github/workflows/manual_release_pipeline.yml +62 -0
  7. package/.github/workflows/publish.yml +85 -0
  8. package/.github/workflows/release_dispatch.yml +38 -0
  9. package/.travis.yml +5 -0
  10. package/CONTRIBUTING.md +92 -0
  11. package/LICENSE +24 -0
  12. package/ONNX_EXPORT.md +87 -0
  13. package/README.md +1173 -0
  14. package/RELEASE.md +54 -0
  15. package/dist-docs/package.json +1 -0
  16. package/dist-docs/scripts/generate-docs.d.ts +2 -0
  17. package/dist-docs/scripts/generate-docs.d.ts.map +1 -0
  18. package/dist-docs/scripts/generate-docs.js +536 -0
  19. package/dist-docs/scripts/generate-docs.js.map +1 -0
  20. package/dist-docs/scripts/render-docs-html.d.ts +2 -0
  21. package/dist-docs/scripts/render-docs-html.d.ts.map +1 -0
  22. package/dist-docs/scripts/render-docs-html.js +148 -0
  23. package/dist-docs/scripts/render-docs-html.js.map +1 -0
  24. package/docs/FOLDERS.md +14 -0
  25. package/docs/README.md +1173 -0
  26. package/docs/architecture/README.md +1391 -0
  27. package/docs/architecture/index.html +938 -0
  28. package/docs/architecture/network/README.md +1210 -0
  29. package/docs/architecture/network/index.html +908 -0
  30. package/docs/assets/ascii-maze.bundle.js +16542 -0
  31. package/docs/assets/ascii-maze.bundle.js.map +7 -0
  32. package/docs/index.html +1419 -0
  33. package/docs/methods/README.md +670 -0
  34. package/docs/methods/index.html +477 -0
  35. package/docs/multithreading/README.md +274 -0
  36. package/docs/multithreading/index.html +215 -0
  37. package/docs/multithreading/workers/README.md +23 -0
  38. package/docs/multithreading/workers/browser/README.md +39 -0
  39. package/docs/multithreading/workers/browser/index.html +70 -0
  40. package/docs/multithreading/workers/index.html +57 -0
  41. package/docs/multithreading/workers/node/README.md +33 -0
  42. package/docs/multithreading/workers/node/index.html +66 -0
  43. package/docs/neat/README.md +1284 -0
  44. package/docs/neat/index.html +906 -0
  45. package/docs/src/README.md +2659 -0
  46. package/docs/src/index.html +1579 -0
  47. package/jest.config.ts +32 -0
  48. package/package.json +99 -0
  49. package/plans/HyperMorphoNEAT.md +293 -0
  50. package/plans/ONNX_EXPORT_PLAN.md +46 -0
  51. package/scripts/generate-docs.ts +486 -0
  52. package/scripts/render-docs-html.ts +138 -0
  53. package/scripts/types.d.ts +2 -0
  54. package/src/README.md +2659 -0
  55. package/src/architecture/README.md +1391 -0
  56. package/src/architecture/activationArrayPool.ts +135 -0
  57. package/src/architecture/architect.ts +635 -0
  58. package/src/architecture/connection.ts +148 -0
  59. package/src/architecture/group.ts +406 -0
  60. package/src/architecture/layer.ts +804 -0
  61. package/src/architecture/network/README.md +1210 -0
  62. package/src/architecture/network/network.activate.ts +223 -0
  63. package/src/architecture/network/network.connect.ts +157 -0
  64. package/src/architecture/network/network.deterministic.ts +167 -0
  65. package/src/architecture/network/network.evolve.ts +426 -0
  66. package/src/architecture/network/network.gating.ts +186 -0
  67. package/src/architecture/network/network.genetic.ts +247 -0
  68. package/src/architecture/network/network.mutate.ts +624 -0
  69. package/src/architecture/network/network.onnx.ts +463 -0
  70. package/src/architecture/network/network.prune.ts +216 -0
  71. package/src/architecture/network/network.remove.ts +96 -0
  72. package/src/architecture/network/network.serialize.ts +309 -0
  73. package/src/architecture/network/network.slab.ts +262 -0
  74. package/src/architecture/network/network.standalone.ts +246 -0
  75. package/src/architecture/network/network.stats.ts +59 -0
  76. package/src/architecture/network/network.topology.ts +86 -0
  77. package/src/architecture/network/network.training.ts +1278 -0
  78. package/src/architecture/network.ts +1302 -0
  79. package/src/architecture/node.ts +1288 -0
  80. package/src/architecture/onnx.ts +3 -0
  81. package/src/config.ts +83 -0
  82. package/src/methods/README.md +670 -0
  83. package/src/methods/activation.ts +372 -0
  84. package/src/methods/connection.ts +31 -0
  85. package/src/methods/cost.ts +347 -0
  86. package/src/methods/crossover.ts +63 -0
  87. package/src/methods/gating.ts +43 -0
  88. package/src/methods/methods.ts +8 -0
  89. package/src/methods/mutation.ts +300 -0
  90. package/src/methods/rate.ts +257 -0
  91. package/src/methods/selection.ts +65 -0
  92. package/src/multithreading/README.md +274 -0
  93. package/src/multithreading/multi.ts +339 -0
  94. package/src/multithreading/workers/README.md +23 -0
  95. package/src/multithreading/workers/browser/README.md +39 -0
  96. package/src/multithreading/workers/browser/testworker.ts +99 -0
  97. package/src/multithreading/workers/node/README.md +33 -0
  98. package/src/multithreading/workers/node/testworker.ts +72 -0
  99. package/src/multithreading/workers/node/worker.ts +70 -0
  100. package/src/multithreading/workers/workers.ts +22 -0
  101. package/src/neat/README.md +1284 -0
  102. package/src/neat/neat.adaptive.ts +544 -0
  103. package/src/neat/neat.compat.ts +164 -0
  104. package/src/neat/neat.constants.ts +20 -0
  105. package/src/neat/neat.diversity.ts +217 -0
  106. package/src/neat/neat.evaluate.ts +328 -0
  107. package/src/neat/neat.evolve.ts +1026 -0
  108. package/src/neat/neat.export.ts +249 -0
  109. package/src/neat/neat.helpers.ts +235 -0
  110. package/src/neat/neat.lineage.ts +220 -0
  111. package/src/neat/neat.multiobjective.ts +260 -0
  112. package/src/neat/neat.mutation.ts +718 -0
  113. package/src/neat/neat.objectives.ts +157 -0
  114. package/src/neat/neat.pruning.ts +190 -0
  115. package/src/neat/neat.selection.ts +269 -0
  116. package/src/neat/neat.speciation.ts +460 -0
  117. package/src/neat/neat.species.ts +151 -0
  118. package/src/neat/neat.telemetry.exports.ts +469 -0
  119. package/src/neat/neat.telemetry.ts +933 -0
  120. package/src/neat/neat.types.ts +275 -0
  121. package/src/neat.ts +1042 -0
  122. package/src/neataptic.ts +10 -0
  123. package/test/architecture/activationArrayPool.capacity.test.ts +19 -0
  124. package/test/architecture/activationArrayPool.test.ts +46 -0
  125. package/test/architecture/connection.test.ts +290 -0
  126. package/test/architecture/group.test.ts +950 -0
  127. package/test/architecture/layer.test.ts +1535 -0
  128. package/test/architecture/network.pruning.test.ts +65 -0
  129. package/test/architecture/node.test.ts +1602 -0
  130. package/test/examples/asciiMaze/asciiMaze.e2e.test.ts +499 -0
  131. package/test/examples/asciiMaze/asciiMaze.ts +41 -0
  132. package/test/examples/asciiMaze/browser-entry.ts +164 -0
  133. package/test/examples/asciiMaze/browserLogger.ts +221 -0
  134. package/test/examples/asciiMaze/browserTerminalUtility.ts +48 -0
  135. package/test/examples/asciiMaze/colors.ts +119 -0
  136. package/test/examples/asciiMaze/dashboardManager.ts +968 -0
  137. package/test/examples/asciiMaze/evolutionEngine.ts +1248 -0
  138. package/test/examples/asciiMaze/fitness.ts +136 -0
  139. package/test/examples/asciiMaze/index.html +128 -0
  140. package/test/examples/asciiMaze/index.ts +26 -0
  141. package/test/examples/asciiMaze/interfaces.ts +235 -0
  142. package/test/examples/asciiMaze/mazeMovement.ts +996 -0
  143. package/test/examples/asciiMaze/mazeUtils.ts +278 -0
  144. package/test/examples/asciiMaze/mazeVision.ts +402 -0
  145. package/test/examples/asciiMaze/mazeVisualization.ts +585 -0
  146. package/test/examples/asciiMaze/mazes.ts +245 -0
  147. package/test/examples/asciiMaze/networkRefinement.ts +76 -0
  148. package/test/examples/asciiMaze/networkVisualization.ts +901 -0
  149. package/test/examples/asciiMaze/terminalUtility.ts +73 -0
  150. package/test/methods/activation.test.ts +1142 -0
  151. package/test/methods/connection.test.ts +146 -0
  152. package/test/methods/cost.test.ts +1123 -0
  153. package/test/methods/crossover.test.ts +202 -0
  154. package/test/methods/gating.test.ts +144 -0
  155. package/test/methods/mutation.test.ts +451 -0
  156. package/test/methods/optimizers.advanced.test.ts +80 -0
  157. package/test/methods/optimizers.behavior.test.ts +105 -0
  158. package/test/methods/optimizers.formula.test.ts +89 -0
  159. package/test/methods/rate.cosineWarmRestarts.test.ts +44 -0
  160. package/test/methods/rate.linearWarmupDecay.test.ts +41 -0
  161. package/test/methods/rate.reduceOnPlateau.test.ts +45 -0
  162. package/test/methods/rate.test.ts +684 -0
  163. package/test/methods/selection.test.ts +245 -0
  164. package/test/multithreading/activations.functions.test.ts +54 -0
  165. package/test/multithreading/multi.test.ts +290 -0
  166. package/test/multithreading/worker.node.process.test.ts +39 -0
  167. package/test/multithreading/workers.coverage.test.ts +36 -0
  168. package/test/multithreading/workers.dynamic.import.test.ts +8 -0
  169. package/test/neat/neat.adaptive.complexityBudget.test.ts +34 -0
  170. package/test/neat/neat.adaptive.criterion.complexity.test.ts +50 -0
  171. package/test/neat/neat.adaptive.mutation.strategy.test.ts +37 -0
  172. package/test/neat/neat.adaptive.operator.decay.test.ts +31 -0
  173. package/test/neat/neat.adaptive.phasedComplexity.test.ts +25 -0
  174. package/test/neat/neat.adaptive.pruning.test.ts +25 -0
  175. package/test/neat/neat.adaptive.targetSpecies.test.ts +43 -0
  176. package/test/neat/neat.additional.coverage.test.ts +126 -0
  177. package/test/neat/neat.advanced.enhancements.test.ts +85 -0
  178. package/test/neat/neat.advanced.test.ts +589 -0
  179. package/test/neat/neat.diversity.autocompat.test.ts +47 -0
  180. package/test/neat/neat.diversity.metrics.test.ts +21 -0
  181. package/test/neat/neat.diversity.stats.test.ts +44 -0
  182. package/test/neat/neat.enhancements.test.ts +79 -0
  183. package/test/neat/neat.entropy.ancestorAdaptive.test.ts +133 -0
  184. package/test/neat/neat.entropy.compat.csv.test.ts +108 -0
  185. package/test/neat/neat.evolution.pruning.test.ts +39 -0
  186. package/test/neat/neat.fastmode.autotune.test.ts +42 -0
  187. package/test/neat/neat.innovation.test.ts +134 -0
  188. package/test/neat/neat.lineage.antibreeding.test.ts +35 -0
  189. package/test/neat/neat.lineage.entropy.test.ts +56 -0
  190. package/test/neat/neat.lineage.inbreeding.test.ts +49 -0
  191. package/test/neat/neat.lineage.pressure.test.ts +29 -0
  192. package/test/neat/neat.multiobjective.adaptive.test.ts +57 -0
  193. package/test/neat/neat.multiobjective.dynamic.schedule.test.ts +46 -0
  194. package/test/neat/neat.multiobjective.dynamic.test.ts +31 -0
  195. package/test/neat/neat.multiobjective.fastsort.delegation.test.ts +51 -0
  196. package/test/neat/neat.multiobjective.prune.test.ts +39 -0
  197. package/test/neat/neat.multiobjective.test.ts +21 -0
  198. package/test/neat/neat.mutation.undefined.pool.test.ts +24 -0
  199. package/test/neat/neat.objective.events.test.ts +26 -0
  200. package/test/neat/neat.objective.importance.test.ts +21 -0
  201. package/test/neat/neat.objective.lifetimes.test.ts +33 -0
  202. package/test/neat/neat.offspring.allocation.test.ts +22 -0
  203. package/test/neat/neat.operator.bandit.test.ts +17 -0
  204. package/test/neat/neat.operator.phases.test.ts +38 -0
  205. package/test/neat/neat.pruneInactive.behavior.test.ts +54 -0
  206. package/test/neat/neat.reenable.adaptation.test.ts +18 -0
  207. package/test/neat/neat.rng.state.test.ts +22 -0
  208. package/test/neat/neat.spawn.add.test.ts +123 -0
  209. package/test/neat/neat.speciation.test.ts +96 -0
  210. package/test/neat/neat.species.allocation.telemetry.test.ts +26 -0
  211. package/test/neat/neat.species.history.csv.test.ts +24 -0
  212. package/test/neat/neat.telemetry.advanced.test.ts +226 -0
  213. package/test/neat/neat.telemetry.csv.lineage.test.ts +19 -0
  214. package/test/neat/neat.telemetry.parity.test.ts +42 -0
  215. package/test/neat/neat.telemetry.stream.test.ts +19 -0
  216. package/test/neat/neat.telemetry.test.ts +16 -0
  217. package/test/neat/neat.test.ts +422 -0
  218. package/test/neat/neat.utilities.test.ts +44 -0
  219. package/test/network/__suppress_console.ts +9 -0
  220. package/test/network/acyclic.topoorder.test.ts +17 -0
  221. package/test/network/checkpoint.metricshook.test.ts +36 -0
  222. package/test/network/error.handling.test.ts +581 -0
  223. package/test/network/evolution.test.ts +285 -0
  224. package/test/network/genetic.test.ts +208 -0
  225. package/test/network/learning.capability.test.ts +244 -0
  226. package/test/network/mutation.effects.test.ts +492 -0
  227. package/test/network/network.activate.test.ts +115 -0
  228. package/test/network/network.activateBatch.test.ts +30 -0
  229. package/test/network/network.deterministic.test.ts +64 -0
  230. package/test/network/network.evolve.branches.test.ts +75 -0
  231. package/test/network/network.evolve.multithread.branches.test.ts +83 -0
  232. package/test/network/network.evolve.test.ts +100 -0
  233. package/test/network/network.gating.removal.test.ts +93 -0
  234. package/test/network/network.mutate.additional.test.ts +145 -0
  235. package/test/network/network.mutate.edgecases.test.ts +101 -0
  236. package/test/network/network.mutate.test.ts +101 -0
  237. package/test/network/network.prune.earlyexit.test.ts +38 -0
  238. package/test/network/network.remove.errors.test.ts +45 -0
  239. package/test/network/network.slab.fallbacks.test.ts +22 -0
  240. package/test/network/network.stats.test.ts +45 -0
  241. package/test/network/network.training.advanced.test.ts +149 -0
  242. package/test/network/network.training.basic.test.ts +228 -0
  243. package/test/network/network.training.helpers.test.ts +183 -0
  244. package/test/network/onnx.export.test.ts +310 -0
  245. package/test/network/onnx.import.test.ts +129 -0
  246. package/test/network/pruning.topology.test.ts +282 -0
  247. package/test/network/regularization.determinism.test.ts +83 -0
  248. package/test/network/regularization.dropconnect.test.ts +17 -0
  249. package/test/network/regularization.dropconnect.validation.test.ts +18 -0
  250. package/test/network/regularization.stochasticdepth.test.ts +27 -0
  251. package/test/network/regularization.test.ts +843 -0
  252. package/test/network/regularization.weightnoise.test.ts +30 -0
  253. package/test/network/setupTests.ts +2 -0
  254. package/test/network/standalone.test.ts +332 -0
  255. package/test/network/structure.serialization.test.ts +660 -0
  256. package/test/training/training.determinism.mixed-precision.test.ts +134 -0
  257. package/test/training/training.earlystopping.test.ts +91 -0
  258. package/test/training/training.edge-cases.test.ts +91 -0
  259. package/test/training/training.extensions.test.ts +47 -0
  260. package/test/training/training.gradient.features.test.ts +110 -0
  261. package/test/training/training.gradient.refinements.test.ts +170 -0
  262. package/test/training/training.gradient.separate-bias.test.ts +41 -0
  263. package/test/training/training.optimizer.test.ts +48 -0
  264. package/test/training/training.plateau.smoothing.test.ts +58 -0
  265. package/test/training/training.smoothing.types.test.ts +174 -0
  266. package/test/training/training.train.options.coverage.test.ts +52 -0
  267. package/test/utils/console-helper.ts +76 -0
  268. package/test/utils/jest-setup.ts +60 -0
  269. package/test/utils/test-helpers.ts +175 -0
  270. package/tsconfig.docs.json +12 -0
  271. package/tsconfig.json +21 -0
  272. package/webpack.config.js +49 -0
@@ -0,0 +1,1248 @@
1
+ // Handles the main NEAT evolution loop for maze solving
2
+ // Exports: EvolutionEngine class with static methods
3
+
4
+ import { Neat, Network, methods } from '../../../src/neataptic';
5
+ import seedrandom from 'seedrandom';
6
+ import { MazeUtils } from './mazeUtils';
7
+ import { MazeMovement } from './mazeMovement';
8
+ import { FitnessEvaluator } from './fitness';
9
+ import {
10
+ INetwork,
11
+ IFitnessEvaluationContext,
12
+ IRunMazeEvolutionOptions,
13
+ } from './interfaces';
14
+
15
+ /**
16
+ * The `EvolutionEngine` class encapsulates the entire neuro-evolution process for training agents to solve mazes.
17
+ * It leverages the NEAT (Neuro-Evolution of Augmenting Topologies) algorithm to evolve neural networks.
18
+ * This class is designed as a static utility, meaning you don't need to instantiate it to use its methods.
19
+ *
20
+ * Key Responsibilities:
21
+ * - Orchestrating the main evolution loop (generations, evaluation, selection, reproduction).
22
+ * - Configuring and initializing the NEAT algorithm with appropriate parameters.
23
+ * - Managing a hybrid evolution strategy that combines genetic exploration (NEAT) with local optimization (backpropagation).
24
+ * - Handling curriculum learning, where agents can be trained on a sequence of increasingly difficult mazes.
25
+ * - Providing utilities for logging, visualization, and debugging the evolutionary process.
26
+ */
27
+ export class EvolutionEngine {
28
+ /**
29
+ * Runs the NEAT neuro-evolution process for an agent to solve a given ASCII maze.
30
+ *
31
+ * This is the core function of the `EvolutionEngine`. It sets up and runs the evolutionary
32
+ * algorithm to train a population of neural networks. Each network acts as the "brain" for an
33
+ * agent, controlling its movement through the maze from a start point 'S' to an exit 'E'.
34
+ *
35
+ * The process involves several key steps:
36
+ * 1. **Initialization**: Sets up the maze, NEAT parameters, and the initial population of networks.
37
+ * 2. **Generational Loop**: Iterates through generations, performing the following for each:
38
+ * a. **Evaluation**: Each network's performance (fitness) is measured by how well its agent navigates the maze.
39
+ * Fitness is typically based on progress towards the exit, speed, and efficiency.
40
+ * b. **Lamarckian Refinement**: Each individual in the population undergoes a brief period of supervised training
41
+ * (backpropagation) on a set of ideal sensory-action pairs. This helps to fine-tune promising behaviors.
42
+ * c. **Selection & Reproduction**: The NEAT algorithm selects the fittest individuals to become parents for the
43
+ * next generation. It uses genetic operators (crossover and mutation) to create offspring.
44
+ * 3. **Termination**: The loop continues until a solution is found (an agent successfully reaches the exit) or other
45
+ * stopping criteria are met (e.g., maximum generations, stagnation).
46
+ *
47
+ * This hybrid approach, combining the global search of evolution with the local search of backpropagation,
48
+ * can significantly accelerate learning and lead to more robust solutions.
49
+ *
50
+ * @param options - A comprehensive configuration object for the maze evolution process.
51
+ * @returns A Promise that resolves with an object containing the best network found, its simulation result, and the final NEAT instance.
52
+ */
53
+ static async runMazeEvolution(options: IRunMazeEvolutionOptions) {
54
+ // --- Step 1: Destructure and Default Configuration ---
55
+ // Extract all the necessary configuration objects from the main options parameter.
56
+ const {
57
+ mazeConfig,
58
+ agentSimConfig,
59
+ evolutionAlgorithmConfig,
60
+ reportingConfig,
61
+ fitnessEvaluator,
62
+ } = options;
63
+ const { maze } = mazeConfig;
64
+ const { logEvery = 10, dashboardManager } = reportingConfig;
65
+
66
+ // Extract evolution parameters, providing sensible defaults for any that are not specified.
67
+ const {
68
+ allowRecurrent = true, // Allow networks to have connections that loop back, enabling memory.
69
+ popSize = 500, // The number of neural networks in each generation.
70
+ maxStagnantGenerations = 500, // Stop evolution if the best fitness doesn't improve for this many generations.
71
+ minProgressToPass = 95, // The percentage of progress required to consider the maze "solved".
72
+ maxGenerations = Infinity, // A safety cap on the total number of generations to prevent infinite loops.
73
+ randomSeed, // An optional seed for the random number generator to ensure reproducible results.
74
+ initialPopulation, // An optional population of networks to start with.
75
+ initialBestNetwork, // An optional pre-trained network to seed the population.
76
+ lamarckianIterations = 10, // The number of backpropagation steps for each individual per generation.
77
+ lamarckianSampleSize, // If set, use a random subset of the training data for Lamarckian learning.
78
+ plateauGenerations = 40, // Number of generations to wait for improvement before considering the population to be on a plateau.
79
+ plateauImprovementThreshold = 1e-6, // The minimum fitness improvement required to reset the plateau counter.
80
+ simplifyDuration = 30, // The number of generations to run the network simplification process.
81
+ simplifyPruneFraction = 0.05, // The fraction of weak connections to prune during simplification.
82
+ simplifyStrategy = 'weakWeight', // The strategy for choosing which connections to prune.
83
+ persistEvery = 25, // Save a snapshot of the best networks every N generations.
84
+ persistDir = './ascii_maze_snapshots', // The directory to save snapshots in.
85
+ persistTopK = 3, // The number of top-performing networks to save in each snapshot.
86
+ dynamicPopEnabled = true, // Enable dynamic adjustment of the population size.
87
+ dynamicPopMax: dynamicPopMaxCfg, // The maximum population size for dynamic adjustments.
88
+ dynamicPopExpandInterval = 25, // The number of generations between population size expansions.
89
+ dynamicPopExpandFactor = 0.15, // The factor by which to expand the population size.
90
+ dynamicPopPlateauSlack = 0.6, // A slack factor for plateau detection when dynamic population is enabled.
91
+ } = evolutionAlgorithmConfig;
92
+
93
+ // Determine the maximum population size, with a fallback if not explicitly configured.
94
+ const dynamicPopMax =
95
+ typeof dynamicPopMaxCfg === 'number'
96
+ ? dynamicPopMaxCfg
97
+ : Math.max(popSize, 120);
98
+
99
+ // --- Step 2: Maze and Environment Setup ---
100
+ // Encode the maze into a numerical format (0 for walls, 1 for paths) for efficient processing.
101
+ const encodedMaze = MazeUtils.encodeMaze(maze);
102
+ // Locate the starting 'S' and exit 'E' positions within the maze.
103
+ const startPosition = MazeUtils.findPosition(maze, 'S');
104
+ const exitPosition = MazeUtils.findPosition(maze, 'E');
105
+ // Pre-calculate the distance from every point in the maze to the exit. This is a crucial
106
+ // optimization and provides a rich source of information for the fitness function.
107
+ const distanceMap = MazeUtils.buildDistanceMap(encodedMaze, exitPosition);
108
+
109
+ // Define the structure of the neural network: 6 inputs and 4 outputs.
110
+ // Inputs: [compassScalar, openN, openE, openS, openW, progressDelta]
111
+ // Outputs: [moveN, moveE, moveS, moveW]
112
+ const inputSize = 6;
113
+ const outputSize = 4;
114
+
115
+ // Select the fitness evaluator function. Use the provided one or a default.
116
+ const currentFitnessEvaluator =
117
+ fitnessEvaluator || FitnessEvaluator.defaultFitnessEvaluator;
118
+
119
+ // --- Step 3: Fitness Evaluation Context ---
120
+ // Bundle all the necessary environmental data into a context object. This object will be
121
+ // passed to the fitness function, so it has all the information it needs to evaluate a network.
122
+ const fitnessContext: IFitnessEvaluationContext = {
123
+ encodedMaze,
124
+ startPosition,
125
+ exitPosition,
126
+ agentSimConfig,
127
+ distanceMap,
128
+ };
129
+
130
+ // Create the fitness callback function that NEAT will use. This function takes a network,
131
+ // runs the simulation, and returns a single numerical score representing its fitness.
132
+ const neatFitnessCallback = (network: Network): number => {
133
+ return currentFitnessEvaluator(network, fitnessContext);
134
+ };
135
+
136
+ // --- Step 4: NEAT Algorithm Initialization ---
137
+ // Create a new instance of the Neat algorithm with a detailed configuration.
138
+ const neat = new Neat(inputSize, outputSize, neatFitnessCallback, {
139
+ popsize: popSize,
140
+ // Define the types of mutations that can occur, allowing for structural evolution.
141
+ mutation: [
142
+ methods.mutation.ADD_NODE,
143
+ methods.mutation.SUB_NODE,
144
+ methods.mutation.ADD_CONN,
145
+ methods.mutation.SUB_CONN,
146
+ methods.mutation.MOD_BIAS,
147
+ methods.mutation.MOD_ACTIVATION,
148
+ methods.mutation.MOD_CONNECTION,
149
+ methods.mutation.ADD_LSTM_NODE, // Allow adding LSTM nodes for more complex memory.
150
+ ],
151
+ mutationRate: 0.2,
152
+ mutationAmount: 0.3,
153
+ elitism: Math.max(1, Math.floor(popSize * 0.1)), // Preserve the top 10% of the population.
154
+ provenance: Math.max(1, Math.floor(popSize * 0.2)), // Keep a portion of the population from previous species.
155
+ allowRecurrent: allowRecurrent,
156
+ minHidden: 6, // Start with a minimum number of hidden nodes.
157
+ // Enable advanced features for more sophisticated evolution.
158
+ adaptiveMutation: { enabled: true, strategy: 'twoTier' },
159
+ multiObjective: {
160
+ enabled: true,
161
+ complexityMetric: 'nodes',
162
+ autoEntropy: true,
163
+ },
164
+ telemetry: {
165
+ enabled: true,
166
+ performance: true,
167
+ complexity: true,
168
+ hypervolume: true,
169
+ },
170
+ lineageTracking: true,
171
+ novelty: {
172
+ enabled: true,
173
+ descriptor: (g: any) => [g.nodes.length, g.connections.length],
174
+ blendFactor: 0.15,
175
+ },
176
+ targetSpecies: 10, // Aim for a target number of species to maintain diversity.
177
+ adaptiveTargetSpecies: {
178
+ enabled: true,
179
+ entropyRange: [0.3, 0.8],
180
+ speciesRange: [6, 14],
181
+ smooth: 0.5,
182
+ },
183
+ });
184
+
185
+ // If an initial population is provided, use it to seed the NEAT instance.
186
+ if (initialPopulation && initialPopulation.length > 0) {
187
+ neat.population = initialPopulation.map((net) =>
188
+ (net as Network).clone()
189
+ );
190
+ }
191
+ // If an initial best network is provided, inject it into the population.
192
+ if (initialBestNetwork) {
193
+ neat.population[0] = (initialBestNetwork as Network).clone();
194
+ }
195
+
196
+ // --- Step 5: Evolution State Tracking ---
197
+ // Initialize variables to track the progress of the evolution.
198
+ let bestNetwork: INetwork | undefined =
199
+ evolutionAlgorithmConfig.initialBestNetwork;
200
+ let bestFitness = -Infinity;
201
+ let bestResult: any;
202
+ let stagnantGenerations = 0;
203
+ let completedGenerations = 0;
204
+ let plateauCounter = 0;
205
+ let simplifyMode = false;
206
+ let simplifyRemaining = 0;
207
+ let lastBestFitnessForPlateau = -Infinity;
208
+
209
+ // --- Step 6: Filesystem and Persistence Setup ---
210
+ // Persistence uses Node fs/path. Guard the require calls so bundlers and browsers
211
+ // don't attempt to execute dynamic requires at runtime.
212
+ let fs: any = null;
213
+ let path: any = null;
214
+ try {
215
+ if (typeof window === 'undefined' && typeof require === 'function') {
216
+ // Running under Node.js environment
217
+ fs = require('fs');
218
+ path = require('path');
219
+ }
220
+ } catch {
221
+ // ignore - in browser environments require may not exist
222
+ fs = null;
223
+ path = null;
224
+ }
225
+
226
+ // Helper to yield to the browser frame so the DOM can repaint between heavy sync loops.
227
+ // In Node it falls back to setImmediate or setTimeout(0).
228
+ const flushToFrame = () => {
229
+ // Cooperative pause: if the host sets `window.asciiMazePaused = true`,
230
+ // the evolution loop will yield repeatedly without progressing. This keeps
231
+ // the UI responsive and allows the user to inspect the live state.
232
+ const rafPromise = () =>
233
+ new Promise<void>((resolve) =>
234
+ window.requestAnimationFrame(() => resolve())
235
+ );
236
+ const immediatePromise = () =>
237
+ new Promise<void>((resolve) =>
238
+ typeof setImmediate === 'function'
239
+ ? setImmediate(resolve)
240
+ : setTimeout(resolve, 0)
241
+ );
242
+
243
+ if (
244
+ typeof window !== 'undefined' &&
245
+ typeof window.requestAnimationFrame === 'function'
246
+ ) {
247
+ return new Promise<void>(async (resolve) => {
248
+ // If the pause flag is set, wait until it's cleared before resolving
249
+ const check = async () => {
250
+ if ((window as any).asciiMazePaused) {
251
+ // yield a frame and check again
252
+ await rafPromise();
253
+ setTimeout(check, 0);
254
+ } else {
255
+ rafPromise().then(() => resolve());
256
+ }
257
+ };
258
+ check();
259
+ });
260
+ }
261
+ if (typeof setImmediate === 'function') {
262
+ return new Promise<void>(async (resolve) => {
263
+ const check = async () => {
264
+ if ((globalThis as any).asciiMazePaused) {
265
+ await immediatePromise();
266
+ setTimeout(check, 0);
267
+ } else {
268
+ immediatePromise().then(() => resolve());
269
+ }
270
+ };
271
+ check();
272
+ });
273
+ }
274
+ return new Promise<void>((resolve) => setTimeout(resolve, 0));
275
+ };
276
+
277
+ if (fs && persistDir && !fs.existsSync(persistDir)) {
278
+ try {
279
+ fs.mkdirSync(persistDir, { recursive: true });
280
+ } catch (e) {
281
+ console.error(
282
+ `Could not create persistence directory: ${persistDir}`,
283
+ e
284
+ );
285
+ }
286
+ }
287
+
288
+ // --- Step 7: Lamarckian Learning Setup ---
289
+ // Define the supervised training set for the Lamarckian refinement process.
290
+ // This dataset consists of idealized sensory inputs and the corresponding optimal actions.
291
+ // It helps to quickly teach the networks basic, correct behaviors.
292
+ /**
293
+ * @const {Array<Object>} lamarckianTrainingSet
294
+ * Encodes idealized agent perceptions and the optimal action for each case.
295
+ * This is used for local search (backpropagation) to refine networks between generations.
296
+ *
297
+ * Input format: `[compassScalar, openN, openE, openS, openW, progressDelta]`
298
+ * - `compassScalar`: Direction to the exit (0=N, 0.25=E, 0.5=S, 0.75=W).
299
+ * - `openN/E/S/W`: Whether the path is open in that direction (1=open, 0=wall).
300
+ * - `progressDelta`: Change in distance to the exit ( >0.5 is good, <0.5 is bad).
301
+ *
302
+ * Output format: A one-hot encoded array representing the desired move `[N, E, S, W]`.
303
+ */
304
+ const lamarckianTrainingSet: {
305
+ input: number[];
306
+ output: number[];
307
+ }[] = (() => {
308
+ const ds: { input: number[]; output: number[] }[] = [];
309
+ // Helper to create a smoothed one-hot output vector.
310
+ const OUT = (d: number) =>
311
+ [0, 1, 2, 3].map((i) => (i === d ? 0.92 : 0.02));
312
+ // Helper to add a new training case.
313
+ const add = (inp: number[], dir: number) =>
314
+ ds.push({ input: inp, output: OUT(dir) });
315
+
316
+ // Cases: Single open path with good progress.
317
+ add([0, 1, 0, 0, 0, 0.7], 0); // Go North
318
+ add([0.25, 0, 1, 0, 0, 0.7], 1); // Go East
319
+ add([0.5, 0, 0, 1, 0, 0.7], 2); // Go South
320
+ add([0.75, 0, 0, 0, 1, 0.7], 3); // Go West
321
+
322
+ // Cases: Single open path with very strong progress.
323
+ add([0, 1, 0, 0, 0, 0.9], 0);
324
+ add([0.25, 0, 1, 0, 0, 0.9], 1);
325
+
326
+ // Cases: Two-way junctions, should follow the compass.
327
+ add([0, 1, 0.6, 0, 0, 0.6], 0);
328
+ add([0, 1, 0, 0.6, 0, 0.6], 0);
329
+ add([0.25, 0.6, 1, 0, 0, 0.6], 1);
330
+ add([0.25, 0, 1, 0.6, 0, 0.6], 1);
331
+ add([0.5, 0, 0.6, 1, 0, 0.6], 2);
332
+ add([0.5, 0, 0, 1, 0.6, 0.6], 2);
333
+ add([0.75, 0, 0, 0.6, 1, 0.6], 3);
334
+ add([0.75, 0.6, 0, 0, 1, 0.6], 3);
335
+
336
+ // Cases: Four-way junctions with slight progress, follow compass.
337
+ add([0, 1, 0.8, 0.5, 0.4, 0.55], 0);
338
+ add([0.25, 0.7, 1, 0.6, 0.5, 0.55], 1);
339
+ add([0.5, 0.6, 0.55, 1, 0.65, 0.55], 2);
340
+ add([0.75, 0.5, 0.45, 0.7, 1, 0.55], 3);
341
+
342
+ // Cases: Regressing (moving away from exit), should still follow compass to reorient.
343
+ add([0, 1, 0.3, 0, 0, 0.4], 0);
344
+ add([0.25, 0.5, 1, 0.4, 0, 0.4], 1);
345
+ add([0.5, 0, 0.3, 1, 0.2, 0.4], 2);
346
+ add([0.75, 0, 0.5, 0.4, 1, 0.4], 3);
347
+ // Back-only retreat pattern (only opposite available)
348
+ add([0, 0, 0, 0.001, 0, 0.45], 2);
349
+ // Mild augmentation (jitter openness & progress)
350
+ ds.forEach((p) => {
351
+ for (let i = 1; i <= 4; i++)
352
+ if (p.input[i] === 1 && Math.random() < 0.25)
353
+ p.input[i] = 0.95 + Math.random() * 0.05;
354
+ if (Math.random() < 0.35)
355
+ p.input[5] = Math.min(
356
+ 1,
357
+ Math.max(0, p.input[5] + (Math.random() * 0.1 - 0.05))
358
+ );
359
+ });
360
+ return ds;
361
+ })();
362
+
363
+ // --- Pre-train generation 0 population on supervised compass dataset (Lamarckian warm start) ---
364
+ if (lamarckianTrainingSet.length) {
365
+ // Helper: recenters output node biases to avoid all outputs saturating high simultaneously.
366
+ const centerOutputBiases = (net: any) => {
367
+ try {
368
+ const outs = net.nodes?.filter((n: any) => n.type === 'output');
369
+ if (!outs?.length) return;
370
+ const mean =
371
+ outs.reduce((a: number, n: any) => a + n.bias, 0) / outs.length;
372
+ let varc = 0;
373
+ outs.forEach((n: any) => {
374
+ varc += Math.pow(n.bias - mean, 2);
375
+ });
376
+ varc /= outs.length;
377
+ const std = Math.sqrt(varc);
378
+ outs.forEach((n: any) => {
379
+ n.bias = Math.max(-5, Math.min(5, n.bias - mean)); // subtract mean & clamp
380
+ });
381
+ (net as any)._outputBiasStats = { mean, std };
382
+ } catch {
383
+ /* ignore */
384
+ }
385
+ };
386
+ neat.population.forEach((net: any, idx: number) => {
387
+ try {
388
+ net.train(lamarckianTrainingSet, {
389
+ iterations: Math.min(
390
+ 60,
391
+ 8 + Math.floor(lamarckianTrainingSet.length / 2)
392
+ ),
393
+ error: 0.01,
394
+ rate: 0.002,
395
+ momentum: 0.1,
396
+ batchSize: 4,
397
+ allowRecurrent: true,
398
+ cost: methods.Cost.softmaxCrossEntropy,
399
+ });
400
+ // Strengthen openness bits -> outputs mapping (inputs 1..4 correspond to N,E,S,W open flags)
401
+ try {
402
+ const outputNodes = net.nodes.filter(
403
+ (n: any) => n.type === 'output'
404
+ );
405
+ const inputNodes = net.nodes.filter((n: any) => n.type === 'input');
406
+ for (let d = 0; d < 4; d++) {
407
+ const inNode = inputNodes[d + 1]; // skip compass scalar at index 0
408
+ const outNode = outputNodes[d];
409
+ if (!inNode || !outNode) continue;
410
+ let conn = net.connections.find(
411
+ (c: any) => c.from === inNode && c.to === outNode
412
+ );
413
+ const w = Math.random() * 0.25 + 0.55; // 0.55..0.8
414
+ if (!conn) net.connect(inNode, outNode, w);
415
+ else conn.weight = w;
416
+ }
417
+ // Light compass scalar fan-out with small weights to allow direction discrimination learning
418
+ const compassNode = inputNodes[0];
419
+ if (compassNode) {
420
+ outputNodes.forEach((out: any, d: number) => {
421
+ let conn = net.connections.find(
422
+ (c: any) => c.from === compassNode && c.to === out
423
+ );
424
+ const base = 0.05 + d * 0.01; // slight differentiation
425
+ if (!conn) net.connect(compassNode, out, base);
426
+ else conn.weight = base;
427
+ });
428
+ }
429
+ } catch {
430
+ /* ignore */
431
+ }
432
+ centerOutputBiases(net);
433
+ } catch {
434
+ /* ignore training errors */
435
+ }
436
+ });
437
+ }
438
+
439
+ // Lightweight profiling (opt-in): set env ASCII_MAZE_PROFILE=1 to enable
440
+ const doProfile =
441
+ typeof process !== 'undefined' &&
442
+ typeof process.env !== 'undefined' &&
443
+ process.env.ASCII_MAZE_PROFILE === '1';
444
+ let tEvolveTotal = 0;
445
+ let tLamarckTotal = 0;
446
+ let tSimTotal = 0;
447
+
448
+ // Safe writer: prefer Node stdout when available, else dashboard logger, else console.log
449
+ const safeWrite = (msg: string) => {
450
+ try {
451
+ if (
452
+ typeof process !== 'undefined' &&
453
+ process &&
454
+ process.stdout &&
455
+ typeof process.stdout.write === 'function'
456
+ ) {
457
+ process.stdout.write(msg);
458
+ return;
459
+ }
460
+ } catch {
461
+ /* ignore */
462
+ }
463
+ // Try to use dashboard manager logger if provided
464
+ try {
465
+ if (dashboardManager && (dashboardManager as any).logFunction) {
466
+ try {
467
+ (dashboardManager as any).logFunction(msg);
468
+ return;
469
+ } catch {
470
+ // fall through to console
471
+ }
472
+ }
473
+ } catch {
474
+ /* ignore */
475
+ }
476
+ if (typeof console !== 'undefined' && console.log)
477
+ console.log(msg.trim());
478
+ };
479
+
480
+ while (true) {
481
+ // === Evolutionary Loop ===
482
+ // 1. Darwinian evolution: evolve the population (shuffle genomes)
483
+ // Evolve one generation and get the fittest network.
484
+ // This applies selection, crossover, and mutation to produce the next population.
485
+ const t0 = doProfile ? Date.now() : 0;
486
+ const fittest = await neat.evolve();
487
+ if (doProfile) tEvolveTotal += Date.now() - t0;
488
+ // Force identity activation on output nodes; we apply softmax externally (improves gradient richness & avoids early saturation)
489
+ (neat.population || []).forEach((g: any) => {
490
+ g.nodes?.forEach((n: any) => {
491
+ if (n.type === 'output') n.squash = methods.Activation.identity;
492
+ });
493
+ });
494
+
495
+ // --- Diversity guardrail: if species collapsed to 1 for 20+ generations, temporarily boost mutation + novelty
496
+ (EvolutionEngine as any)._speciesHistory =
497
+ (EvolutionEngine as any)._speciesHistory || [];
498
+ const speciesCount =
499
+ (neat as any).population?.reduce((set: Set<any>, g: any) => {
500
+ if (g.species) set.add(g.species);
501
+ return set;
502
+ }, new Set()).size || 1;
503
+ (EvolutionEngine as any)._speciesHistory.push(speciesCount);
504
+ if ((EvolutionEngine as any)._speciesHistory.length > 50)
505
+ (EvolutionEngine as any)._speciesHistory.shift();
506
+ const recent = (EvolutionEngine as any)._speciesHistory.slice(-20);
507
+ const collapsed =
508
+ recent.length === 20 && recent.every((c: number) => c === 1);
509
+ if (collapsed) {
510
+ // Temporarily escalate mutation params and novelty blend to force exploration
511
+ const neatAny: any = neat as any;
512
+ if (typeof neatAny.mutationRate === 'number')
513
+ neatAny.mutationRate = Math.min(0.6, neatAny.mutationRate * 1.5);
514
+ if (typeof neatAny.mutationAmount === 'number')
515
+ neatAny.mutationAmount = Math.min(0.8, neatAny.mutationAmount * 1.3);
516
+ if (neatAny.config && neatAny.config.novelty) {
517
+ neatAny.config.novelty.blendFactor = Math.min(
518
+ 0.4,
519
+ neatAny.config.novelty.blendFactor * 1.2
520
+ );
521
+ }
522
+ }
523
+
524
+ // --- Dynamic population expansion (only grows; does not shrink) ---
525
+ // Rationale: Start with smaller population for faster iterations; expand search space when stagnating.
526
+ if (
527
+ dynamicPopEnabled &&
528
+ completedGenerations > 0 &&
529
+ neat.population?.length &&
530
+ neat.population.length < dynamicPopMax
531
+ ) {
532
+ const plateauRatio =
533
+ plateauGenerations > 0 ? plateauCounter / plateauGenerations : 0;
534
+ const genTrigger =
535
+ completedGenerations % dynamicPopExpandInterval === 0;
536
+ if (genTrigger && plateauRatio >= dynamicPopPlateauSlack) {
537
+ const currentSize = neat.population.length;
538
+ const targetAdd = Math.min(
539
+ Math.max(1, Math.floor(currentSize * dynamicPopExpandFactor)),
540
+ dynamicPopMax - currentSize
541
+ );
542
+ if (targetAdd > 0) {
543
+ // Sort by score descending; use top quarter as parents
544
+ const sorted = neat.population
545
+ .slice()
546
+ .sort(
547
+ (a: any, b: any) =>
548
+ (b.score || -Infinity) - (a.score || -Infinity)
549
+ );
550
+ const parentPool = sorted.slice(
551
+ 0,
552
+ Math.max(2, Math.ceil(sorted.length * 0.25))
553
+ );
554
+ for (let i = 0; i < targetAdd; i++) {
555
+ const parent =
556
+ parentPool[Math.floor(Math.random() * parentPool.length)];
557
+ try {
558
+ if (typeof (neat as any).spawnFromParent === 'function') {
559
+ // Prefer neat-managed spawn which keeps lineage, ids and caches consistent
560
+ const mutateCount = 1 + (Math.random() < 0.5 ? 1 : 0);
561
+ const child = (neat as any).spawnFromParent(
562
+ parent,
563
+ mutateCount
564
+ );
565
+ neat.population.push(child);
566
+ } else {
567
+ // Defensive fallback: clone + mutate then use neat.addGenome if available
568
+ const clone = parent.clone ? parent.clone() : parent;
569
+ const mutateCount = 1 + (Math.random() < 0.5 ? 1 : 0);
570
+ for (let m = 0; m < mutateCount; m++) {
571
+ try {
572
+ const mutOps = neat.options.mutation || [];
573
+ if (mutOps.length) {
574
+ const op =
575
+ mutOps[Math.floor(Math.random() * mutOps.length)];
576
+ clone.mutate(op);
577
+ }
578
+ } catch {
579
+ /* ignore */
580
+ }
581
+ }
582
+ clone.score = undefined;
583
+ try {
584
+ if (typeof (neat as any).addGenome === 'function') {
585
+ (neat as any).addGenome(clone, [(parent as any)._id]);
586
+ } else {
587
+ // Last resort: push with minimal metadata
588
+ if ((neat as any)._nextGenomeId !== undefined)
589
+ (clone as any)._id = (neat as any)._nextGenomeId++;
590
+ if ((neat as any)._lineageEnabled) {
591
+ (clone as any)._parents = [(parent as any)._id];
592
+ (clone as any)._depth =
593
+ ((parent as any)._depth ?? 0) + 1;
594
+ }
595
+ if (
596
+ typeof (neat as any)._invalidateGenomeCaches ===
597
+ 'function'
598
+ )
599
+ (neat as any)._invalidateGenomeCaches(clone);
600
+ neat.population.push(clone);
601
+ }
602
+ } catch {
603
+ // fallback: best-effort push
604
+ try {
605
+ neat.population.push(clone);
606
+ } catch {}
607
+ }
608
+ }
609
+ } catch {
610
+ /* ignore per-child failures */
611
+ }
612
+ }
613
+ neat.options.popsize = neat.population.length; // keep config consistent
614
+ safeWrite(
615
+ `[DYNAMIC_POP] Expanded population to ${neat.population.length} at gen ${completedGenerations}\n`
616
+ );
617
+ }
618
+ }
619
+ }
620
+
621
+ // 2. Lamarckian evolution: backprop refinement for each individual (everyone goes to school)
622
+ // Each network is trained with a small number of supervised learning steps on the idealized set.
623
+ // This directly modifies the weights that will be inherited by the next generation (Lamarckian).
624
+ if (lamarckianIterations > 0 && lamarckianTrainingSet.length) {
625
+ const t1 = doProfile ? Date.now() : 0;
626
+ // Optional sampling to cut cost
627
+ let trainingSetRef = lamarckianTrainingSet;
628
+ if (
629
+ lamarckianSampleSize &&
630
+ lamarckianSampleSize < lamarckianTrainingSet.length
631
+ ) {
632
+ // Reservoir sample simple approach
633
+ const picked: typeof lamarckianTrainingSet = [];
634
+ for (let i = 0; i < lamarckianSampleSize; i++) {
635
+ picked.push(
636
+ lamarckianTrainingSet[
637
+ (Math.random() * lamarckianTrainingSet.length) | 0
638
+ ]
639
+ );
640
+ }
641
+ trainingSetRef = picked;
642
+ }
643
+ let gradNormSum = 0;
644
+ let gradSamples = 0;
645
+ neat.population.forEach((network) => {
646
+ network.train(trainingSetRef, {
647
+ iterations: lamarckianIterations, // Small to preserve diversity
648
+ error: 0.01,
649
+ rate: 0.001,
650
+ momentum: 0.2,
651
+ batchSize: 2,
652
+ allowRecurrent: true, // allow recurrent connections
653
+ cost: methods.Cost.softmaxCrossEntropy,
654
+ });
655
+ // Re-center output biases after local refinement
656
+ try {
657
+ const outs = (network as any).nodes?.filter(
658
+ (n: any) => n.type === 'output'
659
+ );
660
+ if (outs?.length) {
661
+ const mean =
662
+ outs.reduce((a: number, n: any) => a + n.bias, 0) / outs.length;
663
+ let varc = 0;
664
+ outs.forEach((n: any) => {
665
+ varc += Math.pow(n.bias - mean, 2);
666
+ });
667
+ varc /= outs.length;
668
+ const std = Math.sqrt(varc);
669
+ outs.forEach((n: any) => {
670
+ let adjusted = n.bias - mean;
671
+ if (std < 0.25) adjusted *= 0.7; // compress if low variance cluster
672
+ n.bias = Math.max(-5, Math.min(5, adjusted));
673
+ });
674
+ }
675
+ } catch {
676
+ /* ignore */
677
+ }
678
+ // Capture gradient norm stats if available
679
+ try {
680
+ if (typeof (network as any).getTrainingStats === 'function') {
681
+ const ts = (network as any).getTrainingStats();
682
+ if (ts && Number.isFinite(ts.gradNorm)) {
683
+ gradNormSum += ts.gradNorm;
684
+ gradSamples++;
685
+ }
686
+ }
687
+ } catch {
688
+ /* ignore */
689
+ }
690
+ });
691
+ if (gradSamples > 0) {
692
+ safeWrite(
693
+ `[GRAD] gen=${completedGenerations} meanGradNorm=${(
694
+ gradNormSum / gradSamples
695
+ ).toFixed(4)} samples=${gradSamples}\n`
696
+ );
697
+ }
698
+ if (doProfile) tLamarckTotal += Date.now() - t1;
699
+ }
700
+
701
+ // 3. Baldwinian refinement: further train the fittest individual for evaluation only.
702
+ // This improves its performance for this generation's evaluation, but only the Lamarckian-trained
703
+ // weights are inherited by offspring. (If you want pure Lamarckian, remove this step.)
704
+ /*
705
+ fittest.train(lamarckianTrainingSet, {
706
+ iterations: 1000, // More steps for the fittest
707
+ error: 0.01,
708
+ rate: 0.001,
709
+ momentum: 0.2,
710
+ batchSize: 20,
711
+ allowRecurrent: true, // allow recurrent connections
712
+ });
713
+ */
714
+
715
+ // 4. Evaluate and track progress
716
+ const fitness = fittest.score ?? 0;
717
+ completedGenerations++;
718
+
719
+ // Plateau detection logic
720
+ if (fitness > lastBestFitnessForPlateau + plateauImprovementThreshold) {
721
+ plateauCounter = 0;
722
+ lastBestFitnessForPlateau = fitness;
723
+ } else {
724
+ plateauCounter++;
725
+ }
726
+ // Enter simplify mode
727
+ if (!simplifyMode && plateauCounter >= plateauGenerations) {
728
+ simplifyMode = true;
729
+ simplifyRemaining = simplifyDuration;
730
+ plateauCounter = 0; // reset
731
+ }
732
+ // Apply simplify pruning if active
733
+ if (simplifyMode) {
734
+ // Disable weakest fraction of enabled connections in each genome
735
+ neat.population.forEach((g: any) => {
736
+ const enabledConns = g.connections.filter(
737
+ (c: any) => c.enabled !== false
738
+ );
739
+ if (!enabledConns.length) return;
740
+ const pruneCount = Math.max(
741
+ 1,
742
+ Math.floor(enabledConns.length * simplifyPruneFraction)
743
+ );
744
+ let candidates = enabledConns.slice();
745
+ if (simplifyStrategy === 'weakRecurrentPreferred') {
746
+ // Identify recurrent (self-loop or cycle gating) connections first; heuristic: from===to or gater present
747
+ const recurrent = candidates.filter(
748
+ (c: any) => c.from === c.to || c.gater
749
+ );
750
+ const nonRecurrent = candidates.filter(
751
+ (c: any) => !(c.from === c.to || c.gater)
752
+ );
753
+ // Sort each group by absolute weight ascending
754
+ recurrent.sort(
755
+ (a: any, b: any) => Math.abs(a.weight) - Math.abs(b.weight)
756
+ );
757
+ nonRecurrent.sort(
758
+ (a: any, b: any) => Math.abs(a.weight) - Math.abs(b.weight)
759
+ );
760
+ // Prefer pruning weak recurrent connections first, then remaining weak weights
761
+ candidates = [...recurrent, ...nonRecurrent];
762
+ } else {
763
+ candidates.sort(
764
+ (a: any, b: any) => Math.abs(a.weight) - Math.abs(b.weight)
765
+ );
766
+ }
767
+ candidates
768
+ .slice(0, pruneCount)
769
+ .forEach((c: any) => (c.enabled = false));
770
+ });
771
+ simplifyRemaining--;
772
+ if (simplifyRemaining <= 0) simplifyMode = false;
773
+ }
774
+
775
+ // Simulate the agent using the fittest network
776
+ // This provides a detailed result (success, progress, steps, etc.)
777
+ const t2 = doProfile ? Date.now() : 0;
778
+ const generationResult = MazeMovement.simulateAgent(
779
+ fittest,
780
+ encodedMaze,
781
+ startPosition,
782
+ exitPosition,
783
+ distanceMap,
784
+ agentSimConfig.maxSteps
785
+ );
786
+ // Capture output history from simulation for telemetry (mazeMovement stores on network)
787
+ try {
788
+ (fittest as any)._lastStepOutputs =
789
+ (fittest as any)._lastStepOutputs ||
790
+ (fittest as any)._lastStepOutputs;
791
+ } catch {}
792
+ // Attach auxiliary metrics to fittest genome for potential external analysis
793
+ (fittest as any)._saturationFraction =
794
+ generationResult.saturationFraction;
795
+ (fittest as any)._actionEntropy = generationResult.actionEntropy;
796
+ // Saturation-based pruning heuristic: if outputs are chronically saturated reduce weak outgoing weights
797
+ if (
798
+ generationResult.saturationFraction &&
799
+ generationResult.saturationFraction > 0.5
800
+ ) {
801
+ try {
802
+ const outNodes = fittest.nodes.filter(
803
+ (n: any) => n.type === 'output'
804
+ );
805
+ // Identify hidden nodes whose outgoing weights converge to all outputs with similar large activations
806
+ const hidden = fittest.nodes.filter((n: any) => n.type === 'hidden');
807
+ hidden.forEach((h: any) => {
808
+ // Gather outgoing connections to outputs
809
+ const outs = h.connections.out.filter(
810
+ (c: any) => outNodes.includes(c.to) && c.enabled !== false
811
+ );
812
+ if (outs.length >= 2) {
813
+ // Compute absolute weight mean & variance
814
+ const weights = outs.map((c: any) => Math.abs(c.weight));
815
+ const mean =
816
+ weights.reduce((a: number, b: number) => a + b, 0) /
817
+ weights.length;
818
+ const varc =
819
+ weights.reduce(
820
+ (a: number, b: number) => a + Math.pow(b - mean, 2),
821
+ 0
822
+ ) / weights.length;
823
+ if (mean < 0.5 && varc < 0.01) {
824
+ // Likely low-signal uniform fan-out: disable weakest half to force differentiation
825
+ outs.sort(
826
+ (a: any, b: any) => Math.abs(a.weight) - Math.abs(b.weight)
827
+ );
828
+ const disableCount = Math.max(1, Math.floor(outs.length / 2));
829
+ for (let i = 0; i < disableCount; i++) outs[i].enabled = false;
830
+ }
831
+ }
832
+ });
833
+ } catch {
834
+ /* soft-fail */
835
+ }
836
+ }
837
+ // Instrumentation: log approximate action entropy (based on path move variety)
838
+ if (completedGenerations % logEvery === 0) {
839
+ try {
840
+ const movesRaw = generationResult.path.map(
841
+ (p: [number, number], idx: number, arr: any[]) => {
842
+ if (idx === 0) return null;
843
+ const prev = arr[idx - 1];
844
+ const dx = p[0] - prev[0];
845
+ const dy = p[1] - prev[1];
846
+ if (dx === 0 && dy === -1) return 0;
847
+ if (dx === 1 && dy === 0) return 1;
848
+ if (dx === 0 && dy === 1) return 2;
849
+ if (dx === -1 && dy === 0) return 3;
850
+ return null;
851
+ }
852
+ );
853
+ const moves: number[] = [];
854
+ for (const mv of movesRaw) {
855
+ if (mv !== null) moves.push(mv as number);
856
+ }
857
+ const counts = [0, 0, 0, 0];
858
+ moves.forEach((m: number) => counts[m]++);
859
+ const totalMoves = moves.length || 1;
860
+ const probs = counts.map((c) => c / totalMoves);
861
+ let entropy = 0;
862
+ probs.forEach((p) => {
863
+ if (p > 0) entropy += -p * Math.log(p);
864
+ });
865
+ // natural log entropy max ln(4)=1.386; normalize
866
+ const entropyNorm = entropy / Math.log(4);
867
+ safeWrite(
868
+ `[ACTION_ENTROPY] gen=${completedGenerations} entropyNorm=${entropyNorm.toFixed(
869
+ 3
870
+ )} uniqueMoves=${counts.filter((c) => c > 0).length} pathLen=${
871
+ generationResult.path.length
872
+ }\n`
873
+ );
874
+ // Output bias stats for fittest network
875
+ try {
876
+ const outs = fittest.nodes.filter((n: any) => n.type === 'output');
877
+ if (outs.length) {
878
+ const meanB =
879
+ outs.reduce((a: number, n: any) => a + n.bias, 0) / outs.length;
880
+ let varcB = 0;
881
+ outs.forEach((n: any) => {
882
+ varcB += Math.pow(n.bias - meanB, 2);
883
+ });
884
+ varcB /= outs.length;
885
+ const stdB = Math.sqrt(varcB);
886
+ safeWrite(
887
+ `[OUTPUT_BIAS] gen=${completedGenerations} mean=${meanB.toFixed(
888
+ 3
889
+ )} std=${stdB.toFixed(3)} biases=${outs
890
+ .map((o: any) => o.bias.toFixed(2))
891
+ .join(',')}\n`
892
+ );
893
+ }
894
+ } catch {}
895
+ // Enhanced output logits / softmax telemetry (if last step outputs captured)
896
+ try {
897
+ const lastHist: number[][] =
898
+ (fittest as any)._lastStepOutputs || [];
899
+ if (lastHist.length) {
900
+ const recent = lastHist.slice(-40);
901
+ // Aggregate per-output mean & std
902
+ const k = 4;
903
+ const means = new Array(k).fill(0);
904
+ recent.forEach((v) => {
905
+ for (let i = 0; i < k; i++) means[i] += v[i];
906
+ });
907
+ for (let i = 0; i < k; i++) means[i] /= recent.length;
908
+ const stds = new Array(k).fill(0);
909
+ recent.forEach((v) => {
910
+ for (let i = 0; i < k; i++)
911
+ stds[i] += Math.pow(v[i] - means[i], 2);
912
+ });
913
+ for (let i = 0; i < k; i++)
914
+ stds[i] = Math.sqrt(stds[i] / recent.length);
915
+ // Kurtosis (Fisher, subtract 3)
916
+ const kurt = new Array(k).fill(0);
917
+ recent.forEach((v) => {
918
+ for (let i = 0; i < k; i++)
919
+ kurt[i] += Math.pow(v[i] - means[i], 4);
920
+ });
921
+ for (let i = 0; i < k; i++) {
922
+ const denom = Math.pow(stds[i] || 1e-9, 4) * recent.length;
923
+ kurt[i] = denom > 0 ? kurt[i] / denom - 3 : 0;
924
+ }
925
+ // Softmax distribution mean entropy over recent steps
926
+ let entAgg = 0;
927
+ recent.forEach((v) => {
928
+ const max = Math.max(...v);
929
+ const exps = v.map((x) => Math.exp(x - max));
930
+ const sum = exps.reduce((a, b) => a + b, 0) || 1;
931
+ const probs = exps.map((e) => e / sum);
932
+ let e = 0;
933
+ probs.forEach((p) => {
934
+ if (p > 0) e += -p * Math.log(p);
935
+ });
936
+ entAgg += e / Math.log(4);
937
+ });
938
+ const entMean = entAgg / recent.length;
939
+ // Decision stability: fraction of consecutive identical argmax
940
+ let stable = 0,
941
+ totalTrans = 0;
942
+ let prevDir = -1;
943
+ recent.forEach((v) => {
944
+ const arg = v.indexOf(Math.max(...v));
945
+ if (prevDir === arg) stable++;
946
+ if (prevDir !== -1) totalTrans++;
947
+ prevDir = arg;
948
+ });
949
+ const stability = totalTrans ? stable / totalTrans : 0;
950
+ safeWrite(
951
+ `[LOGITS] gen=${completedGenerations} means=${means
952
+ .map((m) => m.toFixed(3))
953
+ .join(',')} stds=${stds
954
+ .map((s) => s.toFixed(3))
955
+ .join(',')} kurt=${kurt
956
+ .map((kv) => kv.toFixed(2))
957
+ .join(',')} entMean=${entMean.toFixed(
958
+ 3
959
+ )} stability=${stability.toFixed(3)} steps=${recent.length}\n`
960
+ );
961
+ // Anti-collapse trigger: if all std below threshold & entropy low OR stability extremely high
962
+ (EvolutionEngine as any)._collapseStreak =
963
+ (EvolutionEngine as any)._collapseStreak || 0;
964
+ const collapsed =
965
+ stds.every((s) => s < 0.005) &&
966
+ (entMean < 0.35 || stability > 0.97);
967
+ if (collapsed) (EvolutionEngine as any)._collapseStreak++;
968
+ else (EvolutionEngine as any)._collapseStreak = 0;
969
+ if ((EvolutionEngine as any)._collapseStreak === 6) {
970
+ // Reinitialize a fraction of non-elite population's output weights to break collapse
971
+ try {
972
+ const eliteCount = neat.options.elitism || 0;
973
+ const pop = neat.population || [];
974
+ const reinitTargets = pop
975
+ .slice(eliteCount)
976
+ .filter(() => Math.random() < 0.3);
977
+ let connReset = 0,
978
+ biasReset = 0;
979
+ reinitTargets.forEach((g: any) => {
980
+ const outs = g.nodes.filter(
981
+ (n: any) => n.type === 'output'
982
+ );
983
+ // Reset biases
984
+ outs.forEach((o: any) => {
985
+ o.bias = Math.random() * 0.2 - 0.1;
986
+ biasReset++;
987
+ });
988
+ // Reset incoming connection weights to outputs
989
+ g.connections.forEach((c: any) => {
990
+ if (outs.includes(c.to)) {
991
+ c.weight = Math.random() * 0.4 - 0.2;
992
+ connReset++;
993
+ }
994
+ });
995
+ });
996
+ safeWrite(
997
+ `[ANTICOLLAPSE] gen=${completedGenerations} reinitGenomes=${reinitTargets.length} connReset=${connReset} biasReset=${biasReset}\n`
998
+ );
999
+ } catch {
1000
+ /* ignore */
1001
+ }
1002
+ }
1003
+ }
1004
+ } catch {}
1005
+ // Exploration ratio (unique / path length)
1006
+ try {
1007
+ const unique = generationResult.path.length
1008
+ ? new Set(generationResult.path.map((p: any) => p.join(','))).size
1009
+ : 0;
1010
+ const ratio = generationResult.path.length
1011
+ ? unique / generationResult.path.length
1012
+ : 0;
1013
+ safeWrite(
1014
+ `[EXPLORE] gen=${completedGenerations} unique=${unique} pathLen=${
1015
+ generationResult.path.length
1016
+ } ratio=${ratio.toFixed(
1017
+ 3
1018
+ )} progress=${generationResult.progress.toFixed(
1019
+ 1
1020
+ )} satFrac=${(generationResult as any).saturationFraction?.toFixed(
1021
+ 3
1022
+ )}\n`
1023
+ );
1024
+ } catch {}
1025
+ // Diversity metrics (species distribution + basic connection weight variance)
1026
+ try {
1027
+ const pop: any[] = neat.population || [];
1028
+ const speciesCounts: Record<string, number> = {};
1029
+ pop.forEach((g) => {
1030
+ const sid = g.species != null ? String(g.species) : 'none';
1031
+ speciesCounts[sid] = (speciesCounts[sid] || 0) + 1;
1032
+ });
1033
+ const counts = Object.values(speciesCounts);
1034
+ const total = counts.reduce((a, b) => a + b, 0) || 1;
1035
+ const simpson =
1036
+ 1 - counts.reduce((a, b) => a + Math.pow(b / total, 2), 0); // Simpson diversity index
1037
+ // Weight variance sample (subset for speed)
1038
+ let wMean = 0,
1039
+ wCount = 0;
1040
+ const sample = pop.slice(0, Math.min(pop.length, 40));
1041
+ sample.forEach((g) => {
1042
+ g.connections.forEach((c: any) => {
1043
+ if (c.enabled !== false) {
1044
+ wMean += c.weight;
1045
+ wCount++;
1046
+ }
1047
+ });
1048
+ });
1049
+ wMean = wCount ? wMean / wCount : 0;
1050
+ let wVar = 0;
1051
+ sample.forEach((g) => {
1052
+ g.connections.forEach((c: any) => {
1053
+ if (c.enabled !== false) wVar += Math.pow(c.weight - wMean, 2);
1054
+ });
1055
+ });
1056
+ const wStd = wCount ? Math.sqrt(wVar / wCount) : 0;
1057
+ safeWrite(
1058
+ `[DIVERSITY] gen=${completedGenerations} species=${
1059
+ Object.keys(speciesCounts).length
1060
+ } simpson=${simpson.toFixed(3)} weightStd=${wStd.toFixed(3)}\n`
1061
+ );
1062
+ } catch {}
1063
+ } catch {}
1064
+ }
1065
+ if (doProfile) tSimTotal += Date.now() - t2;
1066
+
1067
+ // If new best, update tracking and dashboard
1068
+ if (fitness > bestFitness) {
1069
+ bestFitness = fitness;
1070
+ bestNetwork = fittest;
1071
+ bestResult = generationResult;
1072
+ stagnantGenerations = 0;
1073
+ dashboardManager.update(
1074
+ maze,
1075
+ generationResult,
1076
+ fittest,
1077
+ completedGenerations,
1078
+ neat
1079
+ );
1080
+ try {
1081
+ // yield to the browser to allow DOM paint
1082
+ await flushToFrame();
1083
+ } catch {}
1084
+ } else {
1085
+ stagnantGenerations++;
1086
+ // Periodically update dashboard with current best
1087
+ if (completedGenerations % logEvery === 0) {
1088
+ if (bestNetwork && bestResult) {
1089
+ dashboardManager.update(
1090
+ maze,
1091
+ bestResult,
1092
+ bestNetwork,
1093
+ completedGenerations,
1094
+ neat
1095
+ );
1096
+ try {
1097
+ await flushToFrame();
1098
+ } catch {}
1099
+ }
1100
+ }
1101
+ }
1102
+
1103
+ // Persistence snapshot
1104
+ if (
1105
+ persistEvery > 0 &&
1106
+ completedGenerations % persistEvery === 0 &&
1107
+ bestNetwork
1108
+ ) {
1109
+ try {
1110
+ const snap: any = {
1111
+ generation: completedGenerations,
1112
+ bestFitness: bestFitness,
1113
+ simplifyMode,
1114
+ plateauCounter,
1115
+ timestamp: Date.now(),
1116
+ telemetryTail: neat.getTelemetry
1117
+ ? neat.getTelemetry().slice(-5)
1118
+ : undefined,
1119
+ };
1120
+ const popSorted = neat.population
1121
+ .slice()
1122
+ .sort(
1123
+ (a: any, b: any) =>
1124
+ (b.score || -Infinity) - (a.score || -Infinity)
1125
+ );
1126
+ const top = popSorted
1127
+ .slice(0, persistTopK)
1128
+ .map((g: any, idx: number) => ({
1129
+ idx,
1130
+ score: g.score,
1131
+ nodes: g.nodes.length,
1132
+ connections: g.connections.length,
1133
+ json: g.toJSON ? g.toJSON() : undefined,
1134
+ }));
1135
+ snap.top = top;
1136
+ const file = path.join(
1137
+ persistDir,
1138
+ `snapshot_gen${completedGenerations}.json`
1139
+ );
1140
+ fs.writeFileSync(file, JSON.stringify(snap, null, 2));
1141
+ } catch (e) {
1142
+ // ignore persistence errors
1143
+ }
1144
+ }
1145
+
1146
+ // Stop if solved or sufficient progress
1147
+ if (bestResult?.success && bestResult.progress >= minProgressToPass) {
1148
+ if (bestNetwork && bestResult) {
1149
+ dashboardManager.update(
1150
+ maze,
1151
+ bestResult,
1152
+ bestNetwork,
1153
+ completedGenerations,
1154
+ neat
1155
+ );
1156
+ try {
1157
+ await flushToFrame();
1158
+ } catch {}
1159
+ }
1160
+ break;
1161
+ }
1162
+
1163
+ // Stop if stagnation limit reached
1164
+ if (stagnantGenerations >= maxStagnantGenerations) {
1165
+ if (bestNetwork && bestResult) {
1166
+ dashboardManager.update(
1167
+ maze,
1168
+ bestResult,
1169
+ bestNetwork,
1170
+ completedGenerations,
1171
+ neat
1172
+ );
1173
+ try {
1174
+ await flushToFrame();
1175
+ } catch {}
1176
+ }
1177
+ break;
1178
+ }
1179
+
1180
+ // Safety cap on generations
1181
+ if (completedGenerations >= maxGenerations) {
1182
+ break;
1183
+ }
1184
+ }
1185
+
1186
+ if (doProfile && completedGenerations > 0) {
1187
+ const gen = completedGenerations;
1188
+ const avgEvolve = (tEvolveTotal / gen).toFixed(2);
1189
+ const avgLamarck = (tLamarckTotal / gen).toFixed(2);
1190
+ const avgSim = (tSimTotal / gen).toFixed(2);
1191
+ // Direct stdout to avoid jest buffering suppression
1192
+ safeWrite(
1193
+ `\n[PROFILE] Generations=${gen} avg(ms): evolve=${avgEvolve} lamarck=${avgLamarck} sim=${avgSim} totalPerGen=${(
1194
+ +avgEvolve +
1195
+ +avgLamarck +
1196
+ +avgSim
1197
+ ).toFixed(2)}\n`
1198
+ );
1199
+ }
1200
+
1201
+ // Return the best network, its result, and the NEAT instance
1202
+ return {
1203
+ bestNetwork,
1204
+ bestResult,
1205
+ neat,
1206
+ };
1207
+ }
1208
+
1209
+ /**
1210
+ * Prints the structure of a given neural network to the console.
1211
+ *
1212
+ * This is useful for debugging and understanding the evolved architectures.
1213
+ * It prints the number of nodes, their types, activation functions, and connection details.
1214
+ *
1215
+ * @param network - The neural network to inspect.
1216
+ */
1217
+ static printNetworkStructure(network: INetwork) {
1218
+ // Print high-level network structure and statistics
1219
+ console.log('Network Structure:');
1220
+ console.log('Nodes: ', network.nodes?.length); // Total number of nodes
1221
+ const inputNodes = network.nodes?.filter((n) => n.type === 'input');
1222
+ const outputNodes = network.nodes?.filter((n) => n.type === 'output');
1223
+ const hiddenNodes = network.nodes?.filter((n) => n.type === 'hidden');
1224
+ console.log('Input nodes: ', inputNodes?.length); // Number of input nodes
1225
+ console.log('Hidden nodes: ', hiddenNodes?.length); // Number of hidden nodes
1226
+ console.log('Output nodes: ', outputNodes?.length); // Number of output nodes
1227
+ console.log(
1228
+ 'Activation functions: ',
1229
+ network.nodes?.map((n) => n.squash?.name || n.squash)
1230
+ ); // List of activation functions
1231
+ console.log('Connections: ', network.connections?.length); // Number of connections
1232
+ const recurrent = network.connections?.some(
1233
+ (c) => c.gater || c.from === c.to
1234
+ ); // Whether there are recurrent/gated connections
1235
+ console.log('Has recurrent/gated connections: ', recurrent);
1236
+ // if (network.layers) { // Property 'layers' does not exist on type 'INetwork'.
1237
+ // Object.entries(network.layers).forEach(([name, layer]) => {
1238
+ // if (Array.isArray(layer)) {
1239
+ // console.log(`Layer ${name}:`, layer.length, ' nodes');
1240
+ // } else if (layer && typeof layer === 'object' && 'nodes' in layer) {
1241
+ // // For Neataptic Layer objects
1242
+ // // @ts-ignore
1243
+ // console.log(`Layer ${name}: `, layer.nodes.length, ' nodes');
1244
+ // }
1245
+ // });
1246
+ // }
1247
+ }
1248
+ }