Repository: stSoftwareAU/NEAT-AI
Pull request #617: V0.155.21
Author: login: nleck
Created: 2025-03-27T06:47:41Z | Merged: 2025-03-27T07:50:43Z



Diff:
diff --git a/deno.json b/deno.json
index 991caf163..4546c408f 100644
--- a/deno.json
+++ b/deno.json
@@ -1,7 +1,7 @@
 {
   "name": "@stsoftware/neat-ai",
   "exports": "./mod.ts",
-  "version": "0.155.20",
+  "version": "0.155.21",
   "imports": {
     "@std/assert": "jsr:@std/assert@^1.0.8",
     "@std/bytes": "jsr:@std/bytes@^1.0.4",
diff --git a/docs/cspell.json b/docs/cspell.json
index b21e82dc3..744a7d676 100644
--- a/docs/cspell.json
+++ b/docs/cspell.json
@@ -31,7 +31,12 @@
     "NARX",
     "ISRU",
     "Softplus",
-    "analyzing"
+    "analyzing",
+    "neuroevolution",
+    "analyzes",
+    "Miikkulainen",
+    "Floreano",
+    "Mattiussi"
   ],
   "flagWords": [
     "hte"
diff --git a/src/architecture/ErrorGuidedStructuralEvolution/DiscoverDirectory.ts b/src/architecture/ErrorGuidedStructuralEvolution/DiscoverDirectory.ts
index 31c7111a0..1748bbdae 100644
--- a/src/architecture/ErrorGuidedStructuralEvolution/DiscoverDirectory.ts
+++ b/src/architecture/ErrorGuidedStructuralEvolution/DiscoverDirectory.ts
@@ -24,8 +24,6 @@ class DataRecorder {
   private discoveryBatchSize: number;
   private ID: string;
   private timeoutTS: number;
-  private disableShuffle: boolean;
-
   constructor(
     private creature: Creature,
     private options: NeatOptions,
@@ -48,8 +46,6 @@ class DataRecorder {
     this.timeoutTS = options.discoveryTimeOutMinutes
       ? Date.now() + options.discoveryTimeOutMinutes * 60 * 1000
       : 0;
-
-    this.disableShuffle = options.discoveryDisableShuffle || false;
   }
 
   private shuffleFiles(files: string[]): string[] {
@@ -67,9 +63,7 @@ class DataRecorder {
         binaryFiles.push(`${dataDir}/${dirEntry.name}`);
       }
     }
-    if (this.disableShuffle) {
-      return binaryFiles;
-    }
+
     return this.shuffleFiles(binaryFiles);
   }
 
@@ -113,9 +107,7 @@ class DataRecorder {
       const sampleSize = Math.ceil(fileRecords * this.sampleRate);
 
       const tmpIndexes = Int32Array.from({ length: fileRecords }, (_, i) => i);
-      if (!this.disableShuffle) {
-        CreatureUtil.shuffle(tmpIndexes);
-      }
+      CreatureUtil.shuffle(tmpIndexes);
       const recordSet = new Set(tmpIndexes.slice(0, sampleSize));
 
       const batchBuffer = new Uint8Array(
@@ -242,27 +234,30 @@ class DataRecorder {
           }`,
         );
       }
-      const focusUUID = creature.neurons[
-        creature.neurons.length - 1 -
-        Math.floor(creature.output * Math.random())
-      ].uuid;
-
-      const analyzeStartTime = Date.now();
-
-      const enhanced = await discoverStructure.analyze(focusUUID);
-      if (options.log) {
-        const analyzeTime = Date.now() - analyzeStartTime;
-        console.log(
-          `Discovery ${blue(this.ID)} analyze time ${
-            yellow(format(analyzeTime, { ignoreZero: true }))
-          }`,
-        );
-      }
 
-      return {
+      const discoverResult: DiscoverResult = {
         ID: this.ID,
-        enhanced: enhanced ? enhanced.exportJSON() : undefined,
+        enhanced: undefined,
       };
+      const focusUUID = await discoverStructure.selectNeuronWeightedByError();
+      if (focusUUID) {
+        const analyzeStartTime = Date.now();
+
+        const enhanced = await discoverStructure.analyze(focusUUID);
+        if (options.log) {
+          const analyzeTime = Date.now() - analyzeStartTime;
+          console.log(
+            `Discovery ${blue(this.ID)} analyze time ${
+              yellow(format(analyzeTime, { ignoreZero: true }))
+            }`,
+          );
+        }
+        if (enhanced) {
+          discoverResult.enhanced = enhanced.exportJSON();
+        }
+      }
+
+      return discoverResult;
     } finally {
       await discoverStructure.cleanUp();
     }
diff --git a/src/architecture/ErrorGuidedStructuralEvolution/DiscoverStructure.ts b/src/architecture/ErrorGuidedStructuralEvolution/DiscoverStructure.ts
index a98491db6..922cc9287 100644
--- a/src/architecture/ErrorGuidedStructuralEvolution/DiscoverStructure.ts
+++ b/src/architecture/ErrorGuidedStructuralEvolution/DiscoverStructure.ts
@@ -2,12 +2,25 @@ import { assert } from "@std/assert";
 import { parse as parseCsv } from "@std/csv";
 import { Creature } from "../../Creature.ts";
 import type { DataRecordInterface } from "../DataSet.ts";
+/**
+ * Implements Error-Driven Synapse Discovery, a neuroevolution technique for optimizing neural network structures.
+ * This class analyzes neuron activations and back-propagation errors to discover beneficial new synapses
+ * that explicitly reduce neuron-level errors.
+ *
+ * References:
+ * - Stanley, K. O., & Miikkulainen, R. (2002). Evolving Neural Networks through Augmenting Topologies (NEAT).
+ *   Evolutionary Computation, 10(2), 99–127.
+ * - Floreano, D., Dürr, P., & Mattiussi, C. (2008). Neuroevolution: from architectures to learning. Evolutionary Intelligence, 1(1), 47-62.
+ */
 
 export interface DiscoverRecord {
   activation: number;
   errors: string;
 }
 
+/**
+ * Represents a potential new synapse and the associated metrics calculated during discovery.
+ */
 interface CandidateSynapse {
   fromNeuronUUID: string;
   toNeuronUUID: string;
@@ -18,6 +31,14 @@ interface CandidateSynapse {
   totalCount: number;
 }
 
+/**
+ * Represents a neuron and its total accumulated error for ranking neurons during discovery.
+ */
+interface NeuronErrorInfo {
+  uuid: string;
+  totalError: number;
+}
+
 /**
  * Implements Error-Driven Synapse Discovery, analyzing neuron activations
  * and errors to identify beneficial new synapses that explicitly reduce neuron-level errors.
@@ -39,28 +60,37 @@ export class DiscoverStructure {
     Deno.mkdirSync(this.tempDir, { recursive: true });
   }
 
+  /**
+   * Initializes the discovery process by preparing temporary storage for neuron data.
+   */
   public initialize(neuronPromisesMap: Map<string, Promise<void>>) {
     assert(!this.initialized, "Already initialized");
     this.initialized = true;
 
     this.creature.neurons.forEach((neuron) => {
-      let headCSV = "activation\n";
-      if (neuron.type !== "input") {
-        headCSV = "activation,errors\n";
-      }
+      const headCSV = neuron.type !== "input"
+        ? "activation,errors\n"
+        : "activation\n";
+      const filePath = `${this.tempDir}/${neuron.uuid}.csv`;
+
+      const writePromise = Deno.writeTextFile(filePath, headCSV, {
+        append: false,
+        createNew: true,
+      }).catch((e) => {
+        console.error(
+          `Failed to initialize CSV for neuron ${neuron.uuid} at ${filePath}`,
+          e,
+        );
+        throw e;
+      });
 
-      const writePromise = Deno.writeTextFile(
-        `${this.tempDir}/${neuron.uuid}.csv`,
-        headCSV,
-        {
-          append: false,
-          createNew: true,
-        },
-      );
       neuronPromisesMap.set(neuron.uuid, writePromise);
     });
   }
 
+  /**
+   * Cleans up temporary resources and resets the internal state after discovery.
+   */
   public async cleanUp() {
     assert(this.initialized, "Not initialized");
     this.initialized = false;
@@ -70,6 +100,9 @@ export class DiscoverStructure {
     await Deno.remove(this.tempDir, { recursive: true });
   }
 
+  /**
+   * Records neuron activations and errors across the provided training data.
+   */
   public record(
     trainingData: DataRecordInterface[],
     neuronPromisesMap: Map<string, Promise<void>>,
@@ -133,6 +166,9 @@ export class DiscoverStructure {
 
   private discoveries: CandidateSynapse[] = [];
 
+  /**
+   * Analyzes recorded neuron data to identify and evaluate potential synapse additions.
+   */
   public async analyze(neuronUUID: string) {
     assert(this.recorded, "Not recorded");
     const records = await this.loadCSV(`${this.tempDir}/${neuronUUID}.csv`);
@@ -184,21 +220,20 @@ export class DiscoverStructure {
   }
 
   private async loadCSV(file: string): Promise<DiscoverRecord[]> {
-    const data = await Deno.readTextFile(file);
     try {
-      const records = parseCsv(data, {
-        skipFirstRow: true,
-      });
-      return records.map((record) => {
-        const activation = Number.parseFloat(record.activation);
+      const data = await Deno.readTextFile(file);
+      const records = parseCsv(data, { skipFirstRow: true });
 
-        const errors = record.errors;
-        return { activation, errors };
+      return records.map((record, idx) => {
+        const activation = Number.parseFloat(record.activation);
+        if (isNaN(activation)) {
+          throw new Error(`Invalid activation at row ${idx + 2} in ${file}`);
+        }
+        return { activation, errors: record.errors };
       });
     } catch (e) {
-      console.error(`File: ${file}`, e);
-      console.info(data);
-      throw e;
+      console.error(`Failed to load or parse CSV file: ${file}`, e);
+      throw e; // re-throw after logging for external handling
     }
   }
 
@@ -228,6 +263,75 @@ export class DiscoverStructure {
     return candidates;
   }
 
+  /**
+   * Lists neurons sorted by their total error, useful for error-driven selection processes.
+   */
+  public async listViableNeurons(): Promise<NeuronErrorInfo[]> {
+    assert(this.recorded, "Must record first before listing neurons.");
+
+    const neuronPromises = this.creature.neurons
+      .filter((neuron) => neuron.type !== "input")
+      .map(async (neuron) => {
+        try {
+          const records = await this.loadCSV(
+            `${this.tempDir}/${neuron.uuid}.csv`,
+          );
+
+          const totalError = records.reduce((sum, record) => {
+            const errors = record.errors.split("|").map(Number);
+            const recordError = errors.reduce(
+              (eSum, e) => eSum + Math.abs(e),
+              0,
+            );
+            return sum + recordError;
+          }, 0);
+
+          return { uuid: neuron.uuid, totalError };
+        } catch (e) {
+          console.error(`Error processing neuron ${neuron.uuid}`, e);
+          return { uuid: neuron.uuid, totalError: 0 }; // Handle gracefully
+        }
+      });
+
+    const neuronErrors = await Promise.all(neuronPromises);
+
+    return neuronErrors
+      .filter((neuron) => neuron.totalError > 0)
+      .sort((a, b) => b.totalError - a.totalError);
+  }
+
+  /**
+   * Selects a neuron randomly, weighted by total error, favoring neurons with higher errors.
+   * Implements "roulette wheel" selection.
+   *
+   * Reference:
+   * - Goldberg, D. E. (1989). Genetic Algorithms in Search, Optimization and Machine Learning.
+   */
+  public async selectNeuronWeightedByError(): Promise<string | undefined> {
+    const neuronErrors = await this.listViableNeurons();
+    if (neuronErrors.length === 0) return undefined;
+
+    const totalErrorSum = neuronErrors.reduce(
+      (sum, n) => sum + n.totalError,
+      0,
+    );
+    const randValue = Math.random() * totalErrorSum;
+
+    let cumulativeError = 0;
+    for (const neuron of neuronErrors) {
+      cumulativeError += neuron.totalError;
+      if (randValue <= cumulativeError) {
+        return neuron.uuid;
+      }
+    }
+
+    // Fallback, though it shouldn't typically reach here
+    return neuronErrors[neuronErrors.length - 1].uuid;
+  }
+
+  /**
+   * Analyzes a candidate synapse by estimating potential error reduction from its addition.
+   */
   async analyzeCandidateSynapse(
     toNeuronUUID: string,
     fromNeuronUUID: string,
diff --git a/src/config/NeatConfig.ts b/src/config/NeatConfig.ts
index 475a0ba5e..391623517 100644
--- a/src/config/NeatConfig.ts
+++ b/src/config/NeatConfig.ts
@@ -93,7 +93,6 @@ export function createNeatConfig(options: NeatOptions) {
     discoveryTimeOutMinutes: options.discoveryTimeOutMinutes || 0,
     discoveryBatchSize: options.discoveryBatchSize || 0,
     discoveryBufferSize: options.discoveryBufferSize || 0,
-    discoveryDisableShuffle: options.discoveryDisableShuffle || false,
   };
   validate(config);
   return Object.freeze(config);
diff --git a/src/config/NeatOptions.ts b/src/config/NeatOptions.ts
index c963e6a3d..d53234c2e 100644
--- a/src/config/NeatOptions.ts
+++ b/src/config/NeatOptions.ts
@@ -141,9 +141,6 @@ export interface NeatArguments {
 
   /** The read buffer size, default 128k */
   discoveryBufferSize: number;
-
-  /** Disable random shuffle of records and files. */
-  discoveryDisableShuffle: boolean;
 }
 
 export type NeatOptions = Partial<NeatArguments>;
diff --git a/test/ErrorGuidedStructuralEvolution/DiscoverStructure.ts b/test/ErrorGuidedStructuralEvolution/DiscoverStructure.ts
index 976e71359..7940eeccb 100644
--- a/test/ErrorGuidedStructuralEvolution/DiscoverStructure.ts
+++ b/test/ErrorGuidedStructuralEvolution/DiscoverStructure.ts
@@ -124,6 +124,27 @@ Deno.test("Error-Driven Synapse Discovery identifies missing synapses both", asy
   );
   assert(input33, "Should have added synapse from input-33");
   assertAlmostEquals(input33?.weight, -0.3, 0.05);
+
+  // New tests for listViableNeurons()
+  const viableNeurons = await discoverStructure.listViableNeurons();
+  assert(viableNeurons.length > 0, "There should be viable neurons listed");
+  // Check descending sort order by total error
+  for (let i = 1; i < viableNeurons.length; i++) {
+    assert(
+      viableNeurons[i - 1].totalError >= viableNeurons[i].totalError,
+      "Viable neurons should be sorted by descending totalError",
+    );
+  }
+
+  // New test for selectNeuronWeightedByError()
+  const selectedNeuronUUID = await discoverStructure
+    .selectNeuronWeightedByError();
+  assert(selectedNeuronUUID, "Should select a neuron UUID");
+  assert(
+    viableNeurons.some((neuron) => neuron.uuid === selectedNeuronUUID),
+    "Selected neuron UUID must be from the viable neurons list",
+  );
+
   await discoverStructure.cleanUp();
 });
 


Reviews:
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T07:36:48Z

Review comments:
diff_hunk: @@ -39,28 +60,37 @@
     Deno.mkdirSync(this.tempDir, { recursive: true });
   }
 
+  /**
+   * Initializes the discovery process by preparing temporary storage for neuron data.
+   */
   public initialize(neuronPromisesMap: Map<string, Promise<void>>) {
     assert(!this.initialized, "Already initialized");
     this.initialized = true;
 
     this.creature.neurons.forEach((neuron) => {
-      let headCSV = "activation\n";
-      if (neuron.type !== "input") {
-        headCSV = "activation,errors\n";
-      }
+      const headCSV = neuron.type !== "input"
+        ? "activation,errors\n"
+        : "activation\n";
+      const filePath = `${this.tempDir}/${neuron.uuid}.csv`;
+
+      const writePromise = Deno.writeTextFile(filePath, headCSV, {
+        append: false,
+        createNew: true,
+      }).catch((e) => {
+        console.error(
+          `Failed to initialize CSV for neuron ${neuron.uuid} at ${filePath}`,
path: src/architecture/ErrorGuidedStructuralEvolution/DiscoverStructure.ts
user: login: github-advanced-security[bot]
body: ## Use of externally-controlled format string

Format string depends on a [user-provided value](1).

[Show more details](https://github.com/stSoftwareAU/NEAT-AI/security/code-scanning/52)
created_at: 2025-03-27T07:36:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -184,21 +220,20 @@
   }
 
   private async loadCSV(file: string): Promise<DiscoverRecord[]> {
-    const data = await Deno.readTextFile(file);
     try {
-      const records = parseCsv(data, {
-        skipFirstRow: true,
-      });
-      return records.map((record) => {
-        const activation = Number.parseFloat(record.activation);
+      const data = await Deno.readTextFile(file);
+      const records = parseCsv(data, { skipFirstRow: true });
 
-        const errors = record.errors;
-        return { activation, errors };
+      return records.map((record, idx) => {
+        const activation = Number.parseFloat(record.activation);
+        if (isNaN(activation)) {
+          throw new Error(`Invalid activation at row ${idx + 2} in ${file}`);
+        }
+        return { activation, errors: record.errors };
       });
     } catch (e) {
-      console.error(`File: ${file}`, e);
-      console.info(data);
-      throw e;
+      console.error(`Failed to load or parse CSV file: ${file}`, e);
path: src/architecture/ErrorGuidedStructuralEvolution/DiscoverStructure.ts
user: login: github-advanced-security[bot]
body: ## Use of externally-controlled format string

Format string depends on a [user-provided value](1).

[Show more details](https://github.com/stSoftwareAU/NEAT-AI/security/code-scanning/53)
created_at: 2025-03-27T07:36:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -228,6 +263,75 @@
     return candidates;
   }
 
+  /**
+   * Lists neurons sorted by their total error, useful for error-driven selection processes.
+   */
+  public async listViableNeurons(): Promise<NeuronErrorInfo[]> {
+    assert(this.recorded, "Must record first before listing neurons.");
+
+    const neuronPromises = this.creature.neurons
+      .filter((neuron) => neuron.type !== "input")
+      .map(async (neuron) => {
+        try {
+          const records = await this.loadCSV(
+            `${this.tempDir}/${neuron.uuid}.csv`,
+          );
+
+          const totalError = records.reduce((sum, record) => {
+            const errors = record.errors.split("|").map(Number);
+            const recordError = errors.reduce(
+              (eSum, e) => eSum + Math.abs(e),
+              0,
+            );
+            return sum + recordError;
+          }, 0);
+
+          return { uuid: neuron.uuid, totalError };
+        } catch (e) {
+          console.error(`Error processing neuron ${neuron.uuid}`, e);
path: src/architecture/ErrorGuidedStructuralEvolution/DiscoverStructure.ts
user: login: github-advanced-security[bot]
body: ## Use of externally-controlled format string

Format string depends on a [user-provided value](1).

[Show more details](https://github.com/stSoftwareAU/NEAT-AI/security/code-scanning/54)
created_at: 2025-03-27T07:36:48Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-27T06:47:56Z
body: <h1>Dependency Review</h1>
✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.<h2>OpenSSF Scorecard</h2>
<table><tr><th>Package</th><th>Version</th><th>Score</th><th>Details</th></tr>
</table><h2>Scanned Files</h2>
<ul>


<!-- dependency-review-pr-comment-marker -->
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-03-27T06:55:00Z
body: ## [Codecov](https://app.codecov.io/gh/stSoftwareAU/NEAT-AI/pull/617?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stSoftwareAU) Report
Attention: Patch coverage is `81.44330%` with `18 lines` in your changes missing coverage. Please review.
> Project coverage is 94.48%. Comparing base [(`3281588`)](https://app.codecov.io/gh/stSoftwareAU/NEAT-AI/commit/32815885c43c89ab2f1e55bc27797a7536a04f62?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stSoftwareAU) to head [(`5d46a03`)](https://app.codecov.io/gh/stSoftwareAU/NEAT-AI/commit/5d46a03a4992155a003e1b92651dd02a6980ed1e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stSoftwareAU).
> Report is 1 commits behind head on Develop.

:white_check_mark: All tests successful. No failed tests found.

| [Files with missing lines](https://app.codecov.io/gh/stSoftwareAU/NEAT-AI/pull/617?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stSoftwareAU) | Patch % | Lines |
|---|---|---|
| [...rrorGuidedStructuralEvolution/DiscoverStructure.ts](https://app.codecov.io/gh/stSoftwareAU/NEAT-AI/pull/617?src=pr&el=tree&filepath=src%2Farchitecture%2FErrorGuidedStructuralEvolution%2FDiscoverStructure.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stSoftwareAU#diff-c3JjL2FyY2hpdGVjdHVyZS9FcnJvckd1aWRlZFN0cnVjdHVyYWxFdm9sdXRpb24vRGlzY292ZXJTdHJ1Y3R1cmUudHM=) | 72.30% | [18 Missing :warning: ](https://app.codecov.io/gh/stSoftwareAU/NEAT-AI/pull/617?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stSoftwareAU) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           Develop     #617      +/-   ##
===========================================
+ Coverage    94.36%   94.48%   +0.12%     
===========================================
  Files          250      250              
  Lines        26426    26477      +51     
  Branches      2341     2359      +18     
===========================================
+ Hits         24936    25017      +81     
+ Misses        1466     1437      -29     
+ Partials        24       23       -1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/stSoftwareAU/NEAT-AI/pull/617?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stSoftwareAU).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=stSoftwareAU).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE