diff --git a/.editorconfig b/.editorconfig index def86c3..64e4e80 100644 --- a/.editorconfig +++ b/.editorconfig @@ -18,3 +18,6 @@ csharp_new_line_between_query_expression_clauses = true # Limit the number of characters in a line max_line_length = 100 # This setting does not enforce it; it's a guideline. + +[*.{cs,vb}] +dotnet_diagnostic.IDE1006.severity = none \ No newline at end of file diff --git a/Assembly-CSharp.csproj b/Assembly-CSharp.csproj index a3f3720..97530fd 100644 --- a/Assembly-CSharp.csproj +++ b/Assembly-CSharp.csproj @@ -52,6 +52,7 @@ + diff --git a/Assets/NanoBrain/Editor/NeuroidWindow.cs b/Assets/NanoBrain/Editor/NeuroidWindow.cs index 0548e29..3560973 100644 --- a/Assets/NanoBrain/Editor/NeuroidWindow.cs +++ b/Assets/NanoBrain/Editor/NeuroidWindow.cs @@ -43,8 +43,9 @@ public class GraphEditorWindow : EditorWindow { } // If this neuroid is not visited while its output neuroid is visited - if (!neuronVisited.Contains(neuroid) && (neuroid.outputNeuroid == null || - (neuronVisited.Contains(neuroid.outputNeuroid) && neuroid.outputNeuroid.layerIx == layerIx - 1))) { + // 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))) { // Add it to the next layer currentLayer.neuroids.Add(neuroid); neuroid.layerIx = layerIx; @@ -54,7 +55,7 @@ public class GraphEditorWindow : EditorWindow { Vector2Int neuroidPosition = new(layerIx, neuroidIx); neuroidPositions[neuroid] = neuroidPosition; 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); int i = 0; - float inputSpacing = 200f / layerNeuroid.synapses.Count; + float inputSpacing = 200f / layerNeuroid.newSynapses.Count; float inputMargin = 100 + inputSpacing / 2; - foreach (Synapse synapse in layerNeuroid.synapses.Values) { + foreach (Synapse synapse in layerNeuroid.newSynapses.Values) { if (synapse.neuroid != null) { if (this.neuroidPositions.ContainsKey(synapse.neuroid)) { @@ -144,7 +145,7 @@ public class GraphEditorWindow : EditorWindow { // Draw the tooltip GUIContent tooltip = new( $"{neuroid.name}" + - $"\nsynapse count {neuroid.synapses.Count}" + + $"\nsynapse count {neuroid.newSynapses.Count}" + $"\nValue: {neuroid.outputValue}" + $"\nStale: {neuroid.stale}"); diff --git a/Assets/NanoBrain/Neuroid.cs b/Assets/NanoBrain/Neuroid.cs index a098aaa..a5d69d3 100644 --- a/Assets/NanoBrain/Neuroid.cs +++ b/Assets/NanoBrain/Neuroid.cs @@ -29,17 +29,16 @@ public class NeuroidNetwork { } public class Neuroid { - public int id; + //public int id; public string name; public int layerIx; public int stale = 0; - public readonly Dictionary synapses = new(); + public readonly Dictionary newSynapses = new(); public Vector3 outputValue; - public Neuroid outputNeuroid; - public int outputNeurix; + public HashSet outputNeuroids = new(); public enum Mode { Sum, @@ -52,69 +51,95 @@ public class Neuroid { public Neuroid(NeuroidNetwork net) { this.net = net; - this.net.neuroids.Add(this); + if (this.net != null) + this.net.neuroids.Add(this); } - public void SetOutputTo(Neuroid neuroid) { - this.outputNeuroid = neuroid; - // neuroid.inputNeuroids.Add(this); - this.outputNeurix = this.id; + public void AddSynapse(Neuroid input) { + input.AddReceiver(this); + this.newSynapses[input] = new(input, Vector3.zero, 1.0f); + } + + public void AddReceiver(Neuroid receiver) { + this.outputNeuroids.Add(receiver); } public void ResetWeights() { - foreach (Synapse synapse in synapses.Values) + foreach (Synapse synapse in this.newSynapses.Values) synapse.weight = 1.0f; } public void SetWeight(Neuroid input, float weight) { - if (synapses.ContainsKey(input.id)) - synapses[input.id] = new(input, synapses[input.id].value, weight); - else - synapses[input.id] = new(input, Vector3.zero, weight); + if (this.newSynapses.ContainsKey(input)) { + this.newSynapses[input].weight = weight; + } + else { + this.newSynapses[input] = new(input, Vector3.zero, weight); + } } public void GetInputFrom(Neuroid input, float weight = 1.0f) { - input.id = this.synapses.Count; - input.SetOutputTo(this); - synapses[input.id] = new(input, Vector3.zero, weight); + input.AddReceiver(this); + this.newSynapses[input] = new(input, Vector3.zero, weight); } - public void SetInput(int inputId, Vector3 value) { - if (synapses.ContainsKey(inputId)) - synapses[inputId].value = value; + public void SetInput(Neuroid input, Vector3 value) { + if (this.newSynapses.ContainsKey(input)) { + Synapse synapse = this.newSynapses[input]; + synapse.value = value; + } else - synapses[inputId] = new(null, value, 1.0f); + this.newSynapses[input] = new(null, value, 1.0f); UpdateState(); } - public void SetInput(int inputIx, Vector3 value, float weight) { - if (synapses.ContainsKey(inputIx)) { - Synapse synapse = synapses[inputIx]; + + public void SetInput(Neuroid input, Vector3 value, float weight) { + if (this.newSynapses.ContainsKey(input)) { + Synapse synapse = this.newSynapses[input]; synapse.value = value; synapse.weight = weight; } else - synapses[inputIx] = new(null, value, weight); + this.newSynapses[input] = new(null, value, weight); UpdateState(); } - void UpdateState() { + public readonly Dictionary 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; - foreach (Synapse synapse in synapses.Values) + foreach (Synapse synapse in this.newSynapses.Values) sum += synapse.value * synapse.weight; this.outputValue = Activation(sum); - this.outputNeuroid?.SetInput(this.outputNeurix, this.outputValue); + foreach (Neuroid neuroid in outputNeuroids) { + neuroid?.SetInput(this, this.outputValue); + } this.stale = 0; } Vector3 Activation(Vector3 sum) { - if (synapses.Count == 0 && mode == Mode.Average) - Debug.LogWarning($"{this.id} {this.name} has zero synapses for average"); + 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.id} {this.name} sum is nan"); + Debug.LogWarning($"{this.name} sum is nan"); return mode switch { Mode.Sum => sum, - Mode.Average => sum / synapses.Count, + Mode.Average => sum / this.newSynapses.Count, _ => sum, }; //return sum; //(sum.magnitude > 0.5f) ? sum : Vector3.zero; diff --git a/Assets/NanoBrain/Perception.cs b/Assets/NanoBrain/Perception.cs new file mode 100644 index 0000000..bb822fe --- /dev/null +++ b/Assets/NanoBrain/Perception.cs @@ -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 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}"); + } +} \ No newline at end of file diff --git a/Assets/NanoBrain/Perception.cs.meta b/Assets/NanoBrain/Perception.cs.meta new file mode 100644 index 0000000..adfa3db --- /dev/null +++ b/Assets/NanoBrain/Perception.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 37d94d399d30e6eb996236adabad87ee \ No newline at end of file diff --git a/Assets/NanoBrain/SensoryNeuroid.cs b/Assets/NanoBrain/SensoryNeuroid.cs index 7651d30..cf6bc38 100644 --- a/Assets/NanoBrain/SensoryNeuroid.cs +++ b/Assets/NanoBrain/SensoryNeuroid.cs @@ -1,32 +1,70 @@ +using System.Linq; using UnityEngine; -public class Receptor { - public SensoryNeuroid neuroid; - public void SetValue(Vector3 value) { - if (neuroid != null) { - neuroid.SetInput(neuroid.id, value); - } +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 Vector3 GetValue() { - if (neuroid != null) - return neuroid.synapses[neuroid.id].value; - else - return Vector3.zero; + +} + +public class Receptor { + + public SensoryNeuroid neuroid; + + public int thingId; + /// + /// Local position of the thing + /// + public virtual Vector3 position { + get { + if (neuroid != null) + return neuroid.newSynapses[neuroid].value; + else + 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 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 - }; + public VelocityNeuroid(NeuroidNetwork net) : base(net) { } + 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; + } } \ No newline at end of file diff --git a/Assets/Scenes/Boids/Boids.unity b/Assets/Scenes/Boids/Boids.unity index 5b7c526..970b54d 100644 --- a/Assets/Scenes/Boids/Boids.unity +++ b/Assets/Scenes/Boids/Boids.unity @@ -378,7 +378,7 @@ MonoBehaviour: separationForce: 5 separationDistance: 0.3 bodyForce: 20 - perceptionDistance: 2 + perceptionDistance: 1 boundaryForce: 5 spaceSize: {x: 10, y: 10, z: 10} boundaryWidth: {x: 1, y: 1, z: 1} @@ -394,7 +394,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: ec888ca5333d45a438f9f417fa5ce135, type: 3} m_Name: m_EditorClassIdentifier: Assembly-CSharp::SwarmSpawn - count: 1000 + count: 5 boidPrefab: {fileID: 8702527964058765413, guid: f9c706268554ce449a8773675b2864b8, type: 3} spawnAreaSize: {x: 0.5, y: 0.5, z: 0.5} minDelay: 0.05 diff --git a/Assets/Scenes/Boids/Scripts/Boid.cs b/Assets/Scenes/Boids/Scripts/Boid.cs index 9b7e00d..d0cd02e 100644 --- a/Assets/Scenes/Boids/Scripts/Boid.cs +++ b/Assets/Scenes/Boids/Scripts/Boid.cs @@ -19,7 +19,8 @@ public class Boid : MonoBehaviour { 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 Neuroid bodyVector; @@ -40,11 +41,14 @@ 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 }; + 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 }; @@ -78,15 +82,15 @@ public class Boid : MonoBehaviour { Vector3 localPosition = neighbour.transform.position - this.transform.position; Vector3 relativeVelocity = neighbour.velocity - this.velocity; - int id = neighbour.GetInstanceID(); - ProcessStimulus(id, localPosition); + int thingId = neighbour.GetInstanceID(); + perception.ProcessStimulus(thingId, localPosition); Vector3 separationForce = -localPosition / localPosition.sqrMagnitude; // which is equivalent to -(localPosition.normalized / localPosition.magnitude) - separation.SetInput(id, separationForce, sc.separationDistance); - //cohesion.SetInput(id, localPosition, sc.cohesionForce); - alignment.SetInput(id, relativeVelocity, sc.alignmentForce); + separation.SetInput(thingId, separationForce, sc.separationDistance, neuroidNet); + //cohesion.SetInput(thingId, localPosition, sc.cohesionForce); + alignment.SetInput(thingId, relativeVelocity, sc.alignmentForce, neuroidNet); neighbourCount++; } @@ -108,7 +112,7 @@ public class Boid : MonoBehaviour { Vector3 direction = (sc.transform.position - this.transform.position).normalized; 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}"); } @@ -128,7 +132,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++) { @@ -157,7 +161,7 @@ public class Boid : MonoBehaviour { } - +/* void ProcessStimulus(int thingId, Vector3 value) { int availableIx = -1; SensoryNeuroid leastInterestingNeuroid = null; @@ -168,8 +172,6 @@ public class Boid : MonoBehaviour { neighbourSensor[i].receptor.SetValue(value); return; } - // if (leastInterestingIx == -1 || neighbourSensor[leastInterestingIx].receptor.GetValue().magnitude > neighbourSensor[i].receptor.GetValue().magnitude) - // leastInterestingIx = i; if (neighbourSensor[i] != null) { if (leastInterestingNeuroid == null || leastInterestingNeuroid.receptor.GetValue().magnitude > neighbourSensor[i].receptor.GetValue().magnitude) leastInterestingNeuroid = neighbourSensor[i]; @@ -197,7 +199,7 @@ public class Boid : MonoBehaviour { //Debug.LogWarning($"No available receptor for {id}"); } - +*/ void OnDrawGizmosSelected() { Gizmos.DrawWireSphere(transform.position, sc.perceptionDistance); }