@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,460 @@
1
+ /**
2
+ * Assign genomes into species based on compatibility distance and maintain species structures.
3
+ * This function creates new species for unassigned genomes, prunes empty species, updates
4
+ * dynamic compatibility threshold controllers, performs optional auto coefficient tuning, and
5
+ * records per‑species history statistics used by telemetry and adaptive controllers.
6
+ *
7
+ * Implementation notes:
8
+ * - Uses existing representatives; any unassigned genome that doesn't fit an existing species
9
+ * creates a new species with itself as representative.
10
+ * - Representatives are refreshed each generation (first member heuristic) to reduce drift cost.
11
+ * - Includes optional age penalty for very old species to gently reduce their reproductive share.
12
+ * - PID‑style controller adjusts the global compatibility threshold toward `targetSpecies`.
13
+ * - Auto compatibility coefficient tuning slightly nudges excess/disjoint coefficients to influence
14
+ * clustering granularity when enabled.
15
+ * - Extended history snapshot captures structural and innovation statistics for richer telemetry.
16
+ */
17
+ /**
18
+ * Partition the current population into species using compatibility distance.
19
+ *
20
+ * This function is responsible for assigning genomes into species based on the
21
+ * configured compatibility threshold and maintaining per-species bookkeeping.
22
+ * It also optionally adjusts the global compatibility threshold (PID-like controller),
23
+ * applies an automatic tuning of compatibility coefficients, and records history
24
+ * snapshots used by telemetry and adaptive controllers.
25
+ *
26
+ * Example:
27
+ * const population = ...; // created genomes
28
+ * neat._speciate();
29
+ * // now neat._species contains species with assigned members and representatives
30
+ *
31
+ * Notes for documentation:
32
+ * - This method mutates `this._species`, `this.options.compatibilityThreshold`, and
33
+ * `this._speciesHistory` as part of each generation's bookkeeping.
34
+ * - It is intentionally conservative: empty species are pruned and representatives are
35
+ * refreshed to the first member each generation to reduce drift in representative choice.
36
+ *
37
+ * @this any Neataptic-like instance with population, options and bookkeeping maps
38
+ */
39
+ export function _speciate(this: any) {
40
+ // Step 1: Preserve previous membership for turnover calculations
41
+ this._prevSpeciesMembers.clear();
42
+ for (const species of this._species) {
43
+ /**
44
+ * prevMemberSet - set of numeric member ids for quick lookup of previous members.
45
+ * Used to compute the turnover rate (fraction of new members since last generation).
46
+ */
47
+ const prevMemberSet = new Set<number>();
48
+ for (const member of species.members)
49
+ prevMemberSet.add((member as any)._id);
50
+ this._prevSpeciesMembers.set(species.id, prevMemberSet);
51
+ }
52
+
53
+ // Step 2: Clear current members to allow reassignment from scratch
54
+ this._species.forEach((species: any) => (species.members = []));
55
+
56
+ // Step 3: Assignment loop - try to place each genome into an existing species,
57
+ // otherwise create a new species with the genome as representative.
58
+ for (const genome of this.population) {
59
+ /**
60
+ * assignedToExisting - whether the genome was placed into an existing species.
61
+ * This flag guards creation of a new species when false.
62
+ */
63
+ let assignedToExisting = false;
64
+ for (const species of this._species) {
65
+ /**
66
+ * compatDist - numeric compatibility distance between the candidate genome
67
+ * and the species representative. Smaller values indicate greater similarity.
68
+ */
69
+ const compatDist = this._compatibilityDistance(
70
+ genome,
71
+ species.representative
72
+ );
73
+ // method step: if distance below threshold, assign to this species
74
+ if (compatDist < (this.options.compatibilityThreshold || 3)) {
75
+ species.members.push(genome);
76
+ assignedToExisting = true;
77
+ break;
78
+ }
79
+ }
80
+ if (!assignedToExisting) {
81
+ /**
82
+ * speciesId - unique id assigned to a newly created species.
83
+ */
84
+ const speciesId = this._nextSpeciesId++;
85
+ this._species.push({
86
+ id: speciesId,
87
+ members: [genome],
88
+ representative: genome,
89
+ lastImproved: this.generation,
90
+ bestScore: genome.score || -Infinity,
91
+ });
92
+ this._speciesCreated.set(speciesId, this.generation);
93
+ }
94
+ }
95
+
96
+ // Step 4: Remove any empty species (defensive - usually not needed)
97
+ this._species = this._species.filter(
98
+ (species: any) => species.members.length > 0
99
+ );
100
+
101
+ // Step 5: Refresh representatives (choose the first member as lightweight heuristic)
102
+ this._species.forEach((species: any) => {
103
+ // method step: refresh representative to the first member of the species
104
+ species.representative = species.members[0];
105
+ });
106
+
107
+ // Step 6: Soft age penalty - gradually reduce fitness for very old species to
108
+ // encourage turnover and prevent lock-in of stale lineages.
109
+ /**
110
+ * ageProtection - configuration controlling grace period and penalty factor.
111
+ * Applied to species older than (grace * 10) generations (heuristic).
112
+ */
113
+ const ageProtection = this.options.speciesAgeProtection || {
114
+ grace: 3,
115
+ oldPenalty: 0.5,
116
+ };
117
+ for (const species of this._species) {
118
+ const createdGen = this._speciesCreated.get(species.id) ?? this.generation;
119
+ const speciesAge = this.generation - createdGen;
120
+ // method step: apply penalty only when age exceeds a threshold (grace * 10)
121
+ if (speciesAge >= (ageProtection.grace ?? 3) * 10) {
122
+ /** penalty - multiplicative fitness penalty applied to members of very old species */
123
+ const penalty = ageProtection.oldPenalty ?? 0.5;
124
+ if (penalty < 1)
125
+ species.members.forEach((member: any) => {
126
+ if (typeof member.score === 'number') member.score *= penalty;
127
+ });
128
+ }
129
+ }
130
+
131
+ // Step 7: Dynamic compatibility threshold controller (PID-like) to steer
132
+ // the number of species toward `targetSpecies` when speciation controller enabled.
133
+ if (this.options.speciation && (this.options.targetSpecies || 0) > 0) {
134
+ /**
135
+ * targetSpeciesCount - the desired number of species set in options.
136
+ */
137
+ const targetSpeciesCount = this.options.targetSpecies!;
138
+ /** observedSpeciesCount - the current number of species observed */
139
+ const observedSpeciesCount = this._species.length;
140
+ /** adjustConfig - PID-like controller configuration from options.compatAdjust */
141
+ const adjustConfig = this.options.compatAdjust!;
142
+ /** smoothingWindow - window size used to compute exponential moving average */
143
+ const smoothingWindow = Math.max(1, adjustConfig.smoothingWindow || 1);
144
+ /** alpha - smoothing coefficient used by the exponential moving average */
145
+ const alpha = 2 / (smoothingWindow + 1);
146
+ this._compatSpeciesEMA =
147
+ this._compatSpeciesEMA === undefined
148
+ ? observedSpeciesCount
149
+ : this._compatSpeciesEMA +
150
+ alpha * (observedSpeciesCount - this._compatSpeciesEMA);
151
+ /** smoothedSpecies - EMA-smoothed observed species count */
152
+ const smoothedSpecies = this._compatSpeciesEMA;
153
+ // error: positive => we want more species => decrease threshold (make clustering harder)
154
+ /** speciesError - difference between desired and smoothed observed species count */
155
+ const speciesError = targetSpeciesCount - smoothedSpecies;
156
+ this._compatIntegral =
157
+ this._compatIntegral * (adjustConfig.decay || 0.95) + speciesError;
158
+ /** delta - PID-like correction term computed from kp/ki and the integrated error */
159
+ const delta =
160
+ (adjustConfig.kp || 0) * speciesError +
161
+ (adjustConfig.ki || 0) * this._compatIntegral;
162
+ /** newThreshold - tentative updated compatibility threshold before clipping */
163
+ let newThreshold = (this.options.compatibilityThreshold || 3) - delta;
164
+ /** minThreshold - lower bound for adjusted compatibility threshold */
165
+ const minThreshold = adjustConfig.minThreshold || 0.5;
166
+ /** maxThreshold - upper bound for adjusted compatibility threshold */
167
+ const maxThreshold = adjustConfig.maxThreshold || 10;
168
+ if (newThreshold < minThreshold) {
169
+ newThreshold = minThreshold;
170
+ this._compatIntegral = 0;
171
+ }
172
+ if (newThreshold > maxThreshold) {
173
+ newThreshold = maxThreshold;
174
+ this._compatIntegral = 0;
175
+ }
176
+ this.options.compatibilityThreshold = newThreshold;
177
+ }
178
+
179
+ // Step 8: Auto compatibility coefficient tuning - gently nudge excess/disjoint
180
+ // coefficients to influence clustering granularity when enabled.
181
+ if (this.options.autoCompatTuning?.enabled) {
182
+ /**
183
+ * autoTarget - desired species target for auto tuning, falls back to sqrt(pop).
184
+ * Helps the controller infer a reasonable clustering target when none is provided.
185
+ */
186
+ const autoTarget =
187
+ this.options.autoCompatTuning.target ??
188
+ this.options.targetSpecies ??
189
+ Math.max(2, Math.round(Math.sqrt(this.population.length)));
190
+ /** observedForTuning - number of species observed for tuning calculations */
191
+ const observedForTuning = this._species.length || 1;
192
+ /** tuningError - positive means we want more species -> reduce coefficients */
193
+ const tuningError = autoTarget - observedForTuning;
194
+ /** adjustRate - step rate used to scale coefficient changes */
195
+ const adjustRate = this.options.autoCompatTuning.adjustRate ?? 0.01;
196
+ /** minCoeff - lower bound for tuned coefficients */
197
+ const minCoeff = this.options.autoCompatTuning.minCoeff ?? 0.1;
198
+ /** maxCoeff - upper bound for tuned coefficients */
199
+ const maxCoeff = this.options.autoCompatTuning.maxCoeff ?? 5.0;
200
+ /** factor - multiplicative factor derived from adjustRate and tuning error sign */
201
+ const factor = 1 - adjustRate * Math.sign(tuningError);
202
+ let effectiveFactor = factor;
203
+ if (tuningError === 0) {
204
+ // mild jitter to avoid stagnation when already at target (helps certain tests)
205
+ effectiveFactor = 1 + (this._getRNG()() - 0.5) * adjustRate * 0.5;
206
+ }
207
+ this.options.excessCoeff = Math.min(
208
+ maxCoeff,
209
+ Math.max(minCoeff, this.options.excessCoeff! * effectiveFactor)
210
+ );
211
+ this.options.disjointCoeff = Math.min(
212
+ maxCoeff,
213
+ Math.max(minCoeff, this.options.disjointCoeff! * effectiveFactor)
214
+ );
215
+ }
216
+
217
+ // Step 9: Extended history snapshot (rich metrics) or minimal snapshot for telemetry.
218
+ if (this.options.speciesAllocation?.extendedHistory) {
219
+ const stats = this._species.map((species: any) => {
220
+ // Build per-member structural summaries used by aggregated stats below
221
+ /** sizes - per-member compact structural summary used for aggregation */
222
+ const sizes = species.members.map((member: any) => ({
223
+ nodes: member.nodes.length,
224
+ conns: member.connections.length,
225
+ score: member.score || 0,
226
+ nov: (member as any)._novelty || 0,
227
+ ent: this._structuralEntropy(member),
228
+ }));
229
+ /** avg - helper to compute arithmetic mean of numeric arrays */
230
+ const avg = (arr: number[]) =>
231
+ arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
232
+ // Pairwise compatibility sampling (bounded to first 10 members for cost control)
233
+ /** compatSum - cumulative sum of sampled pairwise compatibility distances */
234
+ let compatSum = 0;
235
+ /** compatCount - number of pairwise comparisons included in compatSum */
236
+ let compatCount = 0;
237
+ for (let i = 0; i < species.members.length && i < 10; i++)
238
+ for (let j = i + 1; j < species.members.length && j < 10; j++) {
239
+ compatSum += this._compatibilityDistance(
240
+ species.members[i],
241
+ species.members[j]
242
+ );
243
+ compatCount++;
244
+ }
245
+ /** meanCompat - average pairwise compatibility sampled above */
246
+ const meanCompat = compatCount ? compatSum / compatCount : 0;
247
+ /** last - previously recorded summary stats for this species (if any) */
248
+ const last = this._speciesLastStats.get(species.id);
249
+ /** meanNodes - average number of nodes across sampled members */
250
+ const meanNodes = avg(sizes.map((s: any) => s.nodes));
251
+ /** meanConns - average number of connections across sampled members */
252
+ const meanConns = avg(sizes.map((s: any) => s.conns));
253
+ /** deltaMeanNodes - change in mean node count compared to last snapshot */
254
+ const deltaMeanNodes = last ? meanNodes - last.meanNodes : 0;
255
+ /** deltaMeanConns - change in mean connection count compared to last snapshot */
256
+ const deltaMeanConns = last ? meanConns - last.meanConns : 0;
257
+ /** deltaBestScore - improvement of best score compared to last snapshot */
258
+ const deltaBestScore = last ? species.bestScore - last.best : 0;
259
+ /** createdGen - generation when the species was first created (fallback current gen) */
260
+ const createdGen =
261
+ this._speciesCreated.get(species.id) ?? this.generation;
262
+ /** speciesAge - number of generations since species creation */
263
+ const speciesAge = this.generation - createdGen;
264
+ // Turnover rate: fraction of members that are new relative to previous generation
265
+ /** turnoverRate - fraction of members that are new relative to previous generation */
266
+ let turnoverRate = 0;
267
+ /** prevSet - cached Set of previous member ids for this species */
268
+ const prevSet = this._prevSpeciesMembers.get(species.id);
269
+ if (prevSet && species.members.length) {
270
+ /** newCount - number of members not present in prevSet */
271
+ let newCount = 0;
272
+ for (const member of species.members)
273
+ if (!prevSet.has((member as any)._id)) newCount++;
274
+ turnoverRate = newCount / species.members.length;
275
+ }
276
+ // Variance helper
277
+ /** varCalc - helper to compute variance of numeric arrays */
278
+ const varCalc = (arr: number[]) => {
279
+ if (!arr.length) return 0;
280
+ const mean = avg(arr);
281
+ return avg(arr.map((v) => (v - mean) * (v - mean)));
282
+ };
283
+ /** varNodes - variance of node counts across sampled members */
284
+ const varNodes = varCalc(sizes.map((s: any) => s.nodes));
285
+ /** varConns - variance of connection counts across sampled members */
286
+ const varConns = varCalc(sizes.map((s: any) => s.conns));
287
+ // Innovation statistics across connections in the species
288
+ /** innovSum - cumulative innovation ids sum (for mean) */
289
+ let innovSum = 0;
290
+ /** innovCount - number of connection innovations observed */
291
+ let innovCount = 0;
292
+ /** maxInnov - maximum innovation id observed */
293
+ let maxInnov = -Infinity;
294
+ /** minInnov - minimum innovation id observed */
295
+ let minInnov = Infinity;
296
+ /** enabled - number of enabled connections */
297
+ let enabled = 0;
298
+ /** disabled - number of disabled connections */
299
+ let disabled = 0;
300
+ for (const member of species.members)
301
+ for (const conn of member.connections) {
302
+ const innov = (conn as any).innovation ?? this._fallbackInnov(conn);
303
+ innovSum += innov;
304
+ innovCount++;
305
+ if (innov > maxInnov) maxInnov = innov;
306
+ if (innov < minInnov) minInnov = innov;
307
+ if ((conn as any).enabled === false) disabled++;
308
+ else enabled++;
309
+ }
310
+ /** meanInnovation - mean innovation id across sampled connections */
311
+ const meanInnovation = innovCount ? innovSum / innovCount : 0;
312
+ /** innovationRange - span between max and min innovation ids */
313
+ const innovationRange =
314
+ isFinite(maxInnov) && isFinite(minInnov) && maxInnov > minInnov
315
+ ? maxInnov - minInnov
316
+ : 0;
317
+ /** enabledRatio - fraction of connections that are enabled */
318
+ const enabledRatio =
319
+ enabled + disabled > 0 ? enabled / (enabled + disabled) : 0;
320
+ return {
321
+ id: species.id,
322
+ size: species.members.length,
323
+ best: species.bestScore,
324
+ lastImproved: species.lastImproved,
325
+ age: speciesAge,
326
+ meanNodes,
327
+ meanConns,
328
+ meanScore: avg(sizes.map((s: any) => s.score)),
329
+ meanNovelty: avg(sizes.map((s: any) => s.nov)),
330
+ meanCompat,
331
+ meanEntropy: avg(sizes.map((s: any) => s.ent)),
332
+ varNodes,
333
+ varConns,
334
+ deltaMeanNodes,
335
+ deltaMeanConns,
336
+ deltaBestScore,
337
+ turnoverRate,
338
+ meanInnovation,
339
+ innovationRange,
340
+ enabledRatio,
341
+ };
342
+ });
343
+ for (const st of stats)
344
+ this._speciesLastStats.set(st.id, {
345
+ meanNodes: st.meanNodes,
346
+ meanConns: st.meanConns,
347
+ best: st.best,
348
+ });
349
+ this._speciesHistory.push({ generation: this.generation, stats });
350
+ } else {
351
+ // Minimal snapshot: only store the essentials to reduce memory
352
+ this._speciesHistory.push({
353
+ generation: this.generation,
354
+ stats: this._species.map((species: any) => ({
355
+ id: species.id,
356
+ size: species.members.length,
357
+ best: species.bestScore,
358
+ lastImproved: species.lastImproved,
359
+ })),
360
+ });
361
+ }
362
+ // Step 10: Trim history length to cap memory usage (simple FIFO)
363
+ if (this._speciesHistory.length > 200) this._speciesHistory.shift();
364
+ }
365
+ /**
366
+ * Apply fitness sharing within each species.
367
+ *
368
+ * Fitness sharing reduces the effective fitness of genomes that are clustered
369
+ * tightly together (close compatibility distance), promoting diversity by
370
+ * penalizing dense species. Two modes are supported:
371
+ * - Kernel sharing with bandwidth `sharingSigma` (quadratic kernel)
372
+ * - Equal sharing based on species size when `sharingSigma` is 0
373
+ *
374
+ * Example:
375
+ * neat.options.sharingSigma = 3;
376
+ * neat._applyFitnessSharing();
377
+ *
378
+ * @this any Neataptic-like instance with _species and options
379
+ */
380
+ export function _applyFitnessSharing(this: any) {
381
+ /** const sharingSigma - kernel bandwidth controlling neighbor influence */
382
+ const sharingSigma = this.options.sharingSigma || 0;
383
+ if (sharingSigma > 0) {
384
+ // method step: apply kernel-based sharing inside each species
385
+ this._species.forEach((species: any) => {
386
+ const members = species.members;
387
+ for (let i = 0; i < members.length; i++) {
388
+ const memberI = members[i];
389
+ if (typeof memberI.score !== 'number') continue;
390
+ /** shareSum - accumulates kernel values from neighbors used to divide fitness */
391
+ let shareSum = 0;
392
+ for (let j = 0; j < members.length; j++) {
393
+ const memberJ = members[j];
394
+ /** dist - compatibility distance between two members used by the kernel */
395
+ const dist =
396
+ i === j ? 0 : this._compatibilityDistance(memberI, memberJ);
397
+ if (dist < sharingSigma) {
398
+ /** ratio - normalized distance (0..1) relative to sharingSigma bandwidth */
399
+ const ratio = dist / sharingSigma;
400
+ // quadratic kernel: stronger penalty for closer neighbors
401
+ shareSum += 1 - ratio * ratio;
402
+ }
403
+ }
404
+ if (shareSum <= 0) shareSum = 1; // safety to avoid division by zero
405
+ memberI.score = memberI.score / shareSum;
406
+ }
407
+ });
408
+ } else {
409
+ // method step: equal sharing across species members (simple average)
410
+ this._species.forEach((species: any) => {
411
+ /** size - current number of members in the species (used for equal sharing) */
412
+ const size = species.members.length;
413
+ species.members.forEach((member: any) => {
414
+ if (typeof member.score === 'number')
415
+ member.score = member.score / size;
416
+ });
417
+ });
418
+ }
419
+ }
420
+ /**
421
+ * Sort members of a species in descending order by score.
422
+ *
423
+ * Simple utility used by stagnation checks and selection routines to ensure
424
+ * the top-performing genomes are at index 0.
425
+ *
426
+ * @param sp species-like object with a `members` array and member `.score`
427
+ */
428
+ export function _sortSpeciesMembers(this: any, sp: any) {
429
+ // method step: sort in place from highest to lowest score
430
+ sp.members.sort((a: any, b: any) => (b.score || 0) - (a.score || 0));
431
+ }
432
+ /**
433
+ * Update species stagnation statistics and prune species that have not
434
+ * improved within the configured stagnation window.
435
+ *
436
+ * This updates each species' `bestScore` and `lastImproved` fields and then
437
+ * removes species whose age since last improvement exceeds `stagnationGenerations`.
438
+ *
439
+ * @this any Neataptic-like instance with _species and options
440
+ */
441
+ export function _updateSpeciesStagnation(this: any) {
442
+ /** stagnationWindow - number of generations allowed without improvement */
443
+ const stagnationWindow = this.options.stagnationGenerations || 15;
444
+ // method step: refresh member ordering and update per-species bests
445
+ this._species.forEach((species: any) => {
446
+ this._sortSpeciesMembers(species);
447
+ /** top - highest scoring member after sorting (index 0) */
448
+ const top = species.members[0];
449
+ if ((top.score || -Infinity) > species.bestScore) {
450
+ species.bestScore = top.score || -Infinity;
451
+ species.lastImproved = this.generation;
452
+ }
453
+ });
454
+ // method step: keep only species that have improved recently
455
+ /** survivors - species that remain because they have improved within the window */
456
+ const survivors = this._species.filter(
457
+ (species: any) => this.generation - species.lastImproved <= stagnationWindow
458
+ );
459
+ if (survivors.length) this._species = survivors;
460
+ }
@@ -0,0 +1,151 @@
1
+ import { NeatLike, SpeciesHistoryEntry } from './neat.types';
2
+
3
+ /**
4
+ * Get lightweight per-species statistics for the current population.
5
+ *
6
+ * This method intentionally returns a small, immutable-friendly summary per
7
+ * species rather than exposing internal member lists. This avoids accidental
8
+ * mutation of the library's internal state while still providing useful
9
+ * telemetry for UIs, dashboards, or logging.
10
+ *
11
+ * Example:
12
+ * ```ts
13
+ * const stats = neat.getSpeciesStats();
14
+ * // stats => [{ id: 1, size: 12, bestScore: 0.85, lastImproved: 42 }, ...]
15
+ * ```
16
+ *
17
+ * Success criteria:
18
+ * - Returns an array of objects each containing `id`, `size`, `bestScore`,
19
+ * and `lastImproved`.
20
+ * - Does not expose or return references to internal member arrays.
21
+ *
22
+ * @returns Array of per-species summaries suitable for reporting.
23
+ */
24
+ export function getSpeciesStats(
25
+ this: NeatLike
26
+ ): { id: number; size: number; bestScore: number; lastImproved: number }[] {
27
+ // `speciesArray` is a reference to the internal species registry. We map
28
+ // to a minimal representation to avoid exposing the full objects.
29
+ /** const JSDoc short descriptions above each constant */
30
+ /**
31
+ * Array of species stored internally on the Neat instance.
32
+ * This value is intentionally not documented in the public API; we only
33
+ * expose the derived summary below.
34
+ */
35
+ const speciesArray = (this as any)._species as any[];
36
+
37
+ // Map internal species to compact summaries.
38
+ return speciesArray.map((species: any) => ({
39
+ id: species.id,
40
+ size: species.members.length,
41
+ bestScore: species.bestScore,
42
+ lastImproved: species.lastImproved,
43
+ }));
44
+ }
45
+
46
+ /**
47
+ * Retrieve the recorded species history across generations.
48
+ *
49
+ * Each entry in the returned array corresponds to a recorded generation and
50
+ * contains a snapshot of statistics for every species at that generation.
51
+ * This is useful for plotting species sizes over time, tracking innovation
52
+ * spread, or implementing population-level diagnostics.
53
+ *
54
+ * The shape of each entry is defined by `SpeciesHistoryEntry` in the public
55
+ * types. When `options.speciesAllocation.extendedHistory` is enabled the
56
+ * library attempts to include additional metrics such as `innovationRange`
57
+ * and `enabledRatio`. When those extended metrics are missing they are
58
+ * computed lazily from a representative genome to ensure historical data is
59
+ * still useful for analysis.
60
+ *
61
+ * Example:
62
+ * ```ts
63
+ * const history = neat.getSpeciesHistory();
64
+ * // history => [{ generation: 0, stats: [{ id:1, size:10, innovationRange:5, enabledRatio:0.9 }, ...] }, ...]
65
+ * ```
66
+ *
67
+ * Notes for documentation:
68
+ * - The function tries to avoid heavy computation. Extended metrics are
69
+ * computed only when explicitly requested via options.
70
+ * - Computed extended metrics are conservative fallbacks; they use the
71
+ * available member connections and a fallback innovation extractor when
72
+ * connection innovation IDs are not present.
73
+ *
74
+ * @returns Array of generation-stamped species statistic snapshots.
75
+ */
76
+ export function getSpeciesHistory(this: NeatLike): SpeciesHistoryEntry[] {
77
+ /** const JSDoc short descriptions above each constant */
78
+ /**
79
+ * The raw species history array captured on the Neat instance. Each element
80
+ * is a snapshot for a generation and includes a `stats` array of per-species
81
+ * summaries.
82
+ */
83
+ const speciesHistory = (this as any)._speciesHistory as SpeciesHistoryEntry[];
84
+
85
+ // If the user enabled extended history, ensure extended fields exist by
86
+ // backfilling inexpensive fallbacks where possible.
87
+ if (this.options?.speciesAllocation?.extendedHistory) {
88
+ // Iterate over each generation snapshot
89
+ for (const generationEntry of speciesHistory) {
90
+ // Iterate over each per-species stat in the snapshot
91
+ for (const speciesStat of generationEntry.stats as any[]) {
92
+ // If extended fields already present, skip computation
93
+ if ('innovationRange' in speciesStat && 'enabledRatio' in speciesStat)
94
+ continue;
95
+
96
+ // Find a representative species object in the current population by id
97
+ // `speciesObj` is used to compute fallbacks when needed.
98
+ const speciesObj = (this as any)._species.find(
99
+ (s: any) => s.id === speciesStat.id
100
+ );
101
+
102
+ // If we have members, compute cheap fallbacks for innovationRange and enabledRatio
103
+ if (speciesObj && speciesObj.members && speciesObj.members.length) {
104
+ // Initialize tracking variables for the innovation id range and enabled/disabled counts
105
+ let maxInnovation = -Infinity;
106
+ let minInnovation = Infinity;
107
+ let enabledCount = 0;
108
+ let disabledCount = 0;
109
+
110
+ // For each member genome in the species
111
+ for (const member of speciesObj.members) {
112
+ // For each connection in the genome, attempt to read an innovation id
113
+ for (const connection of member.connections) {
114
+ // Prefer an explicit `innovation` property; otherwise call internal
115
+ // fallback innov extractor (if available) and finally default to 0.
116
+ const innovationId =
117
+ (connection as any).innovation ??
118
+ (this as any)._fallbackInnov?.(connection) ??
119
+ 0;
120
+
121
+ // Update min/max innovation trackers
122
+ if (innovationId > maxInnovation) maxInnovation = innovationId;
123
+ if (innovationId < minInnovation) minInnovation = innovationId;
124
+
125
+ // Count enabled vs disabled connections (treat undefined as enabled)
126
+ if ((connection as any).enabled === false) disabledCount++;
127
+ else enabledCount++;
128
+ }
129
+ }
130
+
131
+ // Compute innovationRange: positive difference when valid, otherwise 0
132
+ (speciesStat as any).innovationRange =
133
+ isFinite(maxInnovation) &&
134
+ isFinite(minInnovation) &&
135
+ maxInnovation > minInnovation
136
+ ? maxInnovation - minInnovation
137
+ : 0;
138
+
139
+ // Compute enabledRatio: fraction of enabled connections when any exist
140
+ (speciesStat as any).enabledRatio =
141
+ enabledCount + disabledCount
142
+ ? enabledCount / (enabledCount + disabledCount)
143
+ : 0;
144
+ }
145
+ }
146
+ }
147
+ }
148
+
149
+ // Return the possibly-augmented history. Consumers should treat this as read-only.
150
+ return speciesHistory;
151
+ }