diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterInspector.cs index a534b6d..c90c5cf 100644 --- a/Editor/ClusterInspector.cs +++ b/Editor/ClusterInspector.cs @@ -188,6 +188,20 @@ namespace NanoBrain { anythingChanged = true; } EditorGUILayout.EndHorizontal(); + } else if (this.currentNucleus is Cluster cluster && cluster.clusterArray != null) { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.IntField("Array size", cluster.clusterArray.clusters.Count()); + if (GUILayout.Button("Add")) { + Undo.RecordObject(prefabAsset, "Array add " + prefabAsset.name); + cluster.clusterArray.Add(this.prefab); + anythingChanged = true; + } + if (GUILayout.Button("Del")) { + Undo.RecordObject(prefabAsset, "Array delete " + prefabAsset.name); + cluster.clusterArray.Remove(); + anythingChanged = true; + } + EditorGUILayout.EndHorizontal(); } // Synapses @@ -387,24 +401,18 @@ namespace NanoBrain { case Nucleus.Type.MemoryCell: AddMemoryCellInput(nucleus); break; - // case Nucleus.Type.Selector: - // AddSelectorInput(nucleus); - // break; case Nucleus.Type.Cluster: AddClusterInput(nucleus); break; - // case Nucleus.Type.Pulsar: - // AddPulsarInput(nucleus); - // break; case Nucleus.Type.Receptor: AddReceptorInput(nucleus); break; - // case Nucleus.Type.ReceptorArray: - // AddReceptorArrayInput(nucleus); - // break; case Nucleus.Type.ClusterReceptor: AddClusterReceptorInput(nucleus); break; + case Nucleus.Type.ClusterArray: + AddClusterArrayInput(nucleus); + break; default: break; } @@ -456,6 +464,13 @@ namespace NanoBrain { var editor = Editor.CreateEditor(subCluster.prefab); } + protected virtual void AddClusterArrayInput(Nucleus nucleus) { + ClusterPickerWindow.ShowPicker(prefab => OnPickedClusterArray(nucleus, prefab), "Select Cluster"); + } + private void OnPickedClusterArray(Nucleus nucleus, ClusterPrefab selectedPrefab) { + _ = new ClusterArray(selectedPrefab, this.prefab, 1, nucleus); + } + int selectedConnectNucleus = -1; // Connect to another nucleus in the same cluster protected virtual bool ConnectNucleus(ClusterPrefab cluster, Nucleus nucleusToConnect) { diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index 510de71..a9e5d96 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -439,12 +439,34 @@ namespace NanoBrain { style.normal.textColor = Color.white; } } + else if (nucleus is Cluster cluster && cluster.clusterArray != null) { + if (expandArray) { + // Put array indices above elements + style.alignment = TextAnchor.LowerCenter; + Vector3 labelPos1 = position + Vector3.down * (size + 5); // below disc + int colonPos1 = nucleus.name.IndexOf(":"); + if (colonPos1 > 0) { + string extName = nucleus.name[(colonPos1 + 2)..]; + Handles.Label(labelPos1, extName, style); + } + } + else { + // draw the array size label + if (color.grayscale > 0.5f) + style.normal.textColor = Color.black; + else + style.normal.textColor = Color.white; + Handles.Label(labelPosition, cluster.clusterArray.clusters.Length.ToString(), style); + style.normal.textColor = Color.white; + } + } if (expandArray == false || nucleus is not IReceptor) { // put name below nucleus Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron style.alignment = TextAnchor.UpperCenter; + nucleus.name ??= ""; int colonPos = nucleus.name.IndexOf(":"); if (colonPos > 0 && colonPos < nucleus.name.Length - 2) { // if it is an array, we should not show the :0 of the first element diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index f8ef85a..4626320 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -28,6 +28,9 @@ public class Cluster : Nucleus { } } + [SerializeReference] + public ClusterArray clusterArray; + #region Init /// diff --git a/Runtime/Scripts/Core/ClusterArray.cs b/Runtime/Scripts/Core/ClusterArray.cs new file mode 100644 index 0000000..fec9b23 --- /dev/null +++ b/Runtime/Scripts/Core/ClusterArray.cs @@ -0,0 +1,109 @@ +using UnityEngine; + +namespace NanoBrain { + + public class ClusterArray : Nucleus { + + public ClusterPrefab prefab; + public Cluster[] clusters; + + public ClusterArray(ClusterPrefab prefab, Cluster parent, int size, Nucleus receiver = null) { + this.prefab = prefab; + this.name = prefab.name; + this.clusters = new Cluster[size]; + for (int ix = 0; ix < size; ix++) { + Cluster cluster = new(prefab, parent); + cluster.defaultOutput.AddReceiver(receiver); + cluster.clusterArray = this; + this.clusters[ix] = cluster; + } + } + + public ClusterArray(ClusterPrefab prefab, ClusterPrefab parent, int size, Nucleus receiver = null) { + this.prefab = prefab; + this.name = prefab.name; + this.clusters = new Cluster[size]; + for (int ix = 0; ix < size; ix++) { + Cluster cluster = new(prefab, parent); + cluster.defaultOutput.AddReceiver(receiver); + cluster.clusterArray = this; + this.clusters[ix] = cluster; + } + } + + public override Nucleus ShallowCloneTo(Cluster parent) { + ClusterArray clone = new(this.prefab, parent, this.clusters.Length) { + clusterPrefab = this.clusterPrefab, + }; + + return clone; + } + + public override Nucleus Clone(ClusterPrefab parent) { + ClusterArray clone = new(this.prefab, parent, this.clusters.Length) { + }; + + return clone; + } + + public void Add(ClusterPrefab prefab) { + if (this.clusters.Length == 0) { + Debug.LogError("Empty perceptoid array, cannot add"); + } + int newLength = this.clusters.Length + 1; + Cluster[] newArray = new Cluster[newLength]; + + string baseName = this.name; + int colonPos = baseName.IndexOf(":"); + if (colonPos > 0) + baseName = baseName[..colonPos]; + + for (int i = 0; i < this.clusters.Length; i++) + newArray[i] = this.clusters[i]; + Cluster cluster = this.clusters[0]; + newArray[newLength - 1] = cluster.Clone(prefab) as Cluster; + newArray[newLength - 1].name = $"{baseName}: {newLength - 1}"; + } + + public void Remove() { + int newLength = this.clusters.Length - 1; + if (newLength == 0) { + Debug.LogWarning("Perceptoid array cannot be empty"); + } + Cluster[] newArray = new Cluster[newLength]; + for (int i = 0; i < newLength; i++) + newArray[i] = this.clusters[i]; + // Delete the last perception + //Cluster.Delete(nucleus); + } + + public override void UpdateStateIsolated() { + // Clusters don't do anything, + // The nuclei in them do the work + // and should be called directly, not from the cluster + } + + public virtual Cluster GetThingCluster() { + Cluster selectedCluster = SelectCluster(); + return selectedCluster; + } + + private Cluster SelectCluster() { + // Find a sleeping cluster + foreach (Cluster cluster in clusters) { + if (cluster.defaultOutput.isSleeping) + return cluster; + } + + // Otherwise find the stalest cluster? + Cluster stalestCluster = clusters[0]; + for (int ix = 1; ix < clusters.Length; ix++) { + if (clusters[ix].defaultOutput.stale > stalestCluster.defaultOutput.stale) + stalestCluster = clusters[ix]; + } + + return stalestCluster; + } + } + +} \ No newline at end of file diff --git a/Runtime/Scripts/Core/ClusterArray.cs.meta b/Runtime/Scripts/Core/ClusterArray.cs.meta new file mode 100644 index 0000000..8125246 --- /dev/null +++ b/Runtime/Scripts/Core/ClusterArray.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 837dfcffeb99804479a0887cbfc33372 \ No newline at end of file diff --git a/Runtime/Scripts/Core/ClusterReceptor.cs b/Runtime/Scripts/Core/ClusterReceptor.cs index 0dcd8ec..a6c7e52 100644 --- a/Runtime/Scripts/Core/ClusterReceptor.cs +++ b/Runtime/Scripts/Core/ClusterReceptor.cs @@ -9,194 +9,194 @@ using System.Linq; namespace NanoBrain { -[Serializable] -public class ClusterReceptor : Cluster, IReceptor { - public ClusterReceptor(ClusterPrefab prefab, Cluster parent, string name) : base(prefab, parent) { - this.name = name; - this.array = new NucleusArray(this); - if (this.name.IndexOf(":") < 0) - this.name += ": 0"; + [Serializable] + public class ClusterReceptor : Cluster, IReceptor { + public ClusterReceptor(ClusterPrefab prefab, Cluster parent, string name) : base(prefab, parent) { + this.name = name; + this.array = new NucleusArray(this); + if (this.name.IndexOf(":") < 0) + this.name += ": 0"; - } - public ClusterReceptor(ClusterPrefab prefab, ClusterPrefab parent, string name) : base(prefab, parent) { - this.name = name; - this.array = new NucleusArray(this); - } - - public string GetName() { - return this.name; - } - - public override Nucleus ShallowCloneTo(Cluster parent) { - ClusterReceptor clone = new(this.prefab, parent, this.name) { - clusterPrefab = this.clusterPrefab, - }; - - return clone; - } - - public override Nucleus Clone(ClusterPrefab parent) { - ClusterReceptor clone = new(prefab, parent, this.name) { - array = this._array - }; - - foreach (Synapse synapse in this.synapses) { - Synapse clonedSynapse = clone.AddSynapse(synapse.neuron); - clonedSynapse.weight = synapse.weight; + } + public ClusterReceptor(ClusterPrefab prefab, ClusterPrefab parent, string name) : base(prefab, parent) { + this.name = name; + this.array = new NucleusArray(this); } - this._outputs = null; // Make sure the output are regenerated - foreach (Neuron output in this.outputs) { - int ix = GetNucleusIndex(this.clusterNuclei, output); - if (ix < 0 || clone.clusterNuclei[ix] is not Neuron clonedOutput) - continue; - - foreach (Nucleus receiver in output.receivers) - clonedOutput.AddReceiver(receiver); + public string GetName() { + return this.name; } - return clone; - } - public override List CollectReceivers() { - List receivers = new(); - foreach (Nucleus element in this.nucleiArray) { - if (element is not Cluster clusterElement) - continue; + public override Nucleus ShallowCloneTo(Cluster parent) { + ClusterReceptor clone = new(this.prefab, parent, this.name) { + clusterPrefab = this.clusterPrefab, + }; - foreach (Nucleus outputNucleus in clusterElement.clusterNuclei) { - if (outputNucleus is not Neuron output) + return clone; + } + + public override Nucleus Clone(ClusterPrefab parent) { + ClusterReceptor clone = new(prefab, parent, this.name) { + array = this._array + }; + + foreach (Synapse synapse in this.synapses) { + Synapse clonedSynapse = clone.AddSynapse(synapse.neuron); + clonedSynapse.weight = synapse.weight; + } + + this._outputs = null; // Make sure the output are regenerated + foreach (Neuron output in this.outputs) { + int ix = GetNucleusIndex(this.clusterNuclei, output); + if (ix < 0 || clone.clusterNuclei[ix] is not Neuron clonedOutput) continue; - // this should be clusterElement.outputs, - // but outputs is not updated when correctly and may contain old data... - foreach (Nucleus receiver in output.receivers) { - // Only add receivers outside clusterElement cluster - if (receiver.clusterPrefab != clusterElement.prefab && - receivers.Contains(receiver) == false) - receivers.Add(receiver); + foreach (Nucleus receiver in output.receivers) + clonedOutput.AddReceiver(receiver); + } + return clone; + } + + public override List CollectReceivers() { + List receivers = new(); + foreach (Nucleus element in this.nucleiArray) { + if (element is not Cluster clusterElement) + continue; + + foreach (Nucleus outputNucleus in clusterElement.clusterNuclei) { + if (outputNucleus is not Neuron output) + continue; + + // this should be clusterElement.outputs, + // but outputs is not updated when correctly and may contain old data... + foreach (Nucleus receiver in output.receivers) { + // Only add receivers outside clusterElement cluster + if (receiver.clusterPrefab != clusterElement.prefab && + receivers.Contains(receiver) == false) + receivers.Add(receiver); + } } } - } - return receivers; - } - - [SerializeReference] - private NucleusArray _array; - public NucleusArray array { - set { _array = value; } - } - - public Nucleus[] nucleiArray { - get { return _array.nuclei; } - set { _array.nuclei = value; } - } - - public void AddReceptorElement(ClusterPrefab prefab) { - IReceptorHelpers.AddReceptorElement(this, prefab); - } - - public void RemoveReceptorElement() { - IReceptorHelpers.RemoveReceptorElement(this); - } - - public void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1) { - IReceptorHelpers.AddArrayReceiver(this, receiverToAdd, weight); - } - - public override void UpdateStateIsolated() { - // Clusters don't do anything, - // The nuclei in them do the work - // and should be called directly, not from the cluster - } - - public override void UpdateNuclei() { - foreach (Nucleus nucleus in this.clusterNuclei) - nucleus.UpdateNuclei(); - } - - public override void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) { - Debug.LogError("Process Stimulus was called on clusterreceptor without a neuron specified"); - } - - private readonly Dictionary thingReceivers = new(); - - public virtual void ProcessStimulus(Neuron input, Vector3 inputValue, int thingId = 0, string thingName = null) { - CleanupReceivers(); - - if (!thingReceivers.TryGetValue(thingId, out ClusterReceptor selectedReceiver)) - selectedReceiver = FindReceiver2(thingId, inputValue, input); - if (selectedReceiver == null) - return; - - if (thingName != null) { - string baseName = selectedReceiver.name; - int colonPos = selectedReceiver.name.IndexOf(":"); - if (colonPos > 0) - baseName = selectedReceiver.name[..colonPos]; - selectedReceiver.name = baseName + ": " + thingName; + return receivers; } - int inputIx = GetNucleusIndex(this.clusterNuclei, input); - if (inputIx < 0) - return; + [SerializeReference] + private NucleusArray _array; + public NucleusArray array { + set { _array = value; } + } - if (selectedReceiver.clusterNuclei[inputIx] is Neuron selectedNeuron) - selectedNeuron.ProcessStimulusDirect(inputValue); - } + public Nucleus[] nucleiArray { + get { return _array.nuclei; } + set { _array.nuclei = value; } + } + + public void AddReceptorElement(ClusterPrefab prefab) { + IReceptorHelpers.AddReceptorElement(this, prefab); + } + + public void RemoveReceptorElement() { + IReceptorHelpers.RemoveReceptorElement(this); + } + + public void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1) { + IReceptorHelpers.AddArrayReceiver(this, receiverToAdd, weight); + } + + public override void UpdateStateIsolated() { + // Clusters don't do anything, + // The nuclei in them do the work + // and should be called directly, not from the cluster + } + + public override void UpdateNuclei() { + foreach (Nucleus nucleus in this.clusterNuclei) + nucleus.UpdateNuclei(); + } + + public override void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) { + Debug.LogError("Process Stimulus was called on clusterreceptor without a neuron specified"); + } + + private readonly Dictionary thingReceivers = new(); + + public virtual void ProcessStimulus(Neuron input, Vector3 inputValue, int thingId = 0, string thingName = null) { + CleanupReceivers(); + + if (!thingReceivers.TryGetValue(thingId, out ClusterReceptor selectedReceiver)) + selectedReceiver = FindReceiver2(thingId, inputValue, input); + if (selectedReceiver == null) + return; + + if (thingName != null) { + string baseName = selectedReceiver.name; + int colonPos = selectedReceiver.name.IndexOf(":"); + if (colonPos > 0) + baseName = selectedReceiver.name[..colonPos]; + selectedReceiver.name = baseName + ": " + thingName; + } + + int inputIx = GetNucleusIndex(this.clusterNuclei, input); + if (inputIx < 0) + return; + + if (selectedReceiver.clusterNuclei[inputIx] is Neuron selectedNeuron) + selectedNeuron.ProcessStimulusDirect(inputValue); + } #if UNITY_MATHEMATICS - private ClusterReceptor FindReceiver2(int thingId, float3 inputValue, Neuron input) { - // No existing nucleus for this thing - ClusterReceptor selectedReceiver = null; - float selectedMagnitude = 0; - foreach (ClusterReceptor receiver in this.nucleiArray.Cast()) { - if (thingReceivers.ContainsValue(receiver) == false) { - // We found an unusued receiver - thingReceivers.Add(thingId, receiver); - return receiver; - } - else if (receiver.defaultOutput.isSleeping) { - // A sleeping receiver is not active and can therefore always be used - thingReceivers.Add(thingId, receiver); - receiver.bias = float3(0, 0, 0); - return receiver; - } - else if (selectedReceiver == null) { - // If we haven't found a receiver yet, just start by taking the first - selectedReceiver = receiver; - selectedMagnitude = length(selectedReceiver.defaultOutput.outputValue); - } - // Look for the receiver with the lowest output magnitude - else { - float magnitude = length(receiver.defaultOutput.outputValue); - - if (length(receiver.defaultOutput.outputValue) < selectedMagnitude) { + private ClusterReceptor FindReceiver2(int thingId, float3 inputValue, Neuron input) { + // No existing nucleus for this thing + ClusterReceptor selectedReceiver = null; + float selectedMagnitude = 0; + foreach (ClusterReceptor receiver in this.nucleiArray.Cast()) { + if (thingReceivers.ContainsValue(receiver) == false) { + // We found an unusued receiver + thingReceivers.Add(thingId, receiver); + return receiver; + } + else if (receiver.defaultOutput.isSleeping) { + // A sleeping receiver is not active and can therefore always be used + thingReceivers.Add(thingId, receiver); + receiver.bias = float3(0, 0, 0); + return receiver; + } + else if (selectedReceiver == null) { + // If we haven't found a receiver yet, just start by taking the first selectedReceiver = receiver; selectedMagnitude = length(selectedReceiver.defaultOutput.outputValue); } - } - } - if (selectedReceiver != null) { - // To re-initialize the cluster (esp. memory cells) - // we update the cluster neuron twice. - // Bit of a hack..... - int inputIx = GetNucleusIndex(this.clusterNuclei, input); - if (inputIx >= 0) { - if (selectedReceiver.clusterNuclei[inputIx] is Neuron selectedNeuron) - selectedNeuron.ProcessStimulusDirect(inputValue); - } + // Look for the receiver with the lowest output magnitude + else { + float magnitude = length(receiver.defaultOutput.outputValue); - // Replace the receiver - // Find the thingId current associated with the receiver - int keyToRemove = thingReceivers.FirstOrDefault(r => r.Value.Equals(selectedReceiver)).Key; - if (keyToRemove != 0 || thingReceivers.ContainsKey(keyToRemove)) - thingReceivers.Remove(keyToRemove); - // And add the new association - thingReceivers.Add(thingId, selectedReceiver); + if (length(receiver.defaultOutput.outputValue) < selectedMagnitude) { + selectedReceiver = receiver; + selectedMagnitude = length(selectedReceiver.defaultOutput.outputValue); + } + } + } + if (selectedReceiver != null) { + // To re-initialize the cluster (esp. memory cells) + // we update the cluster neuron twice. + // Bit of a hack..... + int inputIx = GetNucleusIndex(this.clusterNuclei, input); + if (inputIx >= 0) { + if (selectedReceiver.clusterNuclei[inputIx] is Neuron selectedNeuron) + selectedNeuron.ProcessStimulusDirect(inputValue); + } + + // Replace the receiver + // Find the thingId current associated with the receiver + int keyToRemove = thingReceivers.FirstOrDefault(r => r.Value.Equals(selectedReceiver)).Key; + if (keyToRemove != 0 || thingReceivers.ContainsKey(keyToRemove)) + thingReceivers.Remove(keyToRemove); + // And add the new association + thingReceivers.Add(thingId, selectedReceiver); + } + return selectedReceiver; } - return selectedReceiver; - } #else @@ -254,24 +254,25 @@ public class ClusterReceptor : Cluster, IReceptor { #endif - private void CleanupReceivers() { - // Remove a thing-receiver connection when the nucleus is inactive - List receiversToRemove = new(); - foreach (KeyValuePair item in thingReceivers) { - if (item.Value != null && item.Value.defaultOutput.isSleeping) - receiversToRemove.Add(item.Key); + private void CleanupReceivers() { + // Remove a thing-receiver connection when the nucleus is inactive + List receiversToRemove = new(); + foreach (KeyValuePair item in thingReceivers) { + if (item.Value != null && item.Value.defaultOutput.isSleeping) + receiversToRemove.Add(item.Key); + } + foreach (int thingId in receiversToRemove) { + Nucleus selectedReceiver = thingReceivers[thingId]; + + thingReceivers.Remove(thingId); + + int colonPos = selectedReceiver.name.IndexOf(":"); + if (colonPos > 0) + selectedReceiver.name = selectedReceiver.name[..colonPos]; + + } } - foreach (int thingId in receiversToRemove) { - Nucleus selectedReceiver = thingReceivers[thingId]; - thingReceivers.Remove(thingId); - - int colonPos = selectedReceiver.name.IndexOf(":"); - if (colonPos > 0) - selectedReceiver.name = selectedReceiver.name[..colonPos]; - - } } -} } \ No newline at end of file diff --git a/Runtime/Scripts/Core/Nucleus.cs b/Runtime/Scripts/Core/Nucleus.cs index d6a0952..b343d43 100644 --- a/Runtime/Scripts/Core/Nucleus.cs +++ b/Runtime/Scripts/Core/Nucleus.cs @@ -56,6 +56,7 @@ public abstract class Nucleus { Cluster, Receptor, ClusterReceptor, + ClusterArray, } #region Synapses diff --git a/Runtime/Scripts/Core/Receptor.cs b/Runtime/Scripts/Core/Receptor.cs index 38a9cdf..ef2e800 100644 --- a/Runtime/Scripts/Core/Receptor.cs +++ b/Runtime/Scripts/Core/Receptor.cs @@ -29,7 +29,7 @@ namespace NanoBrain { public Receptor(ClusterPrefab prefab, string name) : base(prefab, name) { this.array = new NucleusArray(this); } - + public string GetName() { return this.name; } @@ -108,6 +108,6 @@ namespace NanoBrain { this._array ??= new NucleusArray(this.parent); this._array.ProcessStimulus(thingId, inputValue, thingName); } + } - } \ No newline at end of file