Refactoring

This commit is contained in:
Pascal Serrarens 2025-11-27 17:35:51 +01:00
parent 1771ab7d23
commit 7ce787f5db
9 changed files with 229 additions and 74 deletions

View File

@ -18,3 +18,6 @@ csharp_new_line_between_query_expression_clauses = true
# Limit the number of characters in a line # Limit the number of characters in a line
max_line_length = 100 # This setting does not enforce it; it's a guideline. max_line_length = 100 # This setting does not enforce it; it's a guideline.
[*.{cs,vb}]
dotnet_diagnostic.IDE1006.severity = none

View File

@ -52,6 +52,7 @@
<Compile Include="Assets/NanoBrain/NeuroidBehaviour.cs" /> <Compile Include="Assets/NanoBrain/NeuroidBehaviour.cs" />
<Compile Include="Assets/NanoBrain/SensoryNeuroid.cs" /> <Compile Include="Assets/NanoBrain/SensoryNeuroid.cs" />
<Compile Include="Assets/Scenes/Boids/Scripts/SwarmControl.cs" /> <Compile Include="Assets/Scenes/Boids/Scripts/SwarmControl.cs" />
<Compile Include="Assets/NanoBrain/Perception.cs" />
<Compile Include="Assets/Scenes/Boids/Scripts/Boid.cs" /> <Compile Include="Assets/Scenes/Boids/Scripts/Boid.cs" />
<Compile Include="Assets/NanoBrain/Neuroid.cs" /> <Compile Include="Assets/NanoBrain/Neuroid.cs" />
</ItemGroup> </ItemGroup>

View File

@ -43,8 +43,9 @@ public class GraphEditorWindow : EditorWindow {
} }
// If this neuroid is not visited while its output neuroid is visited // If this neuroid is not visited while its output neuroid is visited
if (!neuronVisited.Contains(neuroid) && (neuroid.outputNeuroid == null || // Note: this does not yet work for multiple outputs yet (see the use of First())
(neuronVisited.Contains(neuroid.outputNeuroid) && neuroid.outputNeuroid.layerIx == layerIx - 1))) { if (!neuronVisited.Contains(neuroid) && (neuroid.outputNeuroids.Count == 0 ||
(neuronVisited.Contains(neuroid.outputNeuroids.First()) && neuroid.outputNeuroids.First().layerIx == layerIx - 1))) {
// Add it to the next layer // Add it to the next layer
currentLayer.neuroids.Add(neuroid); currentLayer.neuroids.Add(neuroid);
neuroid.layerIx = layerIx; neuroid.layerIx = layerIx;
@ -54,7 +55,7 @@ public class GraphEditorWindow : EditorWindow {
Vector2Int neuroidPosition = new(layerIx, neuroidIx); Vector2Int neuroidPosition = new(layerIx, neuroidIx);
neuroidPositions[neuroid] = neuroidPosition; neuroidPositions[neuroid] = neuroidPosition;
neuroidIx++; neuroidIx++;
Debug.Log($"Layer {layerIx} neuron {neuroidIx} id {neuroid.id} {neuroid.name}"); Debug.Log($"Layer {layerIx} neuron {neuroidIx} name {neuroid.name}");
} }
} }
@ -110,9 +111,9 @@ public class GraphEditorWindow : EditorWindow {
Vector3 parentPos = new(100 + layerNeuroidPos.x * 100, margin + layerNeuroidPos.y * spacing, 0.1f); Vector3 parentPos = new(100 + layerNeuroidPos.x * 100, margin + layerNeuroidPos.y * spacing, 0.1f);
int i = 0; int i = 0;
float inputSpacing = 200f / layerNeuroid.synapses.Count; float inputSpacing = 200f / layerNeuroid.newSynapses.Count;
float inputMargin = 100 + inputSpacing / 2; float inputMargin = 100 + inputSpacing / 2;
foreach (Synapse synapse in layerNeuroid.synapses.Values) { foreach (Synapse synapse in layerNeuroid.newSynapses.Values) {
if (synapse.neuroid != null) { if (synapse.neuroid != null) {
if (this.neuroidPositions.ContainsKey(synapse.neuroid)) { if (this.neuroidPositions.ContainsKey(synapse.neuroid)) {
@ -144,7 +145,7 @@ public class GraphEditorWindow : EditorWindow {
// Draw the tooltip // Draw the tooltip
GUIContent tooltip = new( GUIContent tooltip = new(
$"{neuroid.name}" + $"{neuroid.name}" +
$"\nsynapse count {neuroid.synapses.Count}" + $"\nsynapse count {neuroid.newSynapses.Count}" +
$"\nValue: {neuroid.outputValue}" + $"\nValue: {neuroid.outputValue}" +
$"\nStale: {neuroid.stale}"); $"\nStale: {neuroid.stale}");

View File

@ -29,17 +29,16 @@ public class NeuroidNetwork {
} }
public class Neuroid { public class Neuroid {
public int id; //public int id;
public string name; public string name;
public int layerIx; public int layerIx;
public int stale = 0; public int stale = 0;
public readonly Dictionary<int, Synapse> synapses = new(); public readonly Dictionary<Neuroid, Synapse> newSynapses = new();
public Vector3 outputValue; public Vector3 outputValue;
public Neuroid outputNeuroid; public HashSet<Neuroid> outputNeuroids = new();
public int outputNeurix;
public enum Mode { public enum Mode {
Sum, Sum,
@ -52,69 +51,95 @@ public class Neuroid {
public Neuroid(NeuroidNetwork net) { public Neuroid(NeuroidNetwork net) {
this.net = net; this.net = net;
if (this.net != null)
this.net.neuroids.Add(this); this.net.neuroids.Add(this);
} }
public void SetOutputTo(Neuroid neuroid) { public void AddSynapse(Neuroid input) {
this.outputNeuroid = neuroid; input.AddReceiver(this);
// neuroid.inputNeuroids.Add(this); this.newSynapses[input] = new(input, Vector3.zero, 1.0f);
this.outputNeurix = this.id; }
public void AddReceiver(Neuroid receiver) {
this.outputNeuroids.Add(receiver);
} }
public void ResetWeights() { public void ResetWeights() {
foreach (Synapse synapse in synapses.Values) foreach (Synapse synapse in this.newSynapses.Values)
synapse.weight = 1.0f; synapse.weight = 1.0f;
} }
public void SetWeight(Neuroid input, float weight) { public void SetWeight(Neuroid input, float weight) {
if (synapses.ContainsKey(input.id)) if (this.newSynapses.ContainsKey(input)) {
synapses[input.id] = new(input, synapses[input.id].value, weight); this.newSynapses[input].weight = weight;
else }
synapses[input.id] = new(input, Vector3.zero, weight); else {
this.newSynapses[input] = new(input, Vector3.zero, weight);
}
} }
public void GetInputFrom(Neuroid input, float weight = 1.0f) { public void GetInputFrom(Neuroid input, float weight = 1.0f) {
input.id = this.synapses.Count; input.AddReceiver(this);
input.SetOutputTo(this); this.newSynapses[input] = new(input, Vector3.zero, weight);
synapses[input.id] = new(input, Vector3.zero, weight);
} }
public void SetInput(int inputId, Vector3 value) { public void SetInput(Neuroid input, Vector3 value) {
if (synapses.ContainsKey(inputId)) if (this.newSynapses.ContainsKey(input)) {
synapses[inputId].value = value; Synapse synapse = this.newSynapses[input];
synapse.value = value;
}
else else
synapses[inputId] = new(null, value, 1.0f); this.newSynapses[input] = new(null, value, 1.0f);
UpdateState(); UpdateState();
} }
public void SetInput(int inputIx, Vector3 value, float weight) {
if (synapses.ContainsKey(inputIx)) { public void SetInput(Neuroid input, Vector3 value, float weight) {
Synapse synapse = synapses[inputIx]; if (this.newSynapses.ContainsKey(input)) {
Synapse synapse = this.newSynapses[input];
synapse.value = value; synapse.value = value;
synapse.weight = weight; synapse.weight = weight;
} }
else else
synapses[inputIx] = new(null, value, weight); this.newSynapses[input] = new(null, value, weight);
UpdateState(); UpdateState();
} }
void UpdateState() { public readonly Dictionary<int, Neuroid> fakeNeuroids = new();
public void SetInput(int thingId, Vector3 value, float weight, NeuroidNetwork net) {
if (fakeNeuroids.ContainsKey(thingId)) {
Neuroid fakeInput = fakeNeuroids[thingId];
Synapse synapse = this.newSynapses[fakeInput];
synapse.value = value;
synapse.weight = weight;
}
else {
fakeNeuroids[thingId] = new(net);
this.newSynapses[fakeNeuroids[thingId]] = new (null, value, weight);
}
UpdateState();
}
protected virtual void UpdateState() {
Vector3 sum = Vector3.zero; Vector3 sum = Vector3.zero;
foreach (Synapse synapse in synapses.Values) foreach (Synapse synapse in this.newSynapses.Values)
sum += synapse.value * synapse.weight; sum += synapse.value * synapse.weight;
this.outputValue = Activation(sum); this.outputValue = Activation(sum);
this.outputNeuroid?.SetInput(this.outputNeurix, this.outputValue); foreach (Neuroid neuroid in outputNeuroids) {
neuroid?.SetInput(this, this.outputValue);
}
this.stale = 0; this.stale = 0;
} }
Vector3 Activation(Vector3 sum) { Vector3 Activation(Vector3 sum) {
if (synapses.Count == 0 && mode == Mode.Average) if (this.newSynapses.Count == 0 && mode == Mode.Average)
Debug.LogWarning($"{this.id} {this.name} has zero synapses for average"); Debug.LogWarning($"{this.name} has zero synapses for average");
if (float.IsNaN(sum.magnitude)) if (float.IsNaN(sum.magnitude))
Debug.LogWarning($"{this.id} {this.name} sum is nan"); Debug.LogWarning($"{this.name} sum is nan");
return mode switch { return mode switch {
Mode.Sum => sum, Mode.Sum => sum,
Mode.Average => sum / synapses.Count, Mode.Average => sum / this.newSynapses.Count,
_ => sum, _ => sum,
}; };
//return sum; //(sum.magnitude > 0.5f) ? sum : Vector3.zero; //return sum; //(sum.magnitude > 0.5f) ? sum : Vector3.zero;

View File

@ -0,0 +1,83 @@
using System.Collections.Generic;
using UnityEngine;
public class Perception {
public SensoryNeuroid[] sensoryNeuroids = new SensoryNeuroid[7];
//public Neuroid[] velocitySensors = new Neuroid[7];
public NeuroidNetwork neuroidNet { get; protected set; }
public HashSet<Neuroid> receivers { get; protected set; }
public Perception(NeuroidNetwork neuroidNet) {
this.neuroidNet = neuroidNet;
this.receivers = new();
}
// public void SendOutputTo(Neuroid receiver) {
// foreach (SensoryNeuroid neuroid in sensoryNeuroids) {
// if (neuroid != null) {
// neuroid.AddReceiver(receiver);
// receiver.newSynapses[neuroid] = new (neuroid, Vector3.zero, 1.0f);
// }
// }
// }
public void SendPositions(Neuroid receiver) {
receivers.Add(receiver);
foreach (SensoryNeuroid neuroid in sensoryNeuroids) {
if (neuroid != null) {
neuroid.AddReceiver(receiver);
receiver.newSynapses[neuroid] = new (neuroid, Vector3.zero, 1.0f);
}
}
}
public void SendVelocities(Neuroid receiver) {
receivers.Add(receiver);
foreach (SensoryNeuroid neuroid in sensoryNeuroids) {
if (neuroid != null && neuroid.velocityNeuroid != null) {
neuroid.velocityNeuroid.AddReceiver(receiver);
receiver.newSynapses[neuroid] = new (neuroid, Vector3.zero, 1.0f);
}
}
}
public void ProcessStimulus(int thingId, Vector3 localPosition) {
int availableIx = -1;
SensoryNeuroid leastInterestingNeuroid = null;
for (int i = 0; i < sensoryNeuroids.Length; i++) {
if (sensoryNeuroids[i] == null || sensoryNeuroids[i].IsStale())
availableIx = i;
else if (sensoryNeuroids[i].receptor.thingId == thingId) {
sensoryNeuroids[i].receptor.position = localPosition;
return;
}
if (sensoryNeuroids[i] != null) {
if (leastInterestingNeuroid == null || leastInterestingNeuroid.receptor.position.magnitude > sensoryNeuroids[i].receptor.position.magnitude)
leastInterestingNeuroid = sensoryNeuroids[i];
}
}
if (availableIx != -1) {
if (sensoryNeuroids[availableIx] != null) {
// Debug.Log($"revived receptor {availableIx} for {thingId}");
sensoryNeuroids[availableIx].receptor.thingId = thingId;
sensoryNeuroids[availableIx].receptor.position = localPosition;
}
else {
// Debug.Log($"new receptor for {thingId}");
SensoryNeuroid neuroid = new(neuroidNet, thingId);
foreach (Neuroid receiver in receivers)
receiver.GetInputFrom(neuroid);
sensoryNeuroids[availableIx] = neuroid;
neuroid.receptor.position = localPosition;
}
}
else if (leastInterestingNeuroid != null) {
//Debug.Log($"replaced receptor {leastInterestingNeuroid.thingId} for {thingId}");
leastInterestingNeuroid.receptor.thingId = thingId;
leastInterestingNeuroid.receptor.position = localPosition;
}
//Debug.LogWarning($"No available receptor for {id}");
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 37d94d399d30e6eb996236adabad87ee

View File

@ -1,32 +1,70 @@
using System.Linq;
using UnityEngine; using UnityEngine;
public class SensoryNeuroid : Neuroid {
// A neuroid which has no neurons as input
// But receives value from a receptor
public Receptor receptor;
public VelocityNeuroid velocityNeuroid;
public SensoryNeuroid(NeuroidNetwork net, int thingId) : base(net) {
this.name = "sensory neuroid";
this.receptor = new Receptor {
neuroid = this,
thingId = thingId
};
this.velocityNeuroid = new(net);
// The velocity neuroid received position data from this
this.AddReceiver(velocityNeuroid);
}
}
public class Receptor { public class Receptor {
public SensoryNeuroid neuroid; public SensoryNeuroid neuroid;
public void SetValue(Vector3 value) {
if (neuroid != null) { public int thingId;
neuroid.SetInput(neuroid.id, value); /// <summary>
} /// Local position of the thing
} /// </summary>
public Vector3 GetValue() { public virtual Vector3 position {
get {
if (neuroid != null) if (neuroid != null)
return neuroid.synapses[neuroid.id].value; return neuroid.newSynapses[neuroid].value;
else else
return Vector3.zero; return Vector3.zero;
} }
set {
if (neuroid != null)
neuroid.SetInput(neuroid, value);
}
}
} }
public class VelocityNeuroid : Neuroid {
// Would be best if this was received through a synapse via a loop....
private Vector3 lastPosition = Vector3.zero;
private float lastValueTime = 0;
public class SensoryNeuroid : Neuroid { public VelocityNeuroid(NeuroidNetwork net) : base(net) {
public Receptor receptor;
public int thingId;
public SensoryNeuroid(NeuroidNetwork net, int id) : base(net) {
this.name = "sensory neuroid";
// this.id = id;
this.thingId = id;
this.receptor = new Receptor {
neuroid = this
};
} }
protected override void UpdateState() {
// Assuming only one synapse for now....
Vector3 currentPosition = this.newSynapses.First().Value.value;
float currentValueTime = Time.time;
float deltaTime = currentValueTime - lastValueTime;
Vector3 translation = currentPosition - lastPosition;
Vector3 velocity = translation / deltaTime;
// No activation function...
this.outputValue = velocity;
foreach (Neuroid receiver in outputNeuroids)
receiver?.SetInput(this, this.outputValue);
this.stale = 0;
this.lastValueTime = Time.time;
}
} }

View File

@ -378,7 +378,7 @@ MonoBehaviour:
separationForce: 5 separationForce: 5
separationDistance: 0.3 separationDistance: 0.3
bodyForce: 20 bodyForce: 20
perceptionDistance: 2 perceptionDistance: 1
boundaryForce: 5 boundaryForce: 5
spaceSize: {x: 10, y: 10, z: 10} spaceSize: {x: 10, y: 10, z: 10}
boundaryWidth: {x: 1, y: 1, z: 1} boundaryWidth: {x: 1, y: 1, z: 1}
@ -394,7 +394,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: ec888ca5333d45a438f9f417fa5ce135, type: 3} m_Script: {fileID: 11500000, guid: ec888ca5333d45a438f9f417fa5ce135, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: Assembly-CSharp::SwarmSpawn m_EditorClassIdentifier: Assembly-CSharp::SwarmSpawn
count: 1000 count: 5
boidPrefab: {fileID: 8702527964058765413, guid: f9c706268554ce449a8773675b2864b8, type: 3} boidPrefab: {fileID: 8702527964058765413, guid: f9c706268554ce449a8773675b2864b8, type: 3}
spawnAreaSize: {x: 0.5, y: 0.5, z: 0.5} spawnAreaSize: {x: 0.5, y: 0.5, z: 0.5}
minDelay: 0.05 minDelay: 0.05

View File

@ -19,7 +19,8 @@ public class Boid : MonoBehaviour {
readonly Collider[] results = new Collider[10]; readonly Collider[] results = new Collider[10];
public SensoryNeuroid[] neighbourSensor = new SensoryNeuroid[6]; //public SensoryNeuroid[] neighbourSensor = new SensoryNeuroid[6];
public Perception perception;
public NeuroidNetwork neuroidNet = new(); public NeuroidNetwork neuroidNet = new();
public Neuroid bodyVector; public Neuroid bodyVector;
@ -40,11 +41,14 @@ public class Boid : MonoBehaviour {
bounds = new(sc.transform.position, sc.spaceSize - 2 * sc.boundaryWidth); bounds = new(sc.transform.position, sc.spaceSize - 2 * sc.boundaryWidth);
perception = new Perception(neuroidNet);
//neighbourSensor = new(neuroidNet) { name = "Neighbour", id = 879 }; //neighbourSensor = new(neuroidNet) { name = "Neighbour", id = 879 };
cohesion = new(neuroidNet) { name = "Cohesion", mode = Neuroid.Mode.Sum }; cohesion = new(neuroidNet) { name = "Cohesion", mode = Neuroid.Mode.Sum };
perception.SendPositions(cohesion);
//cohesion.GetInputFrom(neighbourSensor); //cohesion.GetInputFrom(neighbourSensor);
alignment = new(neuroidNet) { name = "Alignment", mode = Neuroid.Mode.Average }; alignment = new(neuroidNet) { name = "Alignment", mode = Neuroid.Mode.Average };
//perception.SendVelocities(alignment);
separation = new(neuroidNet) { name = "Separation", mode = Neuroid.Mode.Sum }; separation = new(neuroidNet) { name = "Separation", mode = Neuroid.Mode.Sum };
target = new(neuroidNet) { name = "Target", mode = Neuroid.Mode.Sum }; target = new(neuroidNet) { name = "Target", mode = Neuroid.Mode.Sum };
boundary = new(neuroidNet) { name = "Boundary", mode = Neuroid.Mode.Sum }; boundary = new(neuroidNet) { name = "Boundary", mode = Neuroid.Mode.Sum };
@ -78,15 +82,15 @@ public class Boid : MonoBehaviour {
Vector3 localPosition = neighbour.transform.position - this.transform.position; Vector3 localPosition = neighbour.transform.position - this.transform.position;
Vector3 relativeVelocity = neighbour.velocity - this.velocity; Vector3 relativeVelocity = neighbour.velocity - this.velocity;
int id = neighbour.GetInstanceID(); int thingId = neighbour.GetInstanceID();
ProcessStimulus(id, localPosition); perception.ProcessStimulus(thingId, localPosition);
Vector3 separationForce = -localPosition / localPosition.sqrMagnitude; Vector3 separationForce = -localPosition / localPosition.sqrMagnitude;
// which is equivalent to -(localPosition.normalized / localPosition.magnitude) // which is equivalent to -(localPosition.normalized / localPosition.magnitude)
separation.SetInput(id, separationForce, sc.separationDistance); separation.SetInput(thingId, separationForce, sc.separationDistance, neuroidNet);
//cohesion.SetInput(id, localPosition, sc.cohesionForce); //cohesion.SetInput(thingId, localPosition, sc.cohesionForce);
alignment.SetInput(id, relativeVelocity, sc.alignmentForce); alignment.SetInput(thingId, relativeVelocity, sc.alignmentForce, neuroidNet);
neighbourCount++; neighbourCount++;
} }
@ -108,7 +112,7 @@ public class Boid : MonoBehaviour {
Vector3 direction = (sc.transform.position - this.transform.position).normalized; Vector3 direction = (sc.transform.position - this.transform.position).normalized;
outside = direction * magnitude; outside = direction * magnitude;
boundary.SetInput(id, outside, sc.boundaryForce); boundary.SetInput(id, outside, sc.boundaryForce, neuroidNet);
// Debug.Log($"boundary {this.transform.position} {outside} force = {outside * sc.boundaryForce}"); // Debug.Log($"boundary {this.transform.position} {outside} force = {outside * sc.boundaryForce}");
} }
@ -128,7 +132,7 @@ public class Boid : MonoBehaviour {
//Debug.Log($"neighbours: {neighbourCount} synapses: {cohesion.synapses.Count}"); //Debug.Log($"neighbours: {neighbourCount} synapses: {cohesion.synapses.Count}");
neuroidNet.Update(); neuroidNet.Update();
} }
/*
Receptor GetReceptor(Neuroid perceptionNeuroid, int id) { Receptor GetReceptor(Neuroid perceptionNeuroid, int id) {
int availableIx = -1; int availableIx = -1;
for (int i = 0; i < neighbourSensor.Length; i++) { for (int i = 0; i < neighbourSensor.Length; i++) {
@ -157,7 +161,7 @@ public class Boid : MonoBehaviour {
} }
/*
void ProcessStimulus(int thingId, Vector3 value) { void ProcessStimulus(int thingId, Vector3 value) {
int availableIx = -1; int availableIx = -1;
SensoryNeuroid leastInterestingNeuroid = null; SensoryNeuroid leastInterestingNeuroid = null;
@ -168,8 +172,6 @@ public class Boid : MonoBehaviour {
neighbourSensor[i].receptor.SetValue(value); neighbourSensor[i].receptor.SetValue(value);
return; return;
} }
// if (leastInterestingIx == -1 || neighbourSensor[leastInterestingIx].receptor.GetValue().magnitude > neighbourSensor[i].receptor.GetValue().magnitude)
// leastInterestingIx = i;
if (neighbourSensor[i] != null) { if (neighbourSensor[i] != null) {
if (leastInterestingNeuroid == null || leastInterestingNeuroid.receptor.GetValue().magnitude > neighbourSensor[i].receptor.GetValue().magnitude) if (leastInterestingNeuroid == null || leastInterestingNeuroid.receptor.GetValue().magnitude > neighbourSensor[i].receptor.GetValue().magnitude)
leastInterestingNeuroid = neighbourSensor[i]; leastInterestingNeuroid = neighbourSensor[i];
@ -197,7 +199,7 @@ public class Boid : MonoBehaviour {
//Debug.LogWarning($"No available receptor for {id}"); //Debug.LogWarning($"No available receptor for {id}");
} }
*/
void OnDrawGizmosSelected() { void OnDrawGizmosSelected() {
Gizmos.DrawWireSphere(transform.position, sc.perceptionDistance); Gizmos.DrawWireSphere(transform.position, sc.perceptionDistance);
} }