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