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