@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,426 @@
|
|
|
1
|
+
import Network from '../network';
|
|
2
|
+
import * as methods from '../../methods/methods';
|
|
3
|
+
import { config } from '../../config';
|
|
4
|
+
import Multi from '../../multithreading/multi';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A single supervised training example used to evaluate fitness.
|
|
8
|
+
*/
|
|
9
|
+
interface TrainingSample {
|
|
10
|
+
input: number[];
|
|
11
|
+
output: number[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Internal evolution configuration summary (for potential logging / debugging)
|
|
16
|
+
* capturing normalized option values used by the local evolutionary loop.
|
|
17
|
+
*/
|
|
18
|
+
interface EvolutionConfig {
|
|
19
|
+
targetError: number;
|
|
20
|
+
growth: number;
|
|
21
|
+
cost: any;
|
|
22
|
+
amount: number;
|
|
23
|
+
log: number;
|
|
24
|
+
schedule: any;
|
|
25
|
+
clear: boolean;
|
|
26
|
+
threads: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Cache for complexity penalty computations keyed by genome (Network) reference.
|
|
31
|
+
* We store counts used to derive a simple structural complexity measure so repeated
|
|
32
|
+
* invocations during a generation avoid recomputing the same base value.
|
|
33
|
+
*/
|
|
34
|
+
const _complexityCache: WeakMap<
|
|
35
|
+
Network,
|
|
36
|
+
{ nodes: number; conns: number; gates: number; value: number }
|
|
37
|
+
> = new WeakMap();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Compute a structural complexity penalty scaled by a growth factor.
|
|
41
|
+
*
|
|
42
|
+
* Complexity heuristic:
|
|
43
|
+
* (hidden nodes) + (connections) + (gates)
|
|
44
|
+
* hidden nodes = total nodes - input - output (to avoid penalizing fixed I/O interface size).
|
|
45
|
+
*
|
|
46
|
+
* Rationale: Encourages minimal / parsimonious networks by subtracting a term from fitness
|
|
47
|
+
* proportional to network size, counteracting bloat. Growth hyper‑parameter tunes pressure.
|
|
48
|
+
*
|
|
49
|
+
* Caching strategy: We memoize the base complexity (pre‑growth scaling) per genome when its
|
|
50
|
+
* structural counts (nodes / connections / gates) are unchanged. This is safe because only
|
|
51
|
+
* structural mutations alter these counts, and those invalidate earlier entries naturally
|
|
52
|
+
* (since mutated genomes are distinct object references in typical NEAT flows).
|
|
53
|
+
*
|
|
54
|
+
* @param genome - Candidate network whose complexity to measure.
|
|
55
|
+
* @param growth - Positive scalar controlling strength of parsimony pressure.
|
|
56
|
+
* @returns Complexity * growth (used directly to subtract from fitness score).
|
|
57
|
+
*/
|
|
58
|
+
function computeComplexityPenalty(genome: Network, growth: number): number {
|
|
59
|
+
// Extract structural counts once.
|
|
60
|
+
const n = genome.nodes.length;
|
|
61
|
+
const c = genome.connections.length;
|
|
62
|
+
const g = genome.gates.length;
|
|
63
|
+
// Fast path: counts unchanged -> reuse cached base complexity value.
|
|
64
|
+
const cached = _complexityCache.get(genome);
|
|
65
|
+
if (cached && cached.nodes === n && cached.conns === c && cached.gates === g)
|
|
66
|
+
return cached.value * growth;
|
|
67
|
+
// Base complexity ignoring growth factor.
|
|
68
|
+
const base = n - genome.input - genome.output + c + g;
|
|
69
|
+
_complexityCache.set(genome, { nodes: n, conns: c, gates: g, value: base });
|
|
70
|
+
return base * growth;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build a single-threaded fitness evaluation function (classic NEAT style) evaluating a genome
|
|
75
|
+
* over the provided dataset and returning a scalar score where higher is better.
|
|
76
|
+
*
|
|
77
|
+
* Fitness Definition:
|
|
78
|
+
* fitness = -averageError - complexityPenalty
|
|
79
|
+
* We accumulate negative error (so lower error => higher fitness) over `amount` independent
|
|
80
|
+
* evaluations (amount>1 can smooth stochastic evaluation noise) then subtract complexity penalty.
|
|
81
|
+
*
|
|
82
|
+
* Error handling: If evaluation throws (numerical instability, internal error) we return -Infinity
|
|
83
|
+
* so such genomes are strongly disfavored.
|
|
84
|
+
*
|
|
85
|
+
* @param set - Dataset of training samples.
|
|
86
|
+
* @param cost - Cost function reference (should expose error computation in genome.test).
|
|
87
|
+
* @param amount - Number of repeated evaluations to average.
|
|
88
|
+
* @param growth - Complexity penalty scalar.
|
|
89
|
+
* @returns Function mapping a Network genome to a numeric fitness.
|
|
90
|
+
*/
|
|
91
|
+
function buildSingleThreadFitness(
|
|
92
|
+
set: TrainingSample[],
|
|
93
|
+
cost: any,
|
|
94
|
+
amount: number,
|
|
95
|
+
growth: number
|
|
96
|
+
) {
|
|
97
|
+
return (genome: Network) => {
|
|
98
|
+
let score = 0; // Accumulate negative errors.
|
|
99
|
+
for (let i = 0; i < amount; i++) {
|
|
100
|
+
try {
|
|
101
|
+
score -= genome.test(set, cost).error; // negative adds fitness.
|
|
102
|
+
} catch (e: any) {
|
|
103
|
+
if (config.warnings)
|
|
104
|
+
console.warn(
|
|
105
|
+
`Genome evaluation failed: ${
|
|
106
|
+
(e && e.message) || e
|
|
107
|
+
}. Penalizing with -Infinity fitness.`
|
|
108
|
+
);
|
|
109
|
+
return -Infinity;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Apply structural parsimony pressure.
|
|
113
|
+
score -= computeComplexityPenalty(genome, growth);
|
|
114
|
+
// Guard against NaN pollution.
|
|
115
|
+
score = isNaN(score) ? -Infinity : score;
|
|
116
|
+
// Average over repeats.
|
|
117
|
+
return score / amount;
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Build a multi-threaded (worker-based) population fitness evaluator if worker infrastructure is available.
|
|
123
|
+
*
|
|
124
|
+
* Strategy:
|
|
125
|
+
* - Attempt to dynamically obtain a Worker constructor (node or browser variant).
|
|
126
|
+
* - If not possible, gracefully fall back to single-thread evaluation.
|
|
127
|
+
* - Spawn N workers (threads) each capable of evaluating genomes by calling worker.evaluate(genome).
|
|
128
|
+
* - Provide a fitness function that takes the whole population and returns a Promise that resolves
|
|
129
|
+
* when all queued genomes have been processed. Each genome's score is written in-place.
|
|
130
|
+
*
|
|
131
|
+
* Implementation details:
|
|
132
|
+
* - Queue: simple FIFO (array shift) suffices because ordering is not critical.
|
|
133
|
+
* - Robustness: Each worker evaluation is wrapped with error handling to prevent a single failure
|
|
134
|
+
* from stalling the batch; failed evaluations simply proceed to next genome.
|
|
135
|
+
* - Complexity penalty applied after raw result retrieval: genome.score = -result - penalty.
|
|
136
|
+
*
|
|
137
|
+
* Returned metadata sets options.fitnessPopulation=true so downstream NEAT logic treats the fitness
|
|
138
|
+
* function as operating over the entire population at once (rather than per-genome).
|
|
139
|
+
*
|
|
140
|
+
* @param set - Dataset.
|
|
141
|
+
* @param cost - Cost function.
|
|
142
|
+
* @param amount - Repetition count (unused directly here; assumed handled inside worker.evaluate result metric if needed).
|
|
143
|
+
* @param growth - Complexity penalty scalar.
|
|
144
|
+
* @param threads - Desired worker count.
|
|
145
|
+
* @param options - Evolution options object (mutated to add cleanup hooks & flags).
|
|
146
|
+
* @returns Object with fitnessFunction (population evaluator) and resolved thread count.
|
|
147
|
+
*/
|
|
148
|
+
async function buildMultiThreadFitness(
|
|
149
|
+
set: TrainingSample[],
|
|
150
|
+
cost: any,
|
|
151
|
+
amount: number,
|
|
152
|
+
growth: number,
|
|
153
|
+
threads: number,
|
|
154
|
+
options: any
|
|
155
|
+
) {
|
|
156
|
+
// Serialize dataset once for worker initialization (avoids deep cloning per evaluation call).
|
|
157
|
+
const serializedSet = Multi.serializeDataSet(set);
|
|
158
|
+
/** Collection of worker instances. */
|
|
159
|
+
const workers: any[] = [];
|
|
160
|
+
let WorkerCtor: any = null; // Will hold dynamic Worker class.
|
|
161
|
+
try {
|
|
162
|
+
const isNode =
|
|
163
|
+
typeof process !== 'undefined' && !!(process.versions as any)?.node;
|
|
164
|
+
if (isNode && Multi.workers?.getNodeTestWorker)
|
|
165
|
+
WorkerCtor = await Multi.workers.getNodeTestWorker();
|
|
166
|
+
else if (!isNode && Multi.workers?.getBrowserTestWorker)
|
|
167
|
+
WorkerCtor = await Multi.workers.getBrowserTestWorker();
|
|
168
|
+
} catch (e) {
|
|
169
|
+
if (config.warnings)
|
|
170
|
+
console.warn(
|
|
171
|
+
'Failed to load worker class; falling back to single-thread path:',
|
|
172
|
+
(e as any)?.message || e
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
// Fallback path if no worker support.
|
|
176
|
+
if (!WorkerCtor)
|
|
177
|
+
return {
|
|
178
|
+
fitnessFunction: buildSingleThreadFitness(set, cost, amount, growth),
|
|
179
|
+
threads: 1,
|
|
180
|
+
};
|
|
181
|
+
// Spin up requested workers (best-effort; partial successes still useful).
|
|
182
|
+
for (let i = 0; i < threads; i++) {
|
|
183
|
+
try {
|
|
184
|
+
workers.push(
|
|
185
|
+
new WorkerCtor(serializedSet, {
|
|
186
|
+
name: cost.name || cost.toString?.() || 'cost',
|
|
187
|
+
})
|
|
188
|
+
);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
if (config.warnings) console.warn('Worker spawn failed', e);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Population-level fitness function: resolves when all genomes processed.
|
|
194
|
+
const fitnessFunction = (population: Network[]) =>
|
|
195
|
+
new Promise<void>((resolve) => {
|
|
196
|
+
if (!workers.length) {
|
|
197
|
+
resolve();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const queue = population.slice(); // Shallow copy so we can mutate.
|
|
201
|
+
let active = workers.length; // Number of workers still draining tasks.
|
|
202
|
+
const startNext = (worker: any) => {
|
|
203
|
+
if (!queue.length) {
|
|
204
|
+
if (--active === 0) resolve();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const genome = queue.shift();
|
|
208
|
+
worker
|
|
209
|
+
.evaluate(genome)
|
|
210
|
+
.then((result: number) => {
|
|
211
|
+
if (typeof genome !== 'undefined' && typeof result === 'number') {
|
|
212
|
+
genome.score = -result - computeComplexityPenalty(genome, growth);
|
|
213
|
+
genome.score = isNaN(result) ? -Infinity : genome.score;
|
|
214
|
+
}
|
|
215
|
+
startNext(worker); // Tail recursion style loop.
|
|
216
|
+
})
|
|
217
|
+
.catch(() => startNext(worker)); // On error: skip but keep draining.
|
|
218
|
+
};
|
|
219
|
+
workers.forEach((w) => startNext(w));
|
|
220
|
+
});
|
|
221
|
+
options.fitnessPopulation = true; // Signal population-level semantics.
|
|
222
|
+
// Provide cleanup hook (used after evolution loop) to terminate workers.
|
|
223
|
+
(options as any)._workerTerminators = () => {
|
|
224
|
+
workers.forEach((w) => {
|
|
225
|
+
try {
|
|
226
|
+
w.terminate && w.terminate();
|
|
227
|
+
} catch {}
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
return { fitnessFunction, threads };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Evolve (optimize) the current network's topology and weights using a NEAT-like evolutionary loop
|
|
235
|
+
* until a stopping criterion (target error or max iterations) is met.
|
|
236
|
+
*
|
|
237
|
+
* High-level process:
|
|
238
|
+
* 1. Validate dataset shape (input/output vector sizes must match network I/O counts).
|
|
239
|
+
* 2. Normalize / default option values and construct an internal configuration summary.
|
|
240
|
+
* 3. Build appropriate fitness evaluation function (single or multi-thread).
|
|
241
|
+
* 4. Initialize a Neat population (optionally with speciation) seeded by this network.
|
|
242
|
+
* 5. Iteratively call neat.evolve():
|
|
243
|
+
* - Retrieve fittest genome + its fitness.
|
|
244
|
+
* - Derive an error metric from fitness (inverse relationship considering complexity penalty).
|
|
245
|
+
* - Track best genome overall (elitism) and perform logging/scheduling callbacks.
|
|
246
|
+
* - Break if error criterion satisfied or iterations exceeded.
|
|
247
|
+
* 6. Replace this network's internal structural arrays with the best discovered genome's (in-place upgrade).
|
|
248
|
+
* 7. Cleanup any worker threads and report final statistics.
|
|
249
|
+
*
|
|
250
|
+
* Fitness / Error relationship:
|
|
251
|
+
* fitness = -error - complexityPenalty => error = -(fitness - complexityPenalty)
|
|
252
|
+
* We recompute error from the stored fitness plus penalty to ensure consistent reporting.
|
|
253
|
+
*
|
|
254
|
+
* Resilience strategies:
|
|
255
|
+
* - Guard against infinite / NaN errors; after MAX_INF consecutive invalid errors we abort.
|
|
256
|
+
* - Fallback for tiny populations: increase mutation aggressiveness to prevent premature convergence.
|
|
257
|
+
*
|
|
258
|
+
* @param this - Bound {@link Network} instance being evolved in-place.
|
|
259
|
+
* @param set - Supervised dataset (array of {input, output}).
|
|
260
|
+
* @param options - Evolution options (see README / docs). Key fields include:
|
|
261
|
+
* - iterations: maximum generations (if omitted must supply error target)
|
|
262
|
+
* - error: target error threshold (if omitted must supply iterations)
|
|
263
|
+
* - growth: complexity penalty scaling
|
|
264
|
+
* - amount: number of score evaluations (averaged) per genome
|
|
265
|
+
* - threads: desired worker count (>=2 enables multi-thread path if available)
|
|
266
|
+
* - popsize / populationSize: population size
|
|
267
|
+
* - schedule: { iterations: number, function: (ctx) => void } periodic callback
|
|
268
|
+
* - log: generation interval for console logging
|
|
269
|
+
* - clear: whether to call network.clear() after adopting best genome
|
|
270
|
+
* @returns Summary object { error, iterations, time(ms) }.
|
|
271
|
+
* @throws If dataset is empty or dimensionally incompatible, or if neither iterations nor error is specified.
|
|
272
|
+
*/
|
|
273
|
+
export async function evolveNetwork(
|
|
274
|
+
this: Network,
|
|
275
|
+
set: TrainingSample[],
|
|
276
|
+
options: any
|
|
277
|
+
): Promise<{ error: number; iterations: number; time: number }> {
|
|
278
|
+
// 1. Dataset validation (shape + existence).
|
|
279
|
+
if (
|
|
280
|
+
!set ||
|
|
281
|
+
set.length === 0 ||
|
|
282
|
+
set[0].input.length !== this.input ||
|
|
283
|
+
set[0].output.length !== this.output
|
|
284
|
+
) {
|
|
285
|
+
throw new Error(
|
|
286
|
+
'Dataset is invalid or dimensions do not match network input/output size!'
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
// Defensive defaulting.
|
|
290
|
+
options = options || {};
|
|
291
|
+
let targetError: number = options.error ?? 0.05; // Default target error if provided unspecified.
|
|
292
|
+
const growth: number = options.growth ?? 0.0001; // Complexity penalty scaling.
|
|
293
|
+
const cost = options.cost || methods.Cost.mse; // Default cost function.
|
|
294
|
+
const amount: number = options.amount || 1; // Repetition count for averaging.
|
|
295
|
+
const log: number = options.log || 0; // Logging interval (0 disables).
|
|
296
|
+
const schedule = options.schedule; // Optional user schedule callback spec.
|
|
297
|
+
const clear: boolean = options.clear || false; // Whether to clear state after structural adoption.
|
|
298
|
+
let threads: number =
|
|
299
|
+
typeof options.threads === 'undefined' ? 1 : options.threads; // Worker count.
|
|
300
|
+
const start = Date.now(); // Benchmark start time.
|
|
301
|
+
const evoConfig: EvolutionConfig = {
|
|
302
|
+
targetError,
|
|
303
|
+
growth,
|
|
304
|
+
cost,
|
|
305
|
+
amount,
|
|
306
|
+
log,
|
|
307
|
+
schedule,
|
|
308
|
+
clear,
|
|
309
|
+
threads,
|
|
310
|
+
}; // (Currently unused externally; placeholder for future structured logging.)
|
|
311
|
+
|
|
312
|
+
// 2. Stopping condition checks / normalization.
|
|
313
|
+
if (
|
|
314
|
+
typeof options.iterations === 'undefined' &&
|
|
315
|
+
typeof options.error === 'undefined'
|
|
316
|
+
) {
|
|
317
|
+
throw new Error(
|
|
318
|
+
'At least one stopping condition (`iterations` or `error`) must be specified for evolution.'
|
|
319
|
+
);
|
|
320
|
+
} else if (typeof options.error === 'undefined') targetError = -1;
|
|
321
|
+
// Only iterations constrain.
|
|
322
|
+
else if (typeof options.iterations === 'undefined') options.iterations = 0; // Only error constrains (0 sentinel lets loop run until satisfied).
|
|
323
|
+
|
|
324
|
+
// 3. Build fitness function (single or multi-thread variant).
|
|
325
|
+
let fitnessFunction: any;
|
|
326
|
+
if (threads === 1)
|
|
327
|
+
fitnessFunction = buildSingleThreadFitness(set, cost, amount, growth);
|
|
328
|
+
else {
|
|
329
|
+
const multi = await buildMultiThreadFitness(
|
|
330
|
+
set,
|
|
331
|
+
cost,
|
|
332
|
+
amount,
|
|
333
|
+
growth,
|
|
334
|
+
threads,
|
|
335
|
+
options
|
|
336
|
+
);
|
|
337
|
+
fitnessFunction = multi.fitnessFunction;
|
|
338
|
+
threads = multi.threads;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Provide network reference for NEAT initialization / reproduction methods.
|
|
342
|
+
options.network = this;
|
|
343
|
+
// Alias populationSize -> popsize for backward compat.
|
|
344
|
+
if (options.populationSize != null && options.popsize == null)
|
|
345
|
+
options.popsize = options.populationSize;
|
|
346
|
+
// Speciation default off unless explicitly enabled (simpler baseline behavior).
|
|
347
|
+
if (typeof options.speciation === 'undefined') options.speciation = false;
|
|
348
|
+
|
|
349
|
+
// 4. Lazy import NEAT (avoid heavier modules if evolve isn't used).
|
|
350
|
+
const { default: Neat } = await import('../../neat');
|
|
351
|
+
const neat = new Neat(this.input, this.output, fitnessFunction, options);
|
|
352
|
+
|
|
353
|
+
// Warn if immediate termination conditions could yield empty best genome tracking.
|
|
354
|
+
if (typeof options.iterations === 'number' && options.iterations === 0) {
|
|
355
|
+
if ((neat as any)._warnIfNoBestGenome) {
|
|
356
|
+
try {
|
|
357
|
+
(neat as any)._warnIfNoBestGenome();
|
|
358
|
+
} catch {}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// Micro-population heuristics: increase mutation intensity to promote exploration.
|
|
362
|
+
if (options.popsize && options.popsize <= 10) {
|
|
363
|
+
neat.options.mutationRate = neat.options.mutationRate ?? 0.5;
|
|
364
|
+
neat.options.mutationAmount = neat.options.mutationAmount ?? 1;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// 5. Evolution loop state variables.
|
|
368
|
+
let error = Infinity; // Best error observed this generation (derived from fitness).
|
|
369
|
+
let bestFitness = -Infinity; // Track highest fitness seen.
|
|
370
|
+
let bestGenome: Network | undefined; // Best genome snapshot.
|
|
371
|
+
let infiniteErrorCount = 0; // Consecutive invalid error tallies.
|
|
372
|
+
const MAX_INF = 5; // Abort threshold to prevent endless invalid loops.
|
|
373
|
+
const iterationsSpecified = typeof options.iterations === 'number';
|
|
374
|
+
|
|
375
|
+
// 5a. Main generation loop (terminates on error target or iteration cap).
|
|
376
|
+
while (
|
|
377
|
+
(targetError === -1 || error > targetError) &&
|
|
378
|
+
(!iterationsSpecified || neat.generation < options.iterations)
|
|
379
|
+
) {
|
|
380
|
+
// Perform one generation: breed + evaluate population, returning fittest genome.
|
|
381
|
+
const fittest = await neat.evolve();
|
|
382
|
+
const fitness = fittest.score ?? -Infinity;
|
|
383
|
+
// Derive error metric from fitness (undo sign & complexity adjustment) with fallback Infinity.
|
|
384
|
+
error = -(fitness - computeComplexityPenalty(fittest, growth)) || Infinity;
|
|
385
|
+
// Update elite if improved.
|
|
386
|
+
if (fitness > bestFitness) {
|
|
387
|
+
bestFitness = fitness;
|
|
388
|
+
bestGenome = fittest;
|
|
389
|
+
}
|
|
390
|
+
// Detect runaway invalid values.
|
|
391
|
+
if (!isFinite(error) || isNaN(error)) {
|
|
392
|
+
if (++infiniteErrorCount >= MAX_INF) break;
|
|
393
|
+
} else infiniteErrorCount = 0;
|
|
394
|
+
// User schedule callback hook.
|
|
395
|
+
if (schedule && neat.generation % schedule.iterations === 0) {
|
|
396
|
+
try {
|
|
397
|
+
schedule.function({
|
|
398
|
+
fitness: bestFitness,
|
|
399
|
+
error,
|
|
400
|
+
iteration: neat.generation,
|
|
401
|
+
});
|
|
402
|
+
} catch {}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 6. Adopt best genome's structure into this network instance (in-place upgrade) if available.
|
|
407
|
+
if (typeof bestGenome !== 'undefined') {
|
|
408
|
+
this.nodes = bestGenome.nodes;
|
|
409
|
+
this.connections = bestGenome.connections;
|
|
410
|
+
this.selfconns = bestGenome.selfconns;
|
|
411
|
+
this.gates = bestGenome.gates;
|
|
412
|
+
if (clear) this.clear();
|
|
413
|
+
} else if ((neat as any)._warnIfNoBestGenome) {
|
|
414
|
+
try {
|
|
415
|
+
(neat as any)._warnIfNoBestGenome();
|
|
416
|
+
} catch {}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 7. Cleanup worker resources if any.
|
|
420
|
+
try {
|
|
421
|
+
(options as any)._workerTerminators &&
|
|
422
|
+
(options as any)._workerTerminators();
|
|
423
|
+
} catch {}
|
|
424
|
+
|
|
425
|
+
return { error, iterations: neat.generation, time: Date.now() - start };
|
|
426
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type Network from '../network';
|
|
2
|
+
import Node from '../node';
|
|
3
|
+
import Connection from '../connection';
|
|
4
|
+
import mutation from '../../methods/mutation';
|
|
5
|
+
import { config } from '../../config';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Gating & node removal utilities for {@link Network}.
|
|
9
|
+
*
|
|
10
|
+
* Gating concept:
|
|
11
|
+
* - A "gater" node modulates the effective weight of a target connection. Conceptually the raw
|
|
12
|
+
* connection weight w is multiplied (or otherwise transformed) by a function of the gater node's
|
|
13
|
+
* activation a_g (actual math lives in {@link Node.gate}). This enables dynamic, context-sensitive
|
|
14
|
+
* routing (similar in spirit to attention mechanisms or LSTM-style gates) within an evolved topology.
|
|
15
|
+
*
|
|
16
|
+
* Removal strategy (removeNode):
|
|
17
|
+
* - When excising a hidden node we attempt to preserve overall connectivity by creating bridging
|
|
18
|
+
* connections from each of its predecessors to each of its successors if such edges do not already
|
|
19
|
+
* exist. Optional logic reassigns previous gater nodes to these new edges (best-effort) to preserve
|
|
20
|
+
* modulation diversity.
|
|
21
|
+
*
|
|
22
|
+
* Mutation interplay:
|
|
23
|
+
* - The flag `mutation.SUB_NODE.keep_gates` determines whether gating nodes associated with edges
|
|
24
|
+
* passing through the removed node should be retained and reassigned.
|
|
25
|
+
*
|
|
26
|
+
* Determinism note:
|
|
27
|
+
* - Bridging gate reassignment currently uses Math.random directly; for fully deterministic runs
|
|
28
|
+
* you may consider replacing with the network's seeded RNG (if provided) in future refactors.
|
|
29
|
+
*
|
|
30
|
+
* Exported functions:
|
|
31
|
+
* - {@link gate}: Attach a gater to a connection.
|
|
32
|
+
* - {@link ungate}: Remove gating from a connection.
|
|
33
|
+
* - {@link removeNode}: Remove a hidden node while attempting to preserve connectivity & gating.
|
|
34
|
+
*
|
|
35
|
+
* @module network.gating
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Attach a gater node to a connection so that the connection's effective weight
|
|
40
|
+
* becomes dynamically modulated by the gater's activation (see {@link Node.gate} for exact math).
|
|
41
|
+
*
|
|
42
|
+
* Validation / invariants:
|
|
43
|
+
* - Throws if the gater node is not part of this network (prevents cross-network corruption).
|
|
44
|
+
* - If the connection is already gated, function is a no-op (emits warning when enabled).
|
|
45
|
+
*
|
|
46
|
+
* Complexity: O(1)
|
|
47
|
+
*
|
|
48
|
+
* @param this - Bound {@link Network} instance.
|
|
49
|
+
* @param node - Candidate gater node (must belong to network).
|
|
50
|
+
* @param connection - Connection to gate.
|
|
51
|
+
*/
|
|
52
|
+
export function gate(this: Network, node: Node, connection: Connection) {
|
|
53
|
+
if (!this.nodes.includes(node))
|
|
54
|
+
throw new Error(
|
|
55
|
+
'Gating node must be part of the network to gate a connection!'
|
|
56
|
+
);
|
|
57
|
+
if (connection.gater) {
|
|
58
|
+
if (config.warnings) console.warn('Connection is already gated. Skipping.');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
node.gate(connection); // Delegate per-node bookkeeping (adds to node.connections.gated & sets connection.gater)
|
|
62
|
+
this.gates.push(connection); // Track globally for fast iteration / serialization.
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Remove gating from a connection, restoring its static weight contribution.
|
|
67
|
+
*
|
|
68
|
+
* Idempotent: If the connection is not currently gated, the call performs no structural changes
|
|
69
|
+
* (and optionally logs a warning). After ungating, the connection's weight will be used directly
|
|
70
|
+
* without modulation by a gater activation.
|
|
71
|
+
*
|
|
72
|
+
* Complexity: O(n) where n = number of gated connections (indexOf lookup) – typically small.
|
|
73
|
+
*
|
|
74
|
+
* @param this - Bound {@link Network} instance.
|
|
75
|
+
* @param connection - Connection to ungate.
|
|
76
|
+
*/
|
|
77
|
+
export function ungate(this: Network, connection: Connection) {
|
|
78
|
+
/** Index of the connection within the global gates list ( -1 if not found ). */
|
|
79
|
+
const index = this.gates.indexOf(connection);
|
|
80
|
+
if (index === -1) {
|
|
81
|
+
if (config.warnings)
|
|
82
|
+
console.warn('Attempted to ungate a connection not in the gates list.');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.gates.splice(index, 1); // Remove from global gated list.
|
|
86
|
+
connection.gater?.ungate(connection); // Remove reverse reference from the gater node.
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Remove a hidden node from the network while attempting to preserve functional connectivity.
|
|
91
|
+
*
|
|
92
|
+
* Algorithm outline:
|
|
93
|
+
* 1. Reject removal if node is input/output (structural invariants) or absent (error).
|
|
94
|
+
* 2. Optionally collect gating nodes (if keep_gates flag) from inbound & outbound connections.
|
|
95
|
+
* 3. Remove self-loop (if present) to simplify subsequent edge handling.
|
|
96
|
+
* 4. Disconnect all inbound edges (record their source nodes) and all outbound edges (record targets).
|
|
97
|
+
* 5. For every (input predecessor, output successor) pair create a new connection unless:
|
|
98
|
+
* a. input === output (avoid trivial self loops) OR
|
|
99
|
+
* b. an existing projection already connects them.
|
|
100
|
+
* 6. Reassign preserved gater nodes randomly onto newly created bridging connections.
|
|
101
|
+
* 7. Ungate any connections that were gated BY this node (where node acted as gater).
|
|
102
|
+
* 8. Remove node from network node list and flag node index cache as dirty.
|
|
103
|
+
*
|
|
104
|
+
* Complexity summary:
|
|
105
|
+
* - Let I = number of inbound edges, O = number of outbound edges.
|
|
106
|
+
* - Disconnect phase: O(I + O)
|
|
107
|
+
* - Bridging phase: O(I * O) connection existence checks (isProjectingTo) + potential additions.
|
|
108
|
+
* - Gater reassignment: O(min(G, newConnections)) where G is number of preserved gaters.
|
|
109
|
+
*
|
|
110
|
+
* Preservation rationale:
|
|
111
|
+
* - Reassigning gaters maintains some of the dynamic modulation capacity that would otherwise
|
|
112
|
+
* be lost, aiding continuity during topology simplification.
|
|
113
|
+
*
|
|
114
|
+
* @param this - Bound {@link Network} instance.
|
|
115
|
+
* @param node - Hidden node to remove.
|
|
116
|
+
* @throws If node is input/output or not present in network.
|
|
117
|
+
*/
|
|
118
|
+
export function removeNode(this: Network, node: Node) {
|
|
119
|
+
if (node.type === 'input' || node.type === 'output')
|
|
120
|
+
throw new Error('Cannot remove input or output node from the network.');
|
|
121
|
+
const idx = this.nodes.indexOf(node);
|
|
122
|
+
if (idx === -1) throw new Error('Node not found in the network for removal.');
|
|
123
|
+
|
|
124
|
+
// Collected gating nodes to potentially reattach to new bridging connections.
|
|
125
|
+
/** Collection of gater nodes preserved for reassignment onto new bridging connections. */
|
|
126
|
+
const gaters: Node[] = [];
|
|
127
|
+
|
|
128
|
+
// Remove self-loop first (simplifies later logic and ensures gating removal handled early).
|
|
129
|
+
this.disconnect(node, node);
|
|
130
|
+
|
|
131
|
+
// Gather inbound source nodes and optionally preserve their gaters.
|
|
132
|
+
/** List of source nodes feeding into the node being removed (predecessors). */
|
|
133
|
+
const inputs: Node[] = [];
|
|
134
|
+
for (let i = node.connections.in.length - 1; i >= 0; i--) {
|
|
135
|
+
const c = node.connections.in[i];
|
|
136
|
+
if (mutation.SUB_NODE.keep_gates && c.gater && c.gater !== node)
|
|
137
|
+
gaters.push(c.gater);
|
|
138
|
+
inputs.push(c.from);
|
|
139
|
+
this.disconnect(c.from, node);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Gather outbound destination nodes similarly.
|
|
143
|
+
/** List of destination nodes the node being removed projects to (successors). */
|
|
144
|
+
const outputs: Node[] = [];
|
|
145
|
+
for (let i = node.connections.out.length - 1; i >= 0; i--) {
|
|
146
|
+
const c = node.connections.out[i];
|
|
147
|
+
if (mutation.SUB_NODE.keep_gates && c.gater && c.gater !== node)
|
|
148
|
+
gaters.push(c.gater);
|
|
149
|
+
outputs.push(c.to);
|
|
150
|
+
this.disconnect(node, c.to);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Create bridging connections between every predecessor and successor (if not already connected).
|
|
154
|
+
/** New bridging connections created to preserve path connectivity after removal. */
|
|
155
|
+
const newConns: Connection[] = [];
|
|
156
|
+
for (const input of inputs) {
|
|
157
|
+
for (const output of outputs) {
|
|
158
|
+
// Skip trivial self-loop & skip if an existing connection already links them.
|
|
159
|
+
if (input !== output && !input.isProjectingTo(output)) {
|
|
160
|
+
const conn = this.connect(input, output);
|
|
161
|
+
if (conn.length) newConns.push(conn[0]); // Only record created connection
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Reassign preserved gaters randomly to newly formed bridging connections.
|
|
167
|
+
for (const g of gaters) {
|
|
168
|
+
if (!newConns.length) break; // No more candidate connections
|
|
169
|
+
/** Random index into the remaining pool of new bridging connections for gater reassignment. */
|
|
170
|
+
const ci = Math.floor(Math.random() * newConns.length);
|
|
171
|
+
this.gate(g, newConns[ci]);
|
|
172
|
+
newConns.splice(ci, 1); // Avoid double‑gating same connection
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Ungate connections that were gated by the removed node itself.
|
|
176
|
+
for (let i = node.connections.gated.length - 1; i >= 0; i--) {
|
|
177
|
+
this.ungate(node.connections.gated[i]);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Final removal & cache invalidation (indices may be used by fast lookup structures elsewhere).
|
|
181
|
+
this.nodes.splice(idx, 1);
|
|
182
|
+
(this as any)._nodeIndexDirty = true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Only functions exported; keep module shape predictable for tree-shaking / documentation tooling.
|
|
186
|
+
export {};
|