@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,220 @@
1
+ /**
2
+ * Lineage / ancestry analysis helpers for NEAT populations.
3
+ *
4
+ * These utilities were migrated from the historical implementation inside `src/neat.ts`
5
+ * to keep core NEAT logic lean while still exposing educational metrics for users who
6
+ * want to introspect evolutionary diversity.
7
+ *
8
+ * Glossary:
9
+ * - Genome: An individual network encoding (has a unique `_id` and optional `_parents`).
10
+ * - Ancestor Window: A shallow breadth‑first window (default depth = 4) over the lineage graph.
11
+ * - Jaccard Distance: 1 - |A ∩ B| / |A ∪ B|, measuring dissimilarity between two sets.
12
+ */
13
+
14
+ /**
15
+ * Minimal shape assumed for a genome inside the NEAT population. Additional properties are
16
+ * intentionally left open (index signature) because user implementations may extend genomes.
17
+ */
18
+ export interface GenomeLike {
19
+ /** Unique numeric identifier assigned when the genome is created. */
20
+ _id: number;
21
+ /** Optional list of parent genome IDs (could be 1 or 2 for sexual reproduction, or more in custom ops). */
22
+ _parents?: number[];
23
+ /** Allow arbitrary extra properties without forcing casts. */
24
+ [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
25
+ }
26
+
27
+ /** Expected `this` context for lineage helpers (a subset of the NEAT instance). */
28
+ export interface NeatLineageContext {
29
+ /** Current evolutionary population (array of genomes). */
30
+ population: GenomeLike[];
31
+ /** RNG provider returning a PRNG function; shape taken from core NEAT implementation. */
32
+ _getRNG: () => () => number;
33
+ }
34
+
35
+ /**
36
+ * Depth window (in breadth-first layers) used when gathering ancestor IDs.
37
+ * A small window keeps the metric inexpensive while still capturing recent lineage diversity.
38
+ *
39
+ * Rationale: Deep full ancestry can grow quickly and become O(N * lineage depth). Empirically,
40
+ * a window of 4 gives a stable signal about short‑term innovation mixing without large cost.
41
+ *
42
+ * You can fork and increase this constant if you need deeper lineage metrics, but note that
43
+ * performance will degrade roughly proportionally to the number of enqueued ancestor nodes.
44
+ *
45
+ * Example (changing the window):
46
+ * // (NOT exported) – modify locally before building docs
47
+ * // const ANCESTOR_DEPTH_WINDOW = 6; // capture deeper history
48
+ */
49
+ const ANCESTOR_DEPTH_WINDOW = 4;
50
+
51
+ /**
52
+ * Build the (shallow) ancestor ID set for a single genome using breadth‑first traversal.
53
+ *
54
+ * Traversal Strategy:
55
+ * 1. Seed queue with the genome's parent IDs (depth = 1).
56
+ * 2. Repeatedly dequeue, record its ID, and enqueue its parents with incremented depth.
57
+ * 3. Stop exploring a branch once the configured depth window is exceeded.
58
+ *
59
+ * This bounded BFS gives a quick, memory‑friendly approximation of a genome's lineage neighborhood
60
+ * that works well for diversity/uniqueness metrics without the expense of full historical graphs.
61
+ *
62
+ * Edge Cases:
63
+ * - Missing or empty `_parents` array ⇒ returns an empty set.
64
+ * - Orphan parent IDs (not found in population) are still added (their ID), but no further expansion occurs.
65
+ *
66
+ * Complexity (worst case): O(B^D) where B is average branching factor of parent links (usually <= 2)
67
+ * and D = ANCESTOR_DEPTH_WINDOW (default 4) – so effectively constant for typical NEAT usage.
68
+ *
69
+ * @param this NEAT / evolutionary context; must provide `population` (array) for ID lookups.
70
+ * @param genome Genome whose shallow ancestor set you want to compute.
71
+ * @returns A Set of numeric ancestor IDs (deduplicated).
72
+ *
73
+ * @example
74
+ * // Assuming `neat` is your NEAT instance and `g` a genome inside `neat.population`:
75
+ * import { buildAnc } from 'neataptic';
76
+ * const ancestorIds = buildAnc.call(neat, g);
77
+ * console.log([...ancestorIds]); // -> e.g. [12, 4, 9]
78
+ */
79
+ export function buildAnc(
80
+ this: NeatLineageContext,
81
+ genome: GenomeLike
82
+ ): Set<number> {
83
+ // Initialize ancestor ID accumulator.
84
+ const ancestorSet = new Set<number>();
85
+
86
+ // Fast exit if the genome has no recorded parents.
87
+ if (!Array.isArray(genome._parents)) return ancestorSet;
88
+
89
+ /**
90
+ * BFS queue entries carrying the ancestor ID, current depth within the window,
91
+ * and a direct reference to the ancestor genome (if located) so we can expand its parents.
92
+ */
93
+ const queue: { id: number; depth: number; genomeRef?: GenomeLike }[] = [];
94
+
95
+ // Seed: enqueue each direct parent at depth = 1.
96
+ for (const parentId of genome._parents) {
97
+ queue.push({
98
+ id: parentId,
99
+ depth: 1,
100
+ genomeRef: this.population.find((gm) => gm._id === parentId),
101
+ });
102
+ }
103
+
104
+ // Breadth‑first expansion within the fixed depth window.
105
+ while (queue.length) {
106
+ // Dequeue (FIFO) to ensure breadth‑first order.
107
+ const current = queue.shift()!;
108
+
109
+ // Skip nodes that exceed the depth window limit.
110
+ if (current.depth > ANCESTOR_DEPTH_WINDOW) continue;
111
+
112
+ // Record ancestor ID (dedup automatically handled by Set semantics).
113
+ if (current.id != null) ancestorSet.add(current.id);
114
+
115
+ // If we have a concrete genome reference with parents, enqueue them for the next layer.
116
+ if (current.genomeRef && Array.isArray(current.genomeRef._parents)) {
117
+ for (const parentId of current.genomeRef._parents) {
118
+ queue.push({
119
+ id: parentId,
120
+ // Depth increases as we move one layer further away from the focal genome.
121
+ depth: current.depth + 1,
122
+ genomeRef: this.population.find((gm) => gm._id === parentId),
123
+ });
124
+ }
125
+ }
126
+ }
127
+ return ancestorSet;
128
+ }
129
+
130
+ /** Maximum number of distinct genome pairs to sample when computing uniqueness. */
131
+ const MAX_UNIQUENESS_SAMPLE_PAIRS = 30;
132
+
133
+ /**
134
+ * Compute an "ancestor uniqueness" diversity metric for the current population.
135
+ *
136
+ * The metric = mean Jaccard distance between shallow ancestor sets of randomly sampled genome pairs.
137
+ * A higher value indicates that individuals trace back to more distinct recent lineages (i.e. less
138
+ * overlap in their ancestor windows), while a lower value indicates convergence toward similar ancestry.
139
+ *
140
+ * Why Jaccard Distance? It is scale‑independent: adding unrelated ancestors to both sets simultaneously
141
+ * does not change the proportion of shared ancestry, and distance stays within [0,1].
142
+ *
143
+ * Sampling Strategy:
144
+ * - Uniformly sample up to N = min(30, populationPairs) distinct unordered pairs (with replacement on pair selection, but indices are adjusted to avoid self‑pairs).
145
+ * - For each pair, construct ancestor sets via `buildAnc` and accumulate their Jaccard distance.
146
+ * - Return the average (rounded to 3 decimal places) or 0 if insufficient samples.
147
+ *
148
+ * Edge Cases:
149
+ * - Population < 2 ⇒ returns 0 (cannot form pairs).
150
+ * - Both ancestor sets empty ⇒ pair skipped (no information about uniqueness).
151
+ *
152
+ * Performance: O(S * W) where S is sampled pair count (≤ 30) and W is bounded ancestor set size
153
+ * (kept small by the depth window). This is intentionally lightweight for per‑generation telemetry.
154
+ *
155
+ * @param this NEAT context (`population` and `_getRNG` must exist).
156
+ * @returns Mean Jaccard distance in [0,1]. Higher ⇒ more lineage uniqueness / diversity.
157
+ *
158
+ * @example
159
+ * import { computeAncestorUniqueness } from 'neataptic';
160
+ * // inside an evolutionary loop, with `neat` as your NEAT instance:
161
+ * const uniqueness = computeAncestorUniqueness.call(neat);
162
+ * console.log('Ancestor uniqueness:', uniqueness); // e.g. 0.742
163
+ */
164
+ export function computeAncestorUniqueness(this: NeatLineageContext): number {
165
+ // Bind builder once for clarity & micro‑efficiency.
166
+ const buildAncestorSet = buildAnc.bind(this);
167
+
168
+ // Accumulators for (distance sum, sampled pair count).
169
+ let sampledPairCount = 0;
170
+ let jaccardDistanceSum = 0;
171
+
172
+ /**
173
+ * Maximum number of pair samples respecting both the cap constant and the total
174
+ * possible distinct unordered pairs nC2 = n(n-1)/2.
175
+ */
176
+ const maxSamplePairs = Math.min(
177
+ MAX_UNIQUENESS_SAMPLE_PAIRS,
178
+ (this.population.length * (this.population.length - 1)) / 2
179
+ );
180
+
181
+ // Main sampling loop.
182
+ for (let t = 0; t < maxSamplePairs; t++) {
183
+ if (this.population.length < 2) break; // not enough genomes to form pairs
184
+
185
+ // Randomly pick first genome index.
186
+ const indexA = Math.floor(this._getRNG()() * this.population.length);
187
+ // Pick second index (avoid identical -> simple offset if collision).
188
+ let indexB = Math.floor(this._getRNG()() * this.population.length);
189
+ if (indexB === indexA) indexB = (indexB + 1) % this.population.length;
190
+
191
+ // Build ancestor sets for the pair.
192
+ const ancestorSetA = buildAncestorSet(this.population[indexA]);
193
+ const ancestorSetB = buildAncestorSet(this.population[indexB]);
194
+
195
+ // Skip if both sets are empty (no lineage info to compare yet).
196
+ if (ancestorSetA.size === 0 && ancestorSetB.size === 0) continue;
197
+
198
+ // Compute intersection size.
199
+ let intersectionCount = 0;
200
+ for (const id of ancestorSetA)
201
+ if (ancestorSetB.has(id)) intersectionCount++;
202
+
203
+ // Union size = |A| + |B| - |A ∩ B| (guard against divide-by-zero).
204
+ const unionSize =
205
+ ancestorSetA.size + ancestorSetB.size - intersectionCount || 1;
206
+
207
+ // Jaccard distance = 1 - similarity.
208
+ const jaccardDistance = 1 - intersectionCount / unionSize;
209
+
210
+ // Accumulate for averaging.
211
+ jaccardDistanceSum += jaccardDistance;
212
+ sampledPairCount++;
213
+ }
214
+
215
+ // Average (3 decimal places) or 0 if no valid samples.
216
+ const ancestorUniqueness = sampledPairCount
217
+ ? +(jaccardDistanceSum / sampledPairCount).toFixed(3)
218
+ : 0;
219
+ return ancestorUniqueness;
220
+ }
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Multi-objective helpers (fast non-dominated sorting + crowding distance).
3
+ * Extracted from `neat.ts` to keep the core class slimmer.
4
+ */
5
+ import type Network from '../architecture/network';
6
+
7
+ /**
8
+ * Shape of an objective descriptor used by the Neat instance.
9
+ * - `accessor` extracts a numeric objective from a genome
10
+ * - `direction` optionally indicates whether the objective is maximized or
11
+ * minimized (defaults to 'max')
12
+ */
13
+ type ObjectiveDescriptor = {
14
+ accessor: (genome: Network) => number;
15
+ direction?: 'max' | 'min';
16
+ };
17
+
18
+ /**
19
+ * Perform fast non-dominated sorting and compute crowding distances for a
20
+ * population of networks (genomes). This implements a standard NSGA-II style
21
+ * non-dominated sorting followed by crowding distance assignment.
22
+ *
23
+ * The function annotates genomes with two fields used elsewhere in the codebase:
24
+ * - `_moRank`: integer Pareto front rank (0 = best/frontier)
25
+ * - `_moCrowd`: numeric crowding distance (higher is better; Infinity for
26
+ * boundary solutions)
27
+ *
28
+ * Example
29
+ * ```ts
30
+ * // inside a Neat class that exposes `_getObjectives()` and `options`
31
+ * const fronts = fastNonDominated.call(neatInstance, population);
32
+ * // fronts[0] is the Pareto-optimal set
33
+ * ```
34
+ *
35
+ * Notes for documentation generation:
36
+ * - Each objective descriptor returned by `_getObjectives()` must have an
37
+ * `accessor(genome: Network): number` function and may include
38
+ * `direction: 'max' | 'min'` to indicate optimization direction.
39
+ * - Accessor failures are guarded and will yield a default value of 0.
40
+ *
41
+ * @param this - Neat instance providing `_getObjectives()`, `options` and
42
+ * `_paretoArchive` fields (function is meant to be invoked using `.call`)
43
+ * @param pop - population array of `Network` genomes to be ranked
44
+ * @returns Array of Pareto fronts; each front is an array of `Network` genomes.
45
+ */
46
+ export function fastNonDominated(this: any, pop: Network[]): Network[][] {
47
+ /**
48
+ * const: objective descriptors array
49
+ * Short description: descriptors returned by the Neat instance that define
50
+ * how to extract objective values from genomes and whether each objective is
51
+ * maximized or minimized.
52
+ *
53
+ * Each descriptor must provide:
54
+ * - `accessor(genome: Network): number` — returns numeric score for genome
55
+ * - `direction?: 'max' | 'min'` — optional optimization direction (default 'max')
56
+ */
57
+ const objectiveDescriptors: ObjectiveDescriptor[] = this._getObjectives();
58
+
59
+ /**
60
+ * const: objective values matrix
61
+ * Short description: precomputed numeric values of each objective for every
62
+ * genome in the population. This avoids repeated accessor calls during
63
+ * pairwise domination checks.
64
+ *
65
+ * Shape: `[population.length][objectives.length]` where row i contains the
66
+ * objective vector for `pop[i]`.
67
+ */
68
+ const valuesMatrix: number[][] = pop.map((genomeItem: Network) =>
69
+ objectiveDescriptors.map((descriptor: any) => {
70
+ try {
71
+ return descriptor.accessor(genomeItem);
72
+ } catch {
73
+ // If an objective accessor fails, treat the value as neutral (0).
74
+ return 0;
75
+ }
76
+ })
77
+ );
78
+
79
+ /**
80
+ * const: dominance predicate
81
+ * Short description: returns true when vector `valuesA` Pareto-dominates
82
+ * vector `valuesB`.
83
+ *
84
+ * Detailed behavior:
85
+ * - For each objective the comparator respects the objective's `direction`.
86
+ * - `a` must be at least as good in all objectives and strictly better in at
87
+ * least one objective to be considered dominating.
88
+ *
89
+ * @param valuesA - objective value vector for candidate A
90
+ * @param valuesB - objective value vector for candidate B
91
+ * @returns boolean whether A dominates B
92
+ *
93
+ * Example:
94
+ * ```ts
95
+ * vectorDominates([1,2], [1,3]) // false when both objectives are 'max'
96
+ * ```
97
+ */
98
+ const vectorDominates = (valuesA: number[], valuesB: number[]) => {
99
+ let strictlyBetter = false;
100
+ // Compare each objective, honoring the objective's optimization direction.
101
+ for (
102
+ let objectiveIndex = 0;
103
+ objectiveIndex < valuesA.length;
104
+ objectiveIndex++
105
+ ) {
106
+ const direction = objectiveDescriptors[objectiveIndex].direction || 'max';
107
+ if (direction === 'max') {
108
+ // For maximization, higher is better.
109
+ if (valuesA[objectiveIndex] < valuesB[objectiveIndex]) return false;
110
+ if (valuesA[objectiveIndex] > valuesB[objectiveIndex])
111
+ strictlyBetter = true;
112
+ } else {
113
+ // For minimization, lower is better.
114
+ if (valuesA[objectiveIndex] > valuesB[objectiveIndex]) return false;
115
+ if (valuesA[objectiveIndex] < valuesB[objectiveIndex])
116
+ strictlyBetter = true;
117
+ }
118
+ }
119
+ return strictlyBetter;
120
+ };
121
+
122
+ /**
123
+ * const: paretoFronts
124
+ * Short description: accumulates discovered Pareto fronts during sorting.
125
+ *
126
+ * Each element is a front (array of `Network`) in ascending rank order.
127
+ */
128
+ const paretoFronts: Network[][] = [];
129
+
130
+ /**
131
+ * const: dominationCounts
132
+ * Short description: for each genome index, the count of other genomes that
133
+ * currently dominate it (used to detect front membership when count reaches 0).
134
+ */
135
+ const dominationCounts: number[] = new Array(pop.length).fill(0);
136
+
137
+ /**
138
+ * const: dominatesIndicesList
139
+ * Short description: adjacency list where index p maps to a list of indices q
140
+ * such that genome p dominates genome q. This accelerates propagation when
141
+ * removing a front.
142
+ */
143
+ const dominatedIndicesByIndex: number[][] = pop.map(() => []);
144
+
145
+ /**
146
+ * const: nonDominatedIndices
147
+ * Short description: temporary buffer containing indices of genomes that are
148
+ * not dominated by any other genome — i.e., members of the first front.
149
+ */
150
+ const firstFrontIndices: number[] = [];
151
+
152
+ // Build domination relationships between every pair of genomes.
153
+ // Step: for each pair (p,q) compute who (if any) dominates who, using the
154
+ // precomputed valuesMatrix to avoid repeated accessor calls.
155
+ for (let pIndex = 0; pIndex < pop.length; pIndex++) {
156
+ for (let qIndex = 0; qIndex < pop.length; qIndex++) {
157
+ if (pIndex === qIndex) continue;
158
+ if (vectorDominates(valuesMatrix[pIndex], valuesMatrix[qIndex]))
159
+ dominatedIndicesByIndex[pIndex].push(qIndex);
160
+ else if (vectorDominates(valuesMatrix[qIndex], valuesMatrix[pIndex]))
161
+ dominationCounts[pIndex]++;
162
+ }
163
+ if (dominationCounts[pIndex] === 0) firstFrontIndices.push(pIndex);
164
+ }
165
+
166
+ // Assign genomes to Pareto fronts using a breadth-like ranking algorithm.
167
+ let currentFrontIndices = firstFrontIndices;
168
+ let currentFrontRank = 0;
169
+ while (currentFrontIndices.length) {
170
+ const nextFrontIndices: number[] = [];
171
+ for (const pIndex of currentFrontIndices) {
172
+ // Annotate genome with its multi-objective rank for downstream use.
173
+ (pop[pIndex] as any)._moRank = currentFrontRank;
174
+ // For every genome q dominated by p, reduce its domination count.
175
+ for (const qIndex of dominatedIndicesByIndex[pIndex]) {
176
+ dominationCounts[qIndex]--;
177
+ if (dominationCounts[qIndex] === 0) nextFrontIndices.push(qIndex);
178
+ }
179
+ }
180
+ // Add the actual genomes (not indices) as a front.
181
+ paretoFronts.push(currentFrontIndices.map((i) => pop[i]));
182
+ currentFrontIndices = nextFrontIndices;
183
+ currentFrontRank++;
184
+ // Safety: prevent pathological runs in degenerate cases.
185
+ if (currentFrontRank > 50) break;
186
+ }
187
+
188
+ // Crowding distance calculation: measures density around solutions in each front.
189
+ for (const front of paretoFronts) {
190
+ if (front.length === 0) continue;
191
+ // Initialize crowding distance for each genome in the front.
192
+ for (const genomeItem of front) (genomeItem as any)._moCrowd = 0;
193
+
194
+ // For each objective, sort the front and accumulate normalized distances.
195
+ for (
196
+ let objectiveIndex = 0;
197
+ objectiveIndex < objectiveDescriptors.length;
198
+ objectiveIndex++
199
+ ) {
200
+ // Sort ascending by the objective value so that boundary solutions
201
+ // (min and max) fall at the ends of the sorted array — this is needed
202
+ // to mark boundary genomes with infinite crowding distance.
203
+ const sortedByCurrentObjective = front
204
+ .slice()
205
+ .sort((genomeA, genomeB) => {
206
+ const valA = objectiveDescriptors[objectiveIndex].accessor(genomeA);
207
+ const valB = objectiveDescriptors[objectiveIndex].accessor(genomeB);
208
+ return valA - valB;
209
+ });
210
+
211
+ // Boundary solutions get infinite crowding so they are always preferred.
212
+ (sortedByCurrentObjective[0] as any)._moCrowd = Infinity;
213
+ (sortedByCurrentObjective[
214
+ sortedByCurrentObjective.length - 1
215
+ ] as any)._moCrowd = Infinity;
216
+
217
+ const minVal = objectiveDescriptors[objectiveIndex].accessor(
218
+ sortedByCurrentObjective[0]
219
+ );
220
+ const maxVal = objectiveDescriptors[objectiveIndex].accessor(
221
+ sortedByCurrentObjective[sortedByCurrentObjective.length - 1]
222
+ );
223
+ // Avoid division by zero when all values are equal.
224
+ const valueRange = maxVal - minVal || 1;
225
+
226
+ // For non-boundary genomes, add normalized distance between neighbors.
227
+ for (
228
+ let sortedIndex = 1;
229
+ sortedIndex < sortedByCurrentObjective.length - 1;
230
+ sortedIndex++
231
+ ) {
232
+ const prevVal = objectiveDescriptors[objectiveIndex].accessor(
233
+ sortedByCurrentObjective[sortedIndex - 1]
234
+ );
235
+ const nextVal = objectiveDescriptors[objectiveIndex].accessor(
236
+ sortedByCurrentObjective[sortedIndex + 1]
237
+ );
238
+ (sortedByCurrentObjective[sortedIndex] as any)._moCrowd +=
239
+ (nextVal - prevVal) / valueRange;
240
+ }
241
+ }
242
+ }
243
+
244
+ // Optionally archive a compact Pareto history for visualization/debugging.
245
+ // The archive stores the current generation and the IDs of genomes in the
246
+ // top N fronts (here we keep up to 3 fronts). This is deliberately compact
247
+ // (IDs only) to keep the archive small for long runs.
248
+ if (this.options.multiObjective?.enabled) {
249
+ this._paretoArchive.push({
250
+ generation: this.generation,
251
+ fronts: paretoFronts.slice(0, 3).map((front) =>
252
+ // map each front (array of Network) to an array of genome IDs
253
+ front.map((genome) => (genome as any)._id)
254
+ ),
255
+ });
256
+ if (this._paretoArchive.length > 100) this._paretoArchive.shift();
257
+ }
258
+
259
+ return paretoFronts;
260
+ }