@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.
- package/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +27 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +28 -0
- package/.github/workflows/ci.yml +41 -0
- package/.github/workflows/deploy-pages.yml +29 -0
- package/.github/workflows/manual_release_pipeline.yml +62 -0
- package/.github/workflows/publish.yml +85 -0
- package/.github/workflows/release_dispatch.yml +38 -0
- package/.travis.yml +5 -0
- package/CONTRIBUTING.md +92 -0
- package/LICENSE +24 -0
- package/ONNX_EXPORT.md +87 -0
- package/README.md +1173 -0
- package/RELEASE.md +54 -0
- package/dist-docs/package.json +1 -0
- package/dist-docs/scripts/generate-docs.d.ts +2 -0
- package/dist-docs/scripts/generate-docs.d.ts.map +1 -0
- package/dist-docs/scripts/generate-docs.js +536 -0
- package/dist-docs/scripts/generate-docs.js.map +1 -0
- package/dist-docs/scripts/render-docs-html.d.ts +2 -0
- package/dist-docs/scripts/render-docs-html.d.ts.map +1 -0
- package/dist-docs/scripts/render-docs-html.js +148 -0
- package/dist-docs/scripts/render-docs-html.js.map +1 -0
- package/docs/FOLDERS.md +14 -0
- package/docs/README.md +1173 -0
- package/docs/architecture/README.md +1391 -0
- package/docs/architecture/index.html +938 -0
- package/docs/architecture/network/README.md +1210 -0
- package/docs/architecture/network/index.html +908 -0
- package/docs/assets/ascii-maze.bundle.js +16542 -0
- package/docs/assets/ascii-maze.bundle.js.map +7 -0
- package/docs/index.html +1419 -0
- package/docs/methods/README.md +670 -0
- package/docs/methods/index.html +477 -0
- package/docs/multithreading/README.md +274 -0
- package/docs/multithreading/index.html +215 -0
- package/docs/multithreading/workers/README.md +23 -0
- package/docs/multithreading/workers/browser/README.md +39 -0
- package/docs/multithreading/workers/browser/index.html +70 -0
- package/docs/multithreading/workers/index.html +57 -0
- package/docs/multithreading/workers/node/README.md +33 -0
- package/docs/multithreading/workers/node/index.html +66 -0
- package/docs/neat/README.md +1284 -0
- package/docs/neat/index.html +906 -0
- package/docs/src/README.md +2659 -0
- package/docs/src/index.html +1579 -0
- package/jest.config.ts +32 -0
- package/package.json +99 -0
- package/plans/HyperMorphoNEAT.md +293 -0
- package/plans/ONNX_EXPORT_PLAN.md +46 -0
- package/scripts/generate-docs.ts +486 -0
- package/scripts/render-docs-html.ts +138 -0
- package/scripts/types.d.ts +2 -0
- package/src/README.md +2659 -0
- package/src/architecture/README.md +1391 -0
- package/src/architecture/activationArrayPool.ts +135 -0
- package/src/architecture/architect.ts +635 -0
- package/src/architecture/connection.ts +148 -0
- package/src/architecture/group.ts +406 -0
- package/src/architecture/layer.ts +804 -0
- package/src/architecture/network/README.md +1210 -0
- package/src/architecture/network/network.activate.ts +223 -0
- package/src/architecture/network/network.connect.ts +157 -0
- package/src/architecture/network/network.deterministic.ts +167 -0
- package/src/architecture/network/network.evolve.ts +426 -0
- package/src/architecture/network/network.gating.ts +186 -0
- package/src/architecture/network/network.genetic.ts +247 -0
- package/src/architecture/network/network.mutate.ts +624 -0
- package/src/architecture/network/network.onnx.ts +463 -0
- package/src/architecture/network/network.prune.ts +216 -0
- package/src/architecture/network/network.remove.ts +96 -0
- package/src/architecture/network/network.serialize.ts +309 -0
- package/src/architecture/network/network.slab.ts +262 -0
- package/src/architecture/network/network.standalone.ts +246 -0
- package/src/architecture/network/network.stats.ts +59 -0
- package/src/architecture/network/network.topology.ts +86 -0
- package/src/architecture/network/network.training.ts +1278 -0
- package/src/architecture/network.ts +1302 -0
- package/src/architecture/node.ts +1288 -0
- package/src/architecture/onnx.ts +3 -0
- package/src/config.ts +83 -0
- package/src/methods/README.md +670 -0
- package/src/methods/activation.ts +372 -0
- package/src/methods/connection.ts +31 -0
- package/src/methods/cost.ts +347 -0
- package/src/methods/crossover.ts +63 -0
- package/src/methods/gating.ts +43 -0
- package/src/methods/methods.ts +8 -0
- package/src/methods/mutation.ts +300 -0
- package/src/methods/rate.ts +257 -0
- package/src/methods/selection.ts +65 -0
- package/src/multithreading/README.md +274 -0
- package/src/multithreading/multi.ts +339 -0
- package/src/multithreading/workers/README.md +23 -0
- package/src/multithreading/workers/browser/README.md +39 -0
- package/src/multithreading/workers/browser/testworker.ts +99 -0
- package/src/multithreading/workers/node/README.md +33 -0
- package/src/multithreading/workers/node/testworker.ts +72 -0
- package/src/multithreading/workers/node/worker.ts +70 -0
- package/src/multithreading/workers/workers.ts +22 -0
- package/src/neat/README.md +1284 -0
- package/src/neat/neat.adaptive.ts +544 -0
- package/src/neat/neat.compat.ts +164 -0
- package/src/neat/neat.constants.ts +20 -0
- package/src/neat/neat.diversity.ts +217 -0
- package/src/neat/neat.evaluate.ts +328 -0
- package/src/neat/neat.evolve.ts +1026 -0
- package/src/neat/neat.export.ts +249 -0
- package/src/neat/neat.helpers.ts +235 -0
- package/src/neat/neat.lineage.ts +220 -0
- package/src/neat/neat.multiobjective.ts +260 -0
- package/src/neat/neat.mutation.ts +718 -0
- package/src/neat/neat.objectives.ts +157 -0
- package/src/neat/neat.pruning.ts +190 -0
- package/src/neat/neat.selection.ts +269 -0
- package/src/neat/neat.speciation.ts +460 -0
- package/src/neat/neat.species.ts +151 -0
- package/src/neat/neat.telemetry.exports.ts +469 -0
- package/src/neat/neat.telemetry.ts +933 -0
- package/src/neat/neat.types.ts +275 -0
- package/src/neat.ts +1042 -0
- package/src/neataptic.ts +10 -0
- package/test/architecture/activationArrayPool.capacity.test.ts +19 -0
- package/test/architecture/activationArrayPool.test.ts +46 -0
- package/test/architecture/connection.test.ts +290 -0
- package/test/architecture/group.test.ts +950 -0
- package/test/architecture/layer.test.ts +1535 -0
- package/test/architecture/network.pruning.test.ts +65 -0
- package/test/architecture/node.test.ts +1602 -0
- package/test/examples/asciiMaze/asciiMaze.e2e.test.ts +499 -0
- package/test/examples/asciiMaze/asciiMaze.ts +41 -0
- package/test/examples/asciiMaze/browser-entry.ts +164 -0
- package/test/examples/asciiMaze/browserLogger.ts +221 -0
- package/test/examples/asciiMaze/browserTerminalUtility.ts +48 -0
- package/test/examples/asciiMaze/colors.ts +119 -0
- package/test/examples/asciiMaze/dashboardManager.ts +968 -0
- package/test/examples/asciiMaze/evolutionEngine.ts +1248 -0
- package/test/examples/asciiMaze/fitness.ts +136 -0
- package/test/examples/asciiMaze/index.html +128 -0
- package/test/examples/asciiMaze/index.ts +26 -0
- package/test/examples/asciiMaze/interfaces.ts +235 -0
- package/test/examples/asciiMaze/mazeMovement.ts +996 -0
- package/test/examples/asciiMaze/mazeUtils.ts +278 -0
- package/test/examples/asciiMaze/mazeVision.ts +402 -0
- package/test/examples/asciiMaze/mazeVisualization.ts +585 -0
- package/test/examples/asciiMaze/mazes.ts +245 -0
- package/test/examples/asciiMaze/networkRefinement.ts +76 -0
- package/test/examples/asciiMaze/networkVisualization.ts +901 -0
- package/test/examples/asciiMaze/terminalUtility.ts +73 -0
- package/test/methods/activation.test.ts +1142 -0
- package/test/methods/connection.test.ts +146 -0
- package/test/methods/cost.test.ts +1123 -0
- package/test/methods/crossover.test.ts +202 -0
- package/test/methods/gating.test.ts +144 -0
- package/test/methods/mutation.test.ts +451 -0
- package/test/methods/optimizers.advanced.test.ts +80 -0
- package/test/methods/optimizers.behavior.test.ts +105 -0
- package/test/methods/optimizers.formula.test.ts +89 -0
- package/test/methods/rate.cosineWarmRestarts.test.ts +44 -0
- package/test/methods/rate.linearWarmupDecay.test.ts +41 -0
- package/test/methods/rate.reduceOnPlateau.test.ts +45 -0
- package/test/methods/rate.test.ts +684 -0
- package/test/methods/selection.test.ts +245 -0
- package/test/multithreading/activations.functions.test.ts +54 -0
- package/test/multithreading/multi.test.ts +290 -0
- package/test/multithreading/worker.node.process.test.ts +39 -0
- package/test/multithreading/workers.coverage.test.ts +36 -0
- package/test/multithreading/workers.dynamic.import.test.ts +8 -0
- package/test/neat/neat.adaptive.complexityBudget.test.ts +34 -0
- package/test/neat/neat.adaptive.criterion.complexity.test.ts +50 -0
- package/test/neat/neat.adaptive.mutation.strategy.test.ts +37 -0
- package/test/neat/neat.adaptive.operator.decay.test.ts +31 -0
- package/test/neat/neat.adaptive.phasedComplexity.test.ts +25 -0
- package/test/neat/neat.adaptive.pruning.test.ts +25 -0
- package/test/neat/neat.adaptive.targetSpecies.test.ts +43 -0
- package/test/neat/neat.additional.coverage.test.ts +126 -0
- package/test/neat/neat.advanced.enhancements.test.ts +85 -0
- package/test/neat/neat.advanced.test.ts +589 -0
- package/test/neat/neat.diversity.autocompat.test.ts +47 -0
- package/test/neat/neat.diversity.metrics.test.ts +21 -0
- package/test/neat/neat.diversity.stats.test.ts +44 -0
- package/test/neat/neat.enhancements.test.ts +79 -0
- package/test/neat/neat.entropy.ancestorAdaptive.test.ts +133 -0
- package/test/neat/neat.entropy.compat.csv.test.ts +108 -0
- package/test/neat/neat.evolution.pruning.test.ts +39 -0
- package/test/neat/neat.fastmode.autotune.test.ts +42 -0
- package/test/neat/neat.innovation.test.ts +134 -0
- package/test/neat/neat.lineage.antibreeding.test.ts +35 -0
- package/test/neat/neat.lineage.entropy.test.ts +56 -0
- package/test/neat/neat.lineage.inbreeding.test.ts +49 -0
- package/test/neat/neat.lineage.pressure.test.ts +29 -0
- package/test/neat/neat.multiobjective.adaptive.test.ts +57 -0
- package/test/neat/neat.multiobjective.dynamic.schedule.test.ts +46 -0
- package/test/neat/neat.multiobjective.dynamic.test.ts +31 -0
- package/test/neat/neat.multiobjective.fastsort.delegation.test.ts +51 -0
- package/test/neat/neat.multiobjective.prune.test.ts +39 -0
- package/test/neat/neat.multiobjective.test.ts +21 -0
- package/test/neat/neat.mutation.undefined.pool.test.ts +24 -0
- package/test/neat/neat.objective.events.test.ts +26 -0
- package/test/neat/neat.objective.importance.test.ts +21 -0
- package/test/neat/neat.objective.lifetimes.test.ts +33 -0
- package/test/neat/neat.offspring.allocation.test.ts +22 -0
- package/test/neat/neat.operator.bandit.test.ts +17 -0
- package/test/neat/neat.operator.phases.test.ts +38 -0
- package/test/neat/neat.pruneInactive.behavior.test.ts +54 -0
- package/test/neat/neat.reenable.adaptation.test.ts +18 -0
- package/test/neat/neat.rng.state.test.ts +22 -0
- package/test/neat/neat.spawn.add.test.ts +123 -0
- package/test/neat/neat.speciation.test.ts +96 -0
- package/test/neat/neat.species.allocation.telemetry.test.ts +26 -0
- package/test/neat/neat.species.history.csv.test.ts +24 -0
- package/test/neat/neat.telemetry.advanced.test.ts +226 -0
- package/test/neat/neat.telemetry.csv.lineage.test.ts +19 -0
- package/test/neat/neat.telemetry.parity.test.ts +42 -0
- package/test/neat/neat.telemetry.stream.test.ts +19 -0
- package/test/neat/neat.telemetry.test.ts +16 -0
- package/test/neat/neat.test.ts +422 -0
- package/test/neat/neat.utilities.test.ts +44 -0
- package/test/network/__suppress_console.ts +9 -0
- package/test/network/acyclic.topoorder.test.ts +17 -0
- package/test/network/checkpoint.metricshook.test.ts +36 -0
- package/test/network/error.handling.test.ts +581 -0
- package/test/network/evolution.test.ts +285 -0
- package/test/network/genetic.test.ts +208 -0
- package/test/network/learning.capability.test.ts +244 -0
- package/test/network/mutation.effects.test.ts +492 -0
- package/test/network/network.activate.test.ts +115 -0
- package/test/network/network.activateBatch.test.ts +30 -0
- package/test/network/network.deterministic.test.ts +64 -0
- package/test/network/network.evolve.branches.test.ts +75 -0
- package/test/network/network.evolve.multithread.branches.test.ts +83 -0
- package/test/network/network.evolve.test.ts +100 -0
- package/test/network/network.gating.removal.test.ts +93 -0
- package/test/network/network.mutate.additional.test.ts +145 -0
- package/test/network/network.mutate.edgecases.test.ts +101 -0
- package/test/network/network.mutate.test.ts +101 -0
- package/test/network/network.prune.earlyexit.test.ts +38 -0
- package/test/network/network.remove.errors.test.ts +45 -0
- package/test/network/network.slab.fallbacks.test.ts +22 -0
- package/test/network/network.stats.test.ts +45 -0
- package/test/network/network.training.advanced.test.ts +149 -0
- package/test/network/network.training.basic.test.ts +228 -0
- package/test/network/network.training.helpers.test.ts +183 -0
- package/test/network/onnx.export.test.ts +310 -0
- package/test/network/onnx.import.test.ts +129 -0
- package/test/network/pruning.topology.test.ts +282 -0
- package/test/network/regularization.determinism.test.ts +83 -0
- package/test/network/regularization.dropconnect.test.ts +17 -0
- package/test/network/regularization.dropconnect.validation.test.ts +18 -0
- package/test/network/regularization.stochasticdepth.test.ts +27 -0
- package/test/network/regularization.test.ts +843 -0
- package/test/network/regularization.weightnoise.test.ts +30 -0
- package/test/network/setupTests.ts +2 -0
- package/test/network/standalone.test.ts +332 -0
- package/test/network/structure.serialization.test.ts +660 -0
- package/test/training/training.determinism.mixed-precision.test.ts +134 -0
- package/test/training/training.earlystopping.test.ts +91 -0
- package/test/training/training.edge-cases.test.ts +91 -0
- package/test/training/training.extensions.test.ts +47 -0
- package/test/training/training.gradient.features.test.ts +110 -0
- package/test/training/training.gradient.refinements.test.ts +170 -0
- package/test/training/training.gradient.separate-bias.test.ts +41 -0
- package/test/training/training.optimizer.test.ts +48 -0
- package/test/training/training.plateau.smoothing.test.ts +58 -0
- package/test/training/training.smoothing.types.test.ts +174 -0
- package/test/training/training.train.options.coverage.test.ts +52 -0
- package/test/utils/console-helper.ts +76 -0
- package/test/utils/jest-setup.ts +60 -0
- package/test/utils/test-helpers.ts +175 -0
- package/tsconfig.docs.json +12 -0
- package/tsconfig.json +21 -0
- 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
|
+
}
|