@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,933 @@
1
+ // Telemetry stream and recording helpers
2
+
3
+ import { NeatLike, TelemetryEntry } from './neat.types';
4
+ import { EPSILON } from './neat.constants';
5
+
6
+ /**
7
+ * Apply a telemetry selection whitelist to a telemetry entry.
8
+ *
9
+ * This helper inspects a per-instance Set of telemetry keys stored at
10
+ * `this._telemetrySelect`. If present, only keys included in the set are
11
+ * retained on the produced entry. Core fields (generation, best score and
12
+ * species count) are always preserved.
13
+ *
14
+ * Example:
15
+ * @example
16
+ * // keep only 'gen', 'best', 'species' and 'diversity' fields
17
+ * neat._telemetrySelect = new Set(['diversity']);
18
+ * applyTelemetrySelect.call(neat, entry);
19
+ *
20
+ * @param entry - Raw telemetry object to be filtered in-place.
21
+ * @returns The filtered telemetry object (same reference as input).
22
+ */
23
+ export function applyTelemetrySelect(this: NeatLike, entry: any): any {
24
+ // fast-path: nothing to do when no selection set is configured
25
+ if (!(this as any)._telemetrySelect || !(this as any)._telemetrySelect.size)
26
+ return entry;
27
+
28
+ /**
29
+ * Set of telemetry keys explicitly selected by the user for reporting.
30
+ * Only properties whose keys are present in this set will be retained on the
31
+ * telemetry entry (besides core fields which are always preserved).
32
+ */
33
+ /** Set of telemetry keys the user has chosen to keep when reporting. */
34
+ const keep = (this as any)._telemetrySelect as Set<string>;
35
+
36
+ /**
37
+ * Core telemetry fields that are always preserved regardless of the
38
+ * selection set to guarantee downstream consumers receive the minimal
39
+ * structured snapshot required for charts and logs.
40
+ */
41
+ /** Core telemetry fields always preserved: gen, best, species. */
42
+ const core = { gen: entry.gen, best: entry.best, species: entry.species };
43
+
44
+ // Iterate over entry keys and delete any non-core keys not in the keep set.
45
+ for (const key of Object.keys(entry)) {
46
+ // preserve core fields always
47
+ if (key in core) continue;
48
+ if (!keep.has(key)) delete entry[key];
49
+ }
50
+
51
+ // Re-attach the core fields (ensures ordering and presence)
52
+ return Object.assign(entry, core);
53
+ }
54
+
55
+ /**
56
+ * Lightweight proxy for structural entropy based on degree-distribution.
57
+ *
58
+ * This function computes an approximate entropy of a graph topology by
59
+ * counting node degrees and computing the entropy of the degree histogram.
60
+ * The result is cached on the graph object for the current generation in
61
+ * `_entropyVal` to avoid repeated expensive recomputation.
62
+ *
63
+ * Example:
64
+ * @example
65
+ * const H = structuralEntropy.call(neat, genome);
66
+ * console.log(`Structure entropy: ${H.toFixed(3)}`);
67
+ *
68
+ * @param graph - A genome-like object with `nodes` and `connections` arrays.
69
+ * @returns A non-negative number approximating structural entropy.
70
+ */
71
+ export function structuralEntropy(this: NeatLike, graph: any): number {
72
+ const anyG = graph as any;
73
+
74
+ // Return cached value when available and valid for current generation
75
+ if (
76
+ anyG._entropyGen === (this as any).generation &&
77
+ typeof anyG._entropyVal === 'number'
78
+ )
79
+ return anyG._entropyVal;
80
+
81
+ /**
82
+ * Mapping from each node's unique gene identifier to the degree (number of
83
+ * incident enabled connections). Initialized to 0 for every node prior to
84
+ * accumulation of connection endpoints.
85
+ */
86
+ /** Map from node geneId to degree (enabled incident connections). */
87
+ const degreeCounts: Record<number, number> = {};
88
+
89
+ // Initialize degree counts for every node in the graph
90
+ for (const node of graph.nodes) degreeCounts[(node as any).geneId] = 0;
91
+
92
+ // Accumulate degrees from enabled connections
93
+ for (const conn of graph.connections)
94
+ if (conn.enabled) {
95
+ const fromId = (conn.from as any).geneId;
96
+ const toId = (conn.to as any).geneId;
97
+ if (degreeCounts[fromId] !== undefined) degreeCounts[fromId]++;
98
+ if (degreeCounts[toId] !== undefined) degreeCounts[toId]++;
99
+ }
100
+
101
+ /**
102
+ * Histogram where each key is an observed degree and each value is the
103
+ * number of nodes exhibiting that degree within the current genome.
104
+ */
105
+ /** Histogram mapping degree -> frequency of nodes with that degree. */
106
+ const degreeHistogram: Record<number, number> = {};
107
+
108
+ /**
109
+ * Number of nodes (cardinality of degreeCounts) used to normalize degree
110
+ * frequencies into probabilities. Defaults to 1 to avoid divide-by-zero.
111
+ */
112
+ /** Number of nodes in the graph (falls back to 1). */
113
+ const nodeCount = graph.nodes.length || 1;
114
+
115
+ // Build histogram of degree frequencies
116
+ for (const nodeId in degreeCounts) {
117
+ const d = degreeCounts[nodeId as any];
118
+ degreeHistogram[d] = (degreeHistogram[d] || 0) + 1;
119
+ }
120
+
121
+ // Compute entropy H = -sum p * log(p)
122
+ let entropy = 0;
123
+ for (const k in degreeHistogram) {
124
+ const p = degreeHistogram[k as any] / nodeCount;
125
+ if (p > 0) entropy -= p * Math.log(p + EPSILON);
126
+ }
127
+
128
+ // Cache result on the graph object for the current generation
129
+ anyG._entropyGen = (this as any).generation;
130
+ anyG._entropyVal = entropy;
131
+ return entropy;
132
+ }
133
+
134
+ /**
135
+ * Compute several diversity statistics used by telemetry reporting.
136
+ *
137
+ * This helper is intentionally conservative in runtime: when `fastMode` is
138
+ * enabled it will automatically tune a few sampling defaults to keep the
139
+ * computation cheap. The computed statistics are written to
140
+ * `this._diversityStats` as an object with keys like `meanCompat` and
141
+ * `graphletEntropy`.
142
+ *
143
+ * The method mutates instance-level temporary fields and reads a number of
144
+ * runtime options from `this.options`.
145
+ *
146
+ * @remarks
147
+ * - Uses random sampling of pairs and 3-node subgraphs (graphlets) to
148
+ * approximate diversity metrics.
149
+ *
150
+ * Example:
151
+ * @example
152
+ * // compute and store diversity stats onto the neat instance
153
+ * neat.options.diversityMetrics = { enabled: true };
154
+ * neat.computeDiversityStats();
155
+ * console.log(neat._diversityStats.meanCompat);
156
+ */
157
+ export function computeDiversityStats(this: NeatLike) {
158
+ // Ensure the feature is enabled in options
159
+ if (!(this as any).options.diversityMetrics?.enabled) return;
160
+
161
+ // If running in fast mode, nudge sensible sampling defaults once
162
+ if ((this as any).options.fastMode && !(this as any)._fastModeTuned) {
163
+ const dm = (this as any).options.diversityMetrics;
164
+ if (dm) {
165
+ if (dm.pairSample == null) dm.pairSample = 20;
166
+ if (dm.graphletSample == null) dm.graphletSample = 30;
167
+ }
168
+ if (
169
+ (this as any).options.novelty?.enabled &&
170
+ (this as any).options.novelty.k == null
171
+ )
172
+ (this as any).options.novelty.k = 5;
173
+ (this as any)._fastModeTuned = true;
174
+ }
175
+
176
+ /** Number of random pairwise samples to draw for compatibility stats. */
177
+ /**
178
+ * Target number of random genome pairs sampled to estimate mean and
179
+ * variance of compatibility distance. A smaller fixed-size sample keeps
180
+ * runtime sub-linear in population size while still providing a stable
181
+ * signal for diversity trend tracking.
182
+ */
183
+ const pairSample = (this as any).options.diversityMetrics.pairSample ?? 40;
184
+
185
+ /** Number of 3-node graphlets to sample for motif statistics. */
186
+ /**
187
+ * Number of randomly selected 3-node subgraphs (graphlets) whose internal
188
+ * enabled edge counts are tallied to approximate motif distribution and
189
+ * structural diversity.
190
+ */
191
+ const graphletSample =
192
+ (this as any).options.diversityMetrics.graphletSample ?? 60;
193
+
194
+ /** Reference to the current population array (genomes). */
195
+ /**
196
+ * Array reference to the active population for the current generation.
197
+ * This is sampled repeatedly for compatibility and motif statistics.
198
+ */
199
+ const population = (this as any).population;
200
+
201
+ /** Cached population size (length of `population`). */
202
+ /**
203
+ * Population size scalar cached to avoid repeated property lookups in
204
+ * inner sampling loops where micro-optimizations marginally reduce GC.
205
+ */
206
+ const popSize = population.length;
207
+
208
+ // --- Pairwise compatibility sampling -------------------------------------------------
209
+ /** Sum of compatibility distances sampled. */
210
+ let compatSum = 0;
211
+ /** Sum of squared compatibility distances (for variance). */
212
+ let compatSq = 0;
213
+ /** Number of compatibility pairs sampled. */
214
+ let compatCount = 0;
215
+
216
+ for (let iter = 0; iter < pairSample; iter++) {
217
+ // If population too small, stop sampling
218
+ if (popSize < 2) break;
219
+ const i = Math.floor((this as any)._getRNG()() * popSize);
220
+ let j = Math.floor((this as any)._getRNG()() * popSize);
221
+ if (j === i) j = (j + 1) % popSize;
222
+ const d = (this as any)._compatibilityDistance(
223
+ population[i],
224
+ population[j]
225
+ );
226
+ compatSum += d;
227
+ compatSq += d * d;
228
+ compatCount++;
229
+ }
230
+
231
+ /** Mean compatibility distance from pairwise sampling. */
232
+ const meanCompat = compatCount ? compatSum / compatCount : 0;
233
+
234
+ /** Sample variance of compatibility distances (floored at zero). */
235
+ const varCompat = compatCount
236
+ ? Math.max(0, compatSq / compatCount - meanCompat * meanCompat)
237
+ : 0;
238
+
239
+ // --- Structural entropy across population -------------------------------------------
240
+ /** Structural entropies for each genome in the population. */
241
+ const entropies = population.map((g: any) =>
242
+ (this as any)._structuralEntropy(g)
243
+ );
244
+
245
+ /** Mean structural entropy across the population. */
246
+ const meanEntropy =
247
+ entropies.reduce((a: number, b: number) => a + b, 0) /
248
+ (entropies.length || 1);
249
+
250
+ /** Variance of structural entropy across the population. */
251
+ const varEntropy = entropies.length
252
+ ? entropies.reduce(
253
+ (a: number, b: number) => a + (b - meanEntropy) * (b - meanEntropy),
254
+ 0
255
+ ) / entropies.length
256
+ : 0;
257
+
258
+ // --- Graphlet (3-node motif) sampling -----------------------------------------------
259
+ /** Counters for 3-node motif types (index = number of edges 0..3). */
260
+ /**
261
+ * Frequency counters for sampled 3-node motifs grouped by how many enabled
262
+ * edges connect the three chosen nodes. Index corresponds to edge count.
263
+ */
264
+ const motifCounts = [0, 0, 0, 0];
265
+
266
+ for (let iter = 0; iter < graphletSample; iter++) {
267
+ const g = population[Math.floor((this as any)._getRNG()() * popSize)];
268
+ if (!g) break;
269
+ // skip tiny genomes
270
+ if (g.nodes.length < 3) continue;
271
+
272
+ /** Set of random node indices used to form a 3-node graphlet. */
273
+ const selectedIdxs = new Set<number>();
274
+ while (selectedIdxs.size < 3)
275
+ selectedIdxs.add(Math.floor((this as any)._getRNG()() * g.nodes.length));
276
+
277
+ /** Selected node objects corresponding to sampled indices. */
278
+ const selectedNodes = Array.from(selectedIdxs).map((i) => g.nodes[i]);
279
+
280
+ let edges = 0;
281
+ for (const c of g.connections)
282
+ if (c.enabled) {
283
+ if (selectedNodes.includes(c.from) && selectedNodes.includes(c.to))
284
+ edges++;
285
+ }
286
+ if (edges > 3) edges = 3;
287
+ motifCounts[edges]++;
288
+ }
289
+
290
+ /** Total number of motifs sampled (for normalization). */
291
+ const totalMotifs = motifCounts.reduce((a, b) => a + b, 0) || 1;
292
+
293
+ /** Entropy over 3-node motif type distribution. */
294
+ let graphletEntropy = 0;
295
+ for (let k = 0; k < motifCounts.length; k++) {
296
+ const p = motifCounts[k] / totalMotifs;
297
+ if (p > 0) graphletEntropy -= p * Math.log(p);
298
+ }
299
+
300
+ // --- Lineage-based statistics (if enabled) -----------------------------------------
301
+ /** Mean depth of genomes in the lineage tree (if enabled). */
302
+ let lineageMeanDepth = 0;
303
+
304
+ /** Mean pairwise difference in lineage depth. */
305
+ let lineageMeanPairDist = 0;
306
+
307
+ if ((this as any)._lineageEnabled && popSize > 0) {
308
+ const depths = population.map((g: any) => (g as any)._depth ?? 0);
309
+ lineageMeanDepth =
310
+ depths.reduce((a: number, b: number) => a + b, 0) / popSize;
311
+
312
+ /** Sum of absolute differences between sampled lineage depths. */
313
+ let lineagePairSum = 0;
314
+ /** Number of lineage pairs sampled. */
315
+ let lineagePairN = 0;
316
+ for (
317
+ let iter = 0;
318
+ iter < Math.min(pairSample, (popSize * (popSize - 1)) / 2);
319
+ iter++
320
+ ) {
321
+ if (popSize < 2) break;
322
+ const i = Math.floor((this as any)._getRNG()() * popSize);
323
+ let j = Math.floor((this as any)._getRNG()() * popSize);
324
+ if (j === i) j = (j + 1) % popSize;
325
+ lineagePairSum += Math.abs(depths[i] - depths[j]);
326
+ lineagePairN++;
327
+ }
328
+ lineageMeanPairDist = lineagePairN ? lineagePairSum / lineagePairN : 0;
329
+ }
330
+
331
+ // Store the computed diversity statistics on the instance for telemetry
332
+ (this as any)._diversityStats = {
333
+ meanCompat,
334
+ varCompat,
335
+ meanEntropy,
336
+ varEntropy,
337
+ graphletEntropy,
338
+ lineageMeanDepth,
339
+ lineageMeanPairDist,
340
+ };
341
+ }
342
+
343
+ /**
344
+ * Record a telemetry entry into the instance buffer and optionally stream it.
345
+ *
346
+ * Steps:
347
+ * This method performs the following steps to persist and optionally stream telemetry:
348
+ * 1. Apply `applyTelemetrySelect` to filter fields according to user selection.
349
+ * 2. Ensure `this._telemetry` buffer exists and push the entry.
350
+ * 3. If a telemetry stream callback is configured, call it.
351
+ * 4. Trim the buffer to a conservative max size (500 entries).
352
+ *
353
+ * Example:
354
+ * @example
355
+ * // record a simple telemetry entry from inside the evolve loop
356
+ * neat.recordTelemetryEntry({ gen: neat.generation, best: neat.population[0].score });
357
+ * @param entry - Telemetry entry to record.
358
+ */
359
+ export function recordTelemetryEntry(this: NeatLike, entry: TelemetryEntry) {
360
+ try {
361
+ applyTelemetrySelect.call(this as any, entry);
362
+ } catch {}
363
+
364
+ if (!(this as any)._telemetry) (this as any)._telemetry = [];
365
+ (this as any)._telemetry.push(entry);
366
+
367
+ try {
368
+ if (
369
+ (this as any).options.telemetryStream?.enabled &&
370
+ (this as any).options.telemetryStream.onEntry
371
+ )
372
+ (this as any).options.telemetryStream.onEntry(entry);
373
+ } catch {}
374
+
375
+ // Keep the in-memory telemetry buffer bounded to avoid runaway memory usage
376
+ if ((this as any)._telemetry.length > 500) (this as any)._telemetry.shift();
377
+ }
378
+
379
+ /**
380
+ * Build a comprehensive telemetry entry for the current generation.
381
+ *
382
+ * The returned object contains a snapshot of population statistics, multi-
383
+ * objective front sizes, operator statistics, lineage summaries and optional
384
+ * complexity/performance metrics depending on configured telemetry options.
385
+ *
386
+ * This function intentionally mirrors the legacy in-loop telemetry construction
387
+ * to preserve behavior relied upon by tests and consumers.
388
+ *
389
+ * Example:
390
+ * @example
391
+ * // build a telemetry snapshot for the current generation
392
+ * const snapshot = neat.buildTelemetryEntry(neat.population[0]);
393
+ * neat.recordTelemetryEntry(snapshot);
394
+ *
395
+ * @param fittest - The currently fittest genome (used to report `best` score).
396
+ * @returns A TelemetryEntry object suitable for recording/streaming.
397
+ */
398
+ export function buildTelemetryEntry(
399
+ this: NeatLike,
400
+ fittest: any
401
+ ): TelemetryEntry {
402
+ /**
403
+ * Current generation index for this telemetry snapshot.
404
+ * Anchors all reported statistics to a single evolutionary timestep.
405
+ * @example
406
+ * // use the generation number when inspecting recorded telemetry
407
+ * const generation = neat.generation;
408
+ */
409
+ const gen = (this as any).generation;
410
+
411
+ // ---------------------------------------------------------------------------
412
+ // Multi-objective (MO) path: compute MO-specific telemetry when enabled.
413
+ // Method steps:
414
+ // 1) Compute a lightweight hypervolume-like proxy over the first Pareto
415
+ // front to summarize quality + parsimony.
416
+ // 2) Collect sizes of the first few Pareto fronts to observe convergence.
417
+ // 3) Snapshot operator statistics (success/attempt counts).
418
+ // 4) Attach diversity, lineage and objective meta-data if available.
419
+ // 5) Optionally attach complexity & perf metrics based on options.
420
+ // 6) Return the assembled telemetry entry.
421
+ // ---------------------------------------------------------------------------
422
+
423
+ /**
424
+ * Running accumulator for a lightweight hypervolume-like proxy.
425
+ * This heuristic weights normalized objective score by inverse complexity
426
+ * so smaller Pareto-optimal solutions are favored. Not a formal HV.
427
+ */
428
+ let hyperVolumeProxy = 0;
429
+ if ((this as any).options.multiObjective?.enabled) {
430
+ /**
431
+ * Complexity dimension name used to penalize solutions inside the
432
+ * hypervolume proxy. Expected values: 'nodes' or 'connections'.
433
+ * @example
434
+ * // penalize by number of connections
435
+ * neat.options.multiObjective.complexityMetric = 'connections';
436
+ */
437
+ /**
438
+ * Selected complexity metric used to penalize genomes in the hypervolume
439
+ * proxy. Allowed values: 'nodes' | 'connections'. Defaults to 'connections'.
440
+ * @example
441
+ * // penalize by number of connections
442
+ * neat.options.multiObjective.complexityMetric = 'connections';
443
+ */
444
+ const complexityMetric =
445
+ (this as any).options.multiObjective.complexityMetric || 'connections';
446
+
447
+ /**
448
+ * Primary objective scalar values for the current population. These are
449
+ * used to compute normalization bounds when forming the hypervolume
450
+ * proxy so all scores lie in a comparable [0,1] range.
451
+ */
452
+ /**
453
+ * Array of primary objective scalars (one per genome). Used to compute
454
+ * normalization bounds so scores are comparable when forming the proxy.
455
+ */
456
+ const primaryObjectiveScores = (this as any).population.map(
457
+ (genome: any) => genome.score || 0
458
+ );
459
+
460
+ /** Minimum observed primary objective score in the population. */
461
+ const minPrimaryScore = Math.min(...primaryObjectiveScores);
462
+
463
+ /** Maximum observed primary objective score in the population. */
464
+ const maxPrimaryScore = Math.max(...primaryObjectiveScores);
465
+
466
+ /**
467
+ * Collection of Pareto front sizes for the first few ranks (0..4).
468
+ * Recording only the early fronts keeps telemetry compact while showing
469
+ * population partitioning across non-dominated sets.
470
+ */
471
+ /**
472
+ * Sizes of the first few Pareto fronts (front 0..4). Recording only the
473
+ * early fronts keeps telemetry compact while showing partitioning.
474
+ */
475
+ const paretoFrontSizes: number[] = [];
476
+
477
+ // Collect sizes of the first few Pareto fronts
478
+ for (let r = 0; r < 5; r++) {
479
+ const size = (this as any).population.filter(
480
+ (g: any) => ((g as any)._moRank ?? 0) === r
481
+ ).length;
482
+ if (!size) break;
483
+ paretoFrontSizes.push(size);
484
+ }
485
+
486
+ // Compute a simple hypervolume proxy: normalized score weighted by inverse complexity
487
+ // Accumulate hypervolume proxy contributions from Pareto-front genomes
488
+ for (const genome of (this as any).population) {
489
+ const rank = (genome as any)._moRank ?? 0;
490
+ if (rank !== 0) continue; // only consider Pareto front 0
491
+ /**
492
+ * Normalized primary objective score in [0,1]. When all scores are
493
+ * identical normalization would divide by zero, so we guard and treat
494
+ * contributions as 0 in that degenerate case.
495
+ */
496
+ /**
497
+ * Normalized primary objective score in [0,1]. Guards against
498
+ * divide-by-zero when all scores are identical by treating contribution
499
+ * as 0 in that degenerate case.
500
+ */
501
+ const normalizedScore =
502
+ maxPrimaryScore > minPrimaryScore
503
+ ? ((genome.score || 0) - minPrimaryScore) /
504
+ (maxPrimaryScore - minPrimaryScore)
505
+ : 0;
506
+
507
+ /**
508
+ * Genome complexity measured along the chosen complexity metric. This
509
+ * is used to apply a parsimony penalty so simpler genomes contribute
510
+ * proportionally more to the hypervolume proxy.
511
+ */
512
+ /**
513
+ * Genome complexity measured along the chosen complexity metric. Used
514
+ * to apply a parsimony penalty so simpler genomes contribute more.
515
+ */
516
+ const genomeComplexity =
517
+ complexityMetric === 'nodes'
518
+ ? genome.nodes.length
519
+ : genome.connections.length;
520
+
521
+ // Accumulate the proxy (higher is better): score scaled by inverse complexity
522
+ hyperVolumeProxy += normalizedScore * (1 / (genomeComplexity + 1));
523
+ }
524
+
525
+ /**
526
+ * Snapshot of operator statistics. Each entry is an object describing a
527
+ * genetic operator with counts for successful applications and attempts.
528
+ * These are useful for visualizations showing operator effectiveness.
529
+ * @example
530
+ * // [{ op: 'mutate.addNode', succ: 12, att: 50 }, ...]
531
+ */
532
+ /**
533
+ * Snapshot of operator statistics collected from the running counters.
534
+ * Each entry contains the operator name and its success/attempt counts.
535
+ * Useful for diagnostics and operator effectiveness visualizations.
536
+ * @example
537
+ * // [{ op: 'mutate.addNode', succ: 12, att: 50 }, ...]
538
+ */
539
+ /**
540
+ * Snapshot of operator statistics: an array of {op, succ, att} objects
541
+ * where `succ` is the number of successful applications and `att` is
542
+ * the total attempts. Helpful for diagnostics and operator visualizations.
543
+ * @example
544
+ * // [{ op: 'mutate.addNode', succ: 12, att: 50 }, ...]
545
+ */
546
+ const operatorStatsSnapshot = (Array.from(
547
+ (this as any)._operatorStats.entries()
548
+ ) as any[]).map(([opName, stats]: any) => ({
549
+ op: opName,
550
+ succ: stats.success,
551
+ att: stats.attempts,
552
+ }));
553
+
554
+ /**
555
+ * Telemetry entry being constructed for multi-objective mode. Contains
556
+ * core metrics, the MO proxies and optional snapshots such as diversity,
557
+ * operator stats, lineage and complexity metrics. This object is later
558
+ * augmented conditionally based on enabled features.
559
+ */
560
+ /**
561
+ * Telemetry entry assembled in multi-objective mode. Contains core
562
+ * statistics plus MO-specific proxies and optional detailed snapshots.
563
+ * This object is suitable for recording or streaming as-is.
564
+ *
565
+ * @example
566
+ * // peek at current generation telemetry
567
+ * console.log(entry.gen, entry.best, entry.hyper);
568
+ */
569
+ const entry: any = {
570
+ gen,
571
+ best: fittest.score,
572
+ species: (this as any)._species.length,
573
+ hyper: hyperVolumeProxy,
574
+ fronts: paretoFrontSizes,
575
+ diversity: (this as any)._diversityStats,
576
+ ops: operatorStatsSnapshot,
577
+ };
578
+
579
+ if (!entry.objImportance) entry.objImportance = {};
580
+ // objective importance snapshot already computed in evolve and stored on temp property if any
581
+ if ((this as any)._lastObjImportance)
582
+ entry.objImportance = (this as any)._lastObjImportance;
583
+
584
+ /**
585
+ * Optional snapshot of objective ages: a map objectiveKey -> age (generations).
586
+ */
587
+ if ((this as any)._objectiveAges?.size) {
588
+ entry.objAges = (Array.from(
589
+ (this as any)._objectiveAges.entries()
590
+ ) as any[]).reduce((a: any, kv: any) => {
591
+ a[kv[0]] = kv[1];
592
+ return a;
593
+ }, {} as any);
594
+ }
595
+
596
+ // Record pending objective lifecycle events (adds/removes) for telemetry
597
+ if (
598
+ (this as any)._pendingObjectiveAdds?.length ||
599
+ (this as any)._pendingObjectiveRemoves?.length
600
+ ) {
601
+ entry.objEvents = [] as any[];
602
+ for (const k of (this as any)._pendingObjectiveAdds)
603
+ entry.objEvents.push({ type: 'add', key: k });
604
+ for (const k of (this as any)._pendingObjectiveRemoves)
605
+ entry.objEvents.push({ type: 'remove', key: k });
606
+ (this as any)._objectiveEvents.push(
607
+ ...entry.objEvents.map((e: any) => ({ gen, type: e.type, key: e.key }))
608
+ );
609
+ (this as any)._pendingObjectiveAdds = [];
610
+ (this as any)._pendingObjectiveRemoves = [];
611
+ }
612
+
613
+ /**
614
+ * Optional per-species offspring allocation snapshot from the most recent
615
+ * allocation calculation. Used for tracking reproductive budgets.
616
+ */
617
+ if ((this as any)._lastOffspringAlloc)
618
+ entry.speciesAlloc = (this as any)._lastOffspringAlloc.slice();
619
+ try {
620
+ entry.objectives = ((this as any)._getObjectives() as any[]).map(
621
+ (o: any) => o.key
622
+ );
623
+ } catch {}
624
+ if (
625
+ ((this as any).options as any).rngState &&
626
+ (this as any)._rngState !== undefined
627
+ )
628
+ entry.rng = (this as any)._rngState;
629
+
630
+ if ((this as any)._lineageEnabled) {
631
+ /**
632
+ * Best genome in the population (index 0 assumed to be fittest in the
633
+ * maintained sort order). Used to capture parent references and depth.
634
+ */
635
+ const bestGenome = (this as any).population[0] as any;
636
+ const depths = (this as any).population.map(
637
+ (g: any) => (g as any)._depth ?? 0
638
+ );
639
+ (this as any)._lastMeanDepth =
640
+ depths.reduce((a: number, b: number) => a + b, 0) /
641
+ (depths.length || 1);
642
+ const { computeAncestorUniqueness } = require('./neat.lineage');
643
+ const ancestorUniqueness = computeAncestorUniqueness.call(this as any);
644
+ entry.lineage = {
645
+ parents: Array.isArray(bestGenome._parents)
646
+ ? bestGenome._parents.slice()
647
+ : [],
648
+ depthBest: bestGenome._depth ?? 0,
649
+ meanDepth: +(this as any)._lastMeanDepth.toFixed(2),
650
+ inbreeding: (this as any)._prevInbreedingCount,
651
+ ancestorUniq: ancestorUniqueness,
652
+ };
653
+ }
654
+
655
+ if (
656
+ (this as any).options.telemetry?.hypervolume &&
657
+ (this as any).options.multiObjective?.enabled
658
+ )
659
+ entry.hv = +hyperVolumeProxy.toFixed(4);
660
+
661
+ if ((this as any).options.telemetry?.complexity) {
662
+ const nodesArr = (this as any).population.map((g: any) => g.nodes.length);
663
+ const connsArr = (this as any).population.map(
664
+ (g: any) => g.connections.length
665
+ );
666
+ const meanNodes =
667
+ nodesArr.reduce((a: number, b: number) => a + b, 0) /
668
+ (nodesArr.length || 1);
669
+ const meanConns =
670
+ connsArr.reduce((a: number, b: number) => a + b, 0) /
671
+ (connsArr.length || 1);
672
+ const maxNodes = nodesArr.length ? Math.max(...nodesArr) : 0;
673
+ const maxConns = connsArr.length ? Math.max(...connsArr) : 0;
674
+ const enabledRatios = (this as any).population.map((g: any) => {
675
+ let enabled = 0,
676
+ disabled = 0;
677
+ for (const c of g.connections) {
678
+ if ((c as any).enabled === false) disabled++;
679
+ else enabled++;
680
+ }
681
+ return enabled + disabled ? enabled / (enabled + disabled) : 0;
682
+ });
683
+ const meanEnabledRatio =
684
+ enabledRatios.reduce((a: number, b: number) => a + b, 0) /
685
+ (enabledRatios.length || 1);
686
+ const growthNodes =
687
+ (this as any)._lastMeanNodes !== undefined
688
+ ? meanNodes - (this as any)._lastMeanNodes
689
+ : 0;
690
+ const growthConns =
691
+ (this as any)._lastMeanConns !== undefined
692
+ ? meanConns - (this as any)._lastMeanConns
693
+ : 0;
694
+ (this as any)._lastMeanNodes = meanNodes;
695
+ (this as any)._lastMeanConns = meanConns;
696
+ entry.complexity = {
697
+ meanNodes: +meanNodes.toFixed(2),
698
+ meanConns: +meanConns.toFixed(2),
699
+ maxNodes,
700
+ maxConns,
701
+ meanEnabledRatio: +meanEnabledRatio.toFixed(3),
702
+ growthNodes: +growthNodes.toFixed(2),
703
+ growthConns: +growthConns.toFixed(2),
704
+ budgetMaxNodes: (this as any).options.maxNodes,
705
+ budgetMaxConns: (this as any).options.maxConns,
706
+ };
707
+ }
708
+
709
+ if ((this as any).options.telemetry?.performance)
710
+ entry.perf = {
711
+ evalMs: (this as any)._lastEvalDuration,
712
+ evolveMs: (this as any)._lastEvolveDuration,
713
+ };
714
+ return entry;
715
+ }
716
+
717
+ // Fallback path (mono-objective) retained for parity with legacy behavior.
718
+ /**
719
+ * Snapshot of operator statistics for mono-objective mode. Kept separate
720
+ * from the MO snapshot to document the intent and avoid accidental
721
+ * coupling.
722
+ */
723
+ const operatorStatsSnapshotMono = (Array.from(
724
+ (this as any)._operatorStats.entries()
725
+ ) as any[]).map(([opName, stats]: any) => ({
726
+ op: opName,
727
+ succ: stats.success,
728
+ att: stats.attempts,
729
+ }));
730
+
731
+ /**
732
+ * Telemetry entry object for mono-objective mode. Aligns with the
733
+ * multi-objective structure but omits MO-only fields like `fronts`.
734
+ */
735
+ const entry: TelemetryEntry = {
736
+ gen,
737
+ best: fittest.score,
738
+ species: (this as any)._species.length,
739
+ hyper: hyperVolumeProxy,
740
+ diversity: (this as any)._diversityStats,
741
+ ops: operatorStatsSnapshotMono,
742
+ objImportance: {},
743
+ } as TelemetryEntry;
744
+
745
+ if ((this as any)._lastObjImportance)
746
+ entry.objImportance = (this as any)._lastObjImportance;
747
+ if ((this as any)._objectiveAges?.size)
748
+ entry.objAges = (Array.from(
749
+ (this as any)._objectiveAges.entries()
750
+ ) as any[]).reduce((a: any, kv: any) => {
751
+ a[kv[0]] = kv[1];
752
+ return a;
753
+ }, {} as any);
754
+
755
+ if (
756
+ (this as any)._pendingObjectiveAdds?.length ||
757
+ (this as any)._pendingObjectiveRemoves?.length
758
+ ) {
759
+ entry.objEvents = [] as any[];
760
+ for (const k of (this as any)._pendingObjectiveAdds)
761
+ entry.objEvents.push({ type: 'add', key: k });
762
+ for (const k of (this as any)._pendingObjectiveRemoves)
763
+ entry.objEvents.push({ type: 'remove', key: k });
764
+ (this as any)._objectiveEvents.push(
765
+ ...entry.objEvents.map((e: any) => ({ gen, type: e.type, key: e.key }))
766
+ );
767
+ (this as any)._pendingObjectiveAdds = [];
768
+ (this as any)._pendingObjectiveRemoves = [];
769
+ }
770
+
771
+ if ((this as any)._lastOffspringAlloc)
772
+ entry.speciesAlloc = (this as any)._lastOffspringAlloc.slice();
773
+ try {
774
+ entry.objectives = ((this as any)._getObjectives() as any[]).map(
775
+ (o: any) => o.key
776
+ );
777
+ } catch {}
778
+ if (
779
+ ((this as any).options as any).rngState &&
780
+ (this as any)._rngState !== undefined
781
+ )
782
+ entry.rng = (this as any)._rngState;
783
+
784
+ if ((this as any)._lineageEnabled) {
785
+ /**
786
+ * Best genome in the population (index 0 assumed to be fittest in the
787
+ * maintained sort order). Used to capture parent references and depth.
788
+ */
789
+ const bestGenome = (this as any).population[0] as any;
790
+
791
+ /**
792
+ * Array of lineage depths for each genome in the population. Depth is a
793
+ * lightweight proxy of ancestry tree height for each genome.
794
+ */
795
+ const depths = (this as any).population.map(
796
+ (g: any) => (g as any)._depth ?? 0
797
+ );
798
+ (this as any)._lastMeanDepth =
799
+ depths.reduce((a: number, b: number) => a + b, 0) / (depths.length || 1);
800
+
801
+ const { buildAnc } = require('./neat.lineage');
802
+
803
+ /**
804
+ * Number of lineage pairwise samples actually evaluated. Used to
805
+ * normalize the averaged Jaccard-like ancestor uniqueness metric.
806
+ */
807
+ let sampledPairs = 0;
808
+
809
+ /**
810
+ * Running sum of Jaccard-like distances between sampled ancestor sets.
811
+ */
812
+ let jaccardSum = 0;
813
+
814
+ /**
815
+ * Maximum number of random pairs to sample when estimating ancestor
816
+ * uniqueness. Bounds runtime while providing a stable estimate.
817
+ */
818
+ const samplePairs = Math.min(
819
+ 30,
820
+ ((this as any).population.length *
821
+ ((this as any).population.length - 1)) /
822
+ 2
823
+ );
824
+
825
+ for (let t = 0; t < samplePairs; t++) {
826
+ if ((this as any).population.length < 2) break;
827
+ const i = Math.floor(
828
+ (this as any)._getRNG()() * (this as any).population.length
829
+ );
830
+ let j = Math.floor(
831
+ (this as any)._getRNG()() * (this as any).population.length
832
+ );
833
+ if (j === i) j = (j + 1) % (this as any).population.length;
834
+
835
+ /**
836
+ * Ancestor sets for the two randomly chosen genomes used to compute a
837
+ * Jaccard-like dissimilarity (1 - intersection/union).
838
+ */
839
+ const ancestorsA = buildAnc.call(
840
+ this as any,
841
+ (this as any).population[i] as any
842
+ );
843
+ const ancestorsB = buildAnc.call(
844
+ this as any,
845
+ (this as any).population[j] as any
846
+ );
847
+ if (ancestorsA.size === 0 && ancestorsB.size === 0) continue;
848
+ let intersectionCount = 0;
849
+ for (const id of ancestorsA) if (ancestorsB.has(id)) intersectionCount++;
850
+ const union = ancestorsA.size + ancestorsB.size - intersectionCount || 1;
851
+
852
+ /**
853
+ * Jaccard-like dissimilarity between ancestor sets. A value near 1
854
+ * indicates little shared ancestry; near 0 indicates high overlap.
855
+ */
856
+ const jaccardDistance = 1 - intersectionCount / union;
857
+ jaccardSum += jaccardDistance;
858
+ sampledPairs++;
859
+ }
860
+
861
+ const ancestorUniqueness = sampledPairs
862
+ ? +(jaccardSum / sampledPairs).toFixed(3)
863
+ : 0;
864
+ entry.lineage = {
865
+ parents: Array.isArray(bestGenome._parents)
866
+ ? bestGenome._parents.slice()
867
+ : [],
868
+ depthBest: bestGenome._depth ?? 0,
869
+ meanDepth: +(this as any)._lastMeanDepth.toFixed(2),
870
+ inbreeding: (this as any)._prevInbreedingCount,
871
+ ancestorUniq: ancestorUniqueness,
872
+ };
873
+ }
874
+
875
+ if (
876
+ (this as any).options.telemetry?.hypervolume &&
877
+ (this as any).options.multiObjective?.enabled
878
+ )
879
+ entry.hv = +hyperVolumeProxy.toFixed(4);
880
+ if ((this as any).options.telemetry?.complexity) {
881
+ const nodesArr = (this as any).population.map((g: any) => g.nodes.length);
882
+ const connsArr = (this as any).population.map(
883
+ (g: any) => g.connections.length
884
+ );
885
+ const meanNodes =
886
+ nodesArr.reduce((a: number, b: number) => a + b, 0) /
887
+ (nodesArr.length || 1);
888
+ const meanConns =
889
+ connsArr.reduce((a: number, b: number) => a + b, 0) /
890
+ (connsArr.length || 1);
891
+ const maxNodes = nodesArr.length ? Math.max(...nodesArr) : 0;
892
+ const maxConns = connsArr.length ? Math.max(...connsArr) : 0;
893
+ const enabledRatios = (this as any).population.map((g: any) => {
894
+ let en = 0,
895
+ dis = 0;
896
+ for (const c of g.connections) {
897
+ if ((c as any).enabled === false) dis++;
898
+ else en++;
899
+ }
900
+ return en + dis ? en / (en + dis) : 0;
901
+ });
902
+ const meanEnabledRatio =
903
+ enabledRatios.reduce((a: number, b: number) => a + b, 0) /
904
+ (enabledRatios.length || 1);
905
+ const growthNodes =
906
+ (this as any)._lastMeanNodes !== undefined
907
+ ? meanNodes - (this as any)._lastMeanNodes
908
+ : 0;
909
+ const growthConns =
910
+ (this as any)._lastMeanConns !== undefined
911
+ ? meanConns - (this as any)._lastMeanConns
912
+ : 0;
913
+ (this as any)._lastMeanNodes = meanNodes;
914
+ (this as any)._lastMeanConns = meanConns;
915
+ entry.complexity = {
916
+ meanNodes: +meanNodes.toFixed(2),
917
+ meanConns: +meanConns.toFixed(2),
918
+ maxNodes,
919
+ maxConns,
920
+ meanEnabledRatio: +meanEnabledRatio.toFixed(3),
921
+ growthNodes: +growthNodes.toFixed(2),
922
+ growthConns: +growthConns.toFixed(2),
923
+ budgetMaxNodes: (this as any).options.maxNodes,
924
+ budgetMaxConns: (this as any).options.maxConns,
925
+ };
926
+ }
927
+ if ((this as any).options.telemetry?.performance)
928
+ entry.perf = {
929
+ evalMs: (this as any)._lastEvalDuration,
930
+ evolveMs: (this as any)._lastEvolveDuration,
931
+ };
932
+ return entry;
933
+ }