Move more towards pure neuroids

This commit is contained in:
Pascal Serrarens 2025-11-28 17:15:49 +01:00
parent 7ce787f5db
commit fdabad2895
7 changed files with 113 additions and 201 deletions

View File

@ -37,15 +37,19 @@ public class GraphEditorWindow : EditorWindow {
int neuroidIx = 0;
foreach (Neuroid neuroid in neuroids) {
// Skip neurons we already processed
if (neuronVisited.Contains(neuroid))
continue;
if (neuroid.IsStale()) {
neuronVisited.Add(neuroid);
continue;
}
// If this neuroid is not visited while its output neuroid is visited
// If the output neuroid is visited
// Note: this does not yet work for multiple outputs yet (see the use of First())
if (!neuronVisited.Contains(neuroid) && (neuroid.outputNeuroids.Count == 0 ||
(neuronVisited.Contains(neuroid.outputNeuroids.First()) && neuroid.outputNeuroids.First().layerIx == layerIx - 1))) {
if (neuroid.outputNeuroids.Count == 0 // make sure the root neuroids are processed directly
|| (neuronVisited.Contains(neuroid.outputNeuroids.First()) && neuroid.outputNeuroids.First().layerIx == layerIx - 1)) {
// Add it to the next layer
currentLayer.neuroids.Add(neuroid);
neuroid.layerIx = layerIx;
@ -111,9 +115,9 @@ public class GraphEditorWindow : EditorWindow {
Vector3 parentPos = new(100 + layerNeuroidPos.x * 100, margin + layerNeuroidPos.y * spacing, 0.1f);
int i = 0;
float inputSpacing = 200f / layerNeuroid.newSynapses.Count;
float inputSpacing = 200f / layerNeuroid.synapses.Count;
float inputMargin = 100 + inputSpacing / 2;
foreach (Synapse synapse in layerNeuroid.newSynapses.Values) {
foreach (Synapse synapse in layerNeuroid.synapses.Values) {
if (synapse.neuroid != null) {
if (this.neuroidPositions.ContainsKey(synapse.neuroid)) {
@ -145,7 +149,7 @@ public class GraphEditorWindow : EditorWindow {
// Draw the tooltip
GUIContent tooltip = new(
$"{neuroid.name}" +
$"\nsynapse count {neuroid.newSynapses.Count}" +
$"\nsynapse count {neuroid.synapses.Count}" +
$"\nValue: {neuroid.outputValue}" +
$"\nStale: {neuroid.stale}");

View File

@ -16,8 +16,8 @@ public class Synapse {
public class NeuroidNetwork {
public List<Neuroid> neuroids = new();
public Neuroid AddNeuron() {
Neuroid neuroid = new(this);
public Neuroid AddNeuron(string name) {
Neuroid neuroid = new(this, name);
return neuroid;
}
@ -29,35 +29,33 @@ public class NeuroidNetwork {
}
public class Neuroid {
//public int id;
public string name;
public int layerIx;
public int stale = 0;
public readonly Dictionary<Neuroid, Synapse> newSynapses = new();
public readonly Dictionary<Neuroid, Synapse> synapses = new();
public Vector3 outputValue;
public HashSet<Neuroid> outputNeuroids = new();
public enum Mode {
Sum,
Average,
}
public Mode mode = Mode.Sum;
public bool average = false;
//public bool quadratic = false;
public bool inverse = false;
public float exponent = 1.0f;
public NeuroidNetwork net;
public Neuroid(NeuroidNetwork net) {
public Neuroid(NeuroidNetwork net, string name) {
this.net = net;
this.name = name;
if (this.net != null)
this.net.neuroids.Add(this);
}
public void AddSynapse(Neuroid input) {
input.AddReceiver(this);
this.newSynapses[input] = new(input, Vector3.zero, 1.0f);
this.synapses[input] = new(input, Vector3.zero, 1.0f);
}
public void AddReceiver(Neuroid receiver) {
@ -65,84 +63,77 @@ public class Neuroid {
}
public void ResetWeights() {
foreach (Synapse synapse in this.newSynapses.Values)
foreach (Synapse synapse in this.synapses.Values)
synapse.weight = 1.0f;
}
public void SetWeight(Neuroid input, float weight) {
if (this.newSynapses.ContainsKey(input)) {
this.newSynapses[input].weight = weight;
if (this.synapses.ContainsKey(input)) {
this.synapses[input].weight = weight;
}
else {
this.newSynapses[input] = new(input, Vector3.zero, weight);
this.synapses[input] = new(input, Vector3.zero, weight);
}
}
public void GetInputFrom(Neuroid input, float weight = 1.0f) {
input.AddReceiver(this);
this.newSynapses[input] = new(input, Vector3.zero, weight);
this.synapses[input] = new(input, Vector3.zero, weight);
}
public void SetInput(Neuroid input, Vector3 value) {
if (this.newSynapses.ContainsKey(input)) {
Synapse synapse = this.newSynapses[input];
if (this.synapses.ContainsKey(input)) {
Synapse synapse = this.synapses[input];
synapse.value = value;
}
else
this.newSynapses[input] = new(null, value, 1.0f);
this.synapses[input] = new(null, value, 1.0f);
UpdateState();
}
public void SetInput(Neuroid input, Vector3 value, float weight) {
if (this.newSynapses.ContainsKey(input)) {
Synapse synapse = this.newSynapses[input];
if (this.synapses.ContainsKey(input)) {
Synapse synapse = this.synapses[input];
synapse.value = value;
synapse.weight = weight;
}
else
this.newSynapses[input] = new(null, value, weight);
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);
}
this.synapses[input] = new(null, value, weight);
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.synapses[fakeInput];
// synapse.value = value;
// synapse.weight = weight;
// }
// else {
// fakeNeuroids[thingId] = new(net);
// this.synapses[fakeNeuroids[thingId]] = new(null, value, weight);
// }
// UpdateState();
// }
protected virtual void UpdateState() {
Vector3 sum = Vector3.zero;
foreach (Synapse synapse in this.newSynapses.Values)
sum += synapse.value * synapse.weight;
this.outputValue = Activation(sum);
foreach (Neuroid neuroid in outputNeuroids) {
neuroid?.SetInput(this, this.outputValue);
Vector3 result = Vector3.zero;
foreach (Synapse synapse in this.synapses.Values) {
Vector3 direction = synapse.value.normalized;
float magnitude = synapse.value.magnitude;
magnitude = synapse.weight * Mathf.Pow(magnitude, exponent);
if (inverse)
magnitude = 1 / magnitude;
result += direction * magnitude;
}
this.stale = 0;
}
if (average && this.synapses.Count > 0)
result /= this.synapses.Count;
Vector3 Activation(Vector3 sum) {
if (this.newSynapses.Count == 0 && mode == Mode.Average)
Debug.LogWarning($"{this.name} has zero synapses for average");
if (float.IsNaN(sum.magnitude))
Debug.LogWarning($"{this.name} sum is nan");
return mode switch {
Mode.Sum => sum,
Mode.Average => sum / this.newSynapses.Count,
_ => sum,
};
//return sum; //(sum.magnitude > 0.5f) ? sum : Vector3.zero;
this.outputValue = result;
foreach (Neuroid neuroid in outputNeuroids)
neuroid.SetInput(this, this.outputValue);
this.stale = 0;
}
public bool IsStale() {

View File

@ -3,40 +3,33 @@ 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 HashSet<Neuroid> positionReceivers { get; protected set; }
public HashSet<Neuroid> velocityReceivers { get; protected set; }
public Perception(NeuroidNetwork neuroidNet) {
this.neuroidNet = neuroidNet;
this.receivers = new();
this.positionReceivers = new();
this.velocityReceivers = 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);
public void SendPositions(Neuroid receiver, float weight = 1.0f) {
positionReceivers.Add(receiver);
foreach (SensoryNeuroid neuroid in sensoryNeuroids) {
if (neuroid != null) {
neuroid.AddReceiver(receiver);
receiver.newSynapses[neuroid] = new (neuroid, Vector3.zero, 1.0f);
receiver.synapses[neuroid] = new (neuroid, Vector3.zero, weight);
}
}
}
public void SendVelocities(Neuroid receiver) {
receivers.Add(receiver);
velocityReceivers.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);
receiver.synapses[neuroid] = new (neuroid, Vector3.zero, 1.0f);
}
}
}
@ -65,8 +58,10 @@ public class Perception {
else {
// Debug.Log($"new receptor for {thingId}");
SensoryNeuroid neuroid = new(neuroidNet, thingId);
foreach (Neuroid receiver in receivers)
foreach (Neuroid receiver in positionReceivers)
receiver.GetInputFrom(neuroid);
foreach (Neuroid receiver in velocityReceivers)
receiver.GetInputFrom(neuroid.velocityNeuroid);
sensoryNeuroids[availableIx] = neuroid;
neuroid.receptor.position = localPosition;

View File

@ -7,8 +7,7 @@ public class SensoryNeuroid : Neuroid {
public Receptor receptor;
public VelocityNeuroid velocityNeuroid;
public SensoryNeuroid(NeuroidNetwork net, int thingId) : base(net) {
this.name = "sensory neuroid";
public SensoryNeuroid(NeuroidNetwork net, int thingId) : base(net, "sensory neuroid") {
this.receptor = new Receptor {
neuroid = this,
thingId = thingId
@ -31,7 +30,7 @@ public class Receptor {
public virtual Vector3 position {
get {
if (neuroid != null)
return neuroid.newSynapses[neuroid].value;
return neuroid.synapses[neuroid].value;
else
return Vector3.zero;
}
@ -47,12 +46,12 @@ public class VelocityNeuroid : Neuroid {
private Vector3 lastPosition = Vector3.zero;
private float lastValueTime = 0;
public VelocityNeuroid(NeuroidNetwork net) : base(net) {
public VelocityNeuroid(NeuroidNetwork net) : base(net, "Velocity") {
}
protected override void UpdateState() {
// Assuming only one synapse for now....
Vector3 currentPosition = this.newSynapses.First().Value.value;
Vector3 currentPosition = this.synapses.First().Value.value;
float currentValueTime = Time.time;
float deltaTime = currentValueTime - lastValueTime;

View File

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

View File

@ -11,6 +11,8 @@ public class Boid : MonoBehaviour {
public float separationDistance = 0.5f;
public float bodyForce = 1;
public bool debug = false;
public SwarmControl sc;
public Vector3 velocity = Vector3.zero;
public Vector3 acceleration = Vector3.zero;
@ -27,7 +29,7 @@ public class Boid : MonoBehaviour {
public Neuroid cohesion;
public Neuroid alignment;
public Neuroid separation;
public Neuroid target;
// public Neuroid target;
public Neuroid boundary;
public Neuroid totalForce;
@ -42,34 +44,28 @@ public class Boid : MonoBehaviour {
bounds = new(sc.transform.position, sc.spaceSize - 2 * sc.boundaryWidth);
perception = new Perception(neuroidNet);
//neighbourSensor = new(neuroidNet) { name = "Neighbour", id = 879 };
cohesion = new(neuroidNet) { name = "Cohesion", mode = Neuroid.Mode.Sum };
cohesion = new(neuroidNet, "Cohesion");
perception.SendPositions(cohesion);
//cohesion.GetInputFrom(neighbourSensor);
alignment = new(neuroidNet) { name = "Alignment", mode = Neuroid.Mode.Average };
//perception.SendVelocities(alignment);
separation = new(neuroidNet) { name = "Separation", mode = Neuroid.Mode.Sum };
target = new(neuroidNet) { name = "Target", mode = Neuroid.Mode.Sum };
boundary = new(neuroidNet) { name = "Boundary", mode = Neuroid.Mode.Sum };
alignment = new(neuroidNet, "Alignment") { average = true };
perception.SendVelocities(alignment);
separation = new(neuroidNet, "Separation") { inverse = true, exponent = 2 };
perception.SendPositions(separation, sc.separationForce);
boundary = new(neuroidNet, "Boundary");
totalForce = new(neuroidNet) { name = "Total force", mode = Neuroid.Mode.Sum };
totalForce = new(neuroidNet, "Total");
totalForce.GetInputFrom(alignment, sc.alignmentForce);
totalForce.GetInputFrom(cohesion, sc.cohesionForce);
totalForce.GetInputFrom(separation, sc.separationForce);
totalForce.GetInputFrom(target, sc.bodyForce);
totalForce.GetInputFrom(separation, -sc.separationForce);
totalForce.GetInputFrom(boundary, sc.boundaryForce);
}
void Update() {
Physics.OverlapSphereNonAlloc(this.transform.position, sc.perceptionDistance, results);
neighbourCount = 0;
cohesion.ResetWeights();
alignment.ResetWeights();
//separation.ResetWeights();
foreach (Collider c in results) {
if (c == null)
continue;
@ -80,45 +76,40 @@ public class Boid : MonoBehaviour {
continue;
Vector3 localPosition = neighbour.transform.position - this.transform.position;
Vector3 relativeVelocity = neighbour.velocity - this.velocity;
if (debug)
Debug.Log($" distance {localPosition.magnitude}");
int thingId = neighbour.GetInstanceID();
perception.ProcessStimulus(thingId, localPosition);
Vector3 separationForce = -localPosition / localPosition.sqrMagnitude;
// which is equivalent to -(localPosition.normalized / localPosition.magnitude)
separation.SetInput(thingId, separationForce, sc.separationDistance, neuroidNet);
//cohesion.SetInput(thingId, localPosition, sc.cohesionForce);
alignment.SetInput(thingId, relativeVelocity, sc.alignmentForce, neuroidNet);
neighbourCount++;
}
}
//Vector3 spaceLocalPosition = sc.transform.InverseTransformPoint(this.transform.position);
if (!bounds.Contains(this.transform.position)) {
Vector3 point = this.transform.position;
// Vector3 distanceOutside = Vector3.Max(bounds.min - this.transform.position, this.transform.position - bounds.max);
// // Ensure value is > 0 (but isn't this already)
// Vector3 outside = distanceOutside; // Vector3.Max(Vector3.zero, distanceOutside);
// if (!bounds.Contains(this.transform.position)) {
// Vector3 point = this.transform.position;
// // Vector3 distanceOutside = Vector3.Max(bounds.min - this.transform.position, this.transform.position - bounds.max);
// // // Ensure value is > 0 (but isn't this already)
// // Vector3 outside = distanceOutside; // Vector3.Max(Vector3.zero, distanceOutside);
Vector3 below = bounds.min - point; // positive where point < min
Vector3 above = point - bounds.max; // positive where point > max
// Vector3 below = bounds.min - point; // positive where point < min
// Vector3 above = point - bounds.max; // positive where point > max
// outside distances per axis (0 if inside on that axis)
Vector3 outside = Vector3.Max(Vector3.zero, Vector3.Max(below, above));
float magnitude = outside.magnitude;
Vector3 direction = (sc.transform.position - this.transform.position).normalized;
outside = direction * magnitude;
// // outside distances per axis (0 if inside on that axis)
// Vector3 outside = Vector3.Max(Vector3.zero, Vector3.Max(below, above));
// float magnitude = outside.magnitude;
// Vector3 direction = (sc.transform.position - this.transform.position).normalized;
// outside = direction * magnitude;
boundary.SetInput(id, outside, sc.boundaryForce, neuroidNet);
// Debug.Log($"boundary {this.transform.position} {outside} force = {outside * sc.boundaryForce}");
}
// boundary.SetInput(id, outside, sc.boundaryForce, neuroidNet);
// // Debug.Log($"boundary {this.transform.position} {outside} force = {outside * sc.boundaryForce}");
// }
Vector3 totalForceVector = totalForce.outputValue;
//Debug.DrawRay(this.transform.position, totalForceVector, Color.magenta);
if (this.debug) {
Debug.Log($"Cohesion {cohesion.outputValue.magnitude} separation {separation.outputValue.magnitude} alignment {alignment.outputValue.magnitude}");
}
this.velocity = (1 - sc.inertia) * (totalForceVector * Time.deltaTime) + sc.inertia * velocity + (sc.speed * transform.forward);
//this.velocity = Vector3.ClampMagnitude(this.velocity, sc.speed);
@ -132,74 +123,7 @@ public class Boid : MonoBehaviour {
//Debug.Log($"neighbours: {neighbourCount} synapses: {cohesion.synapses.Count}");
neuroidNet.Update();
}
/*
Receptor GetReceptor(Neuroid perceptionNeuroid, int id) {
int availableIx = -1;
for (int i = 0; i < neighbourSensor.Length; i++) {
if (neighbourSensor[i] == null || neighbourSensor[i].IsStale())
availableIx = i;
else if (neighbourSensor[i].thingId == id)
return neighbourSensor[i].receptor;
}
if (availableIx != -1) {
if (neighbourSensor[availableIx] != null) {
Debug.Log($"revived receptor {availableIx} for {id}");
neighbourSensor[availableIx].thingId = id;
return neighbourSensor[availableIx].receptor;
}
else {
Debug.Log($"new receptor for {id}");
SensoryNeuroid neuroid = new(neuroidNet, id);
perceptionNeuroid.GetInputFrom(neuroid);
neighbourSensor[availableIx] = neuroid;
return neuroid.receptor;
}
}
//Debug.LogWarning($"No available receptor for {id}");
return null;
}
/*
void ProcessStimulus(int thingId, Vector3 value) {
int availableIx = -1;
SensoryNeuroid leastInterestingNeuroid = null;
for (int i = 0; i < neighbourSensor.Length; i++) {
if (neighbourSensor[i] == null || neighbourSensor[i].IsStale())
availableIx = i;
else if (neighbourSensor[i].thingId == thingId) {
neighbourSensor[i].receptor.SetValue(value);
return;
}
if (neighbourSensor[i] != null) {
if (leastInterestingNeuroid == null || leastInterestingNeuroid.receptor.GetValue().magnitude > neighbourSensor[i].receptor.GetValue().magnitude)
leastInterestingNeuroid = neighbourSensor[i];
}
}
if (availableIx != -1) {
if (neighbourSensor[availableIx] != null) {
// Debug.Log($"revived receptor {availableIx} for {thingId}");
neighbourSensor[availableIx].thingId = thingId;
neighbourSensor[availableIx].receptor.SetValue(value);
}
else {
// Debug.Log($"new receptor for {thingId}");
SensoryNeuroid neuroid = new(neuroidNet, thingId);
cohesion.GetInputFrom(neuroid);
neighbourSensor[availableIx] = neuroid;
neuroid.receptor.SetValue(value);
}
}
else if (leastInterestingNeuroid != null) {
//Debug.Log($"replaced receptor {leastInterestingNeuroid.thingId} for {thingId}");
leastInterestingNeuroid.thingId = thingId;
leastInterestingNeuroid.receptor.SetValue(value);
}
//Debug.LogWarning($"No available receptor for {id}");
}
*/
void OnDrawGizmosSelected() {
Gizmos.DrawWireSphere(transform.position, sc.perceptionDistance);
}

View File

@ -10,7 +10,7 @@ public class SwarmControl : MonoBehaviour
public float cohesionForce = 10.0f;
public float separationForce = 5.0f;
public float separationDistance = 0.5f;
public float bodyForce = 20;
// public float bodyForce = 20;
public float perceptionDistance = 1.0f;
public float boundaryForce = 2.0f;