diff --git a/Editor/ClusterEditor.cs b/Editor/ClusterEditor.cs index 6e6ef22..725b27d 100644 --- a/Editor/ClusterEditor.cs +++ b/Editor/ClusterEditor.cs @@ -64,30 +64,22 @@ namespace NanoBrain { public class GraphEditor : GraphView { protected ClusterPrefab prefab; + protected Nucleus currentPrefabNucleus; + + protected override Nucleus currentNucleus { + get => base.currentNucleus; + set { + base.currentNucleus = value; + this.currentPrefabNucleus = value != null ? this.prefab.GetNucleus(value.name) : null; + } + } public GraphEditor(ClusterPrefab prefab) : base(prefab.output.parent) { this.prefab = prefab; // In a Prefab editor, no instance exists but we need it for the ClusterViewer. // So we create a temporary instance - Cluster cluster = new(prefab); - this.currentCluster = cluster; - - Button addButton = new(() => OnAddClusterOutput()) { - text = "Add" - }; - topMenuContainer?.Add(addButton); - - Add(topMenuContainer); - } - - void OnAddClusterOutput() { - Nucleus newOutput = new Neuron(this.prefab, "New Output"); - this.prefab.RefreshOutputs(); - // outputsPopup.choices = this.prefab.outputs.Select(output => output.name).ToList(); - // outputsPopup.value = newOutput.name; - - this.currentNucleus = newOutput; + this.currentCluster = new(prefab); } public void SetGraph(GameObject gameObject, VisualElement inspectorContainer) { @@ -151,69 +143,76 @@ namespace NanoBrain { if (serializedObject == null || serializedObject.targetObject == null) return; - if (this.currentNucleus == null) - return; - serializedObject.Update(); - GUIStyle headerStyle = new(EditorStyles.boldLabel) { - alignment = TextAnchor.MiddleLeft, - margin = new RectOffset(10, 0, 4, 4) - }; GUIStyle boldTextFieldStyle = new(EditorStyles.textField) { fontStyle = FontStyle.Bold }; - // Nucleus type - string nucleusType = this.currentNucleus.GetType().Name; - GUILayout.Label(nucleusType, headerStyle); - - // Nucleus name - if (this.currentNucleus.parent is Cluster parentCluster) { - EditorGUILayout.BeginHorizontal(); - if (GUILayout.Button(this.currentNucleus.parent.name)) - OnClusterClick(parentCluster); - EditorGUI.BeginDisabledGroup(true); - EditorGUILayout.TextField(this.currentNucleus.name, boldTextFieldStyle); - EditorGUI.EndDisabledGroup(); - if (GUILayout.Button("Reimport")) - ReimportCluster(parentCluster); - EditorGUILayout.EndHorizontal(); + if (this.currentNucleus == null) { + OutputsInspector(ref anythingChanged); + return; } else { - string newName = EditorGUILayout.TextField(this.currentNucleus.name, boldTextFieldStyle); - if (newName != this.currentNucleus.name) { - this.currentNucleus.name = newName; - this.prefab.RefreshOutputs(); - // outputsPopup.choices = this.prefab.outputs.Select(output => output.name).ToList(); - anythingChanged = true; - } - } + GUIStyle headerStyle = new(EditorStyles.boldLabel) { + alignment = TextAnchor.MiddleLeft, + margin = new RectOffset(10, 0, 4, 4) + }; + // Nucleus type + string nucleusType = this.currentNucleus.GetType().Name; + GUILayout.Label(nucleusType, headerStyle); - // Current output value - if (Application.isPlaying) { - if (currentNucleus is Neuron currentNeuron1) { - GUIContent nameLabel = new("Output", currentNeuron1.outputValue.ToString()); - EditorGUILayout.FloatField(nameLabel, currentNeuron1.outputMagnitude); + // Nucleus name + Cluster cluster = this.currentPrefabNucleus as Cluster; + if (cluster != null) { + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button(this.currentNucleus.parent.name)) + OnClusterClick(cluster); + EditorGUI.BeginDisabledGroup(true); + EditorGUILayout.TextField(this.currentNucleus.name, boldTextFieldStyle); + EditorGUI.EndDisabledGroup(); + if (GUILayout.Button("Reimport")) + ReimportCluster(cluster); + EditorGUILayout.EndHorizontal(); + } + else { + string newName = EditorGUILayout.TextField(this.currentNucleus.name, boldTextFieldStyle); + if (newName != this.currentNucleus.name) { + Nucleus prefabNucleus = this.prefab.GetNucleus(this.currentNucleus.name); + prefabNucleus.name = newName; + // This changes it in the temporary cluster instance + this.currentNucleus.name = newName; + this.prefab.RefreshOutputs(); + // outputsPopup.choices = this.prefab.outputs.Select(output => output.name).ToList(); + anythingChanged = true; + } + } + + // Current output value + if (Application.isPlaying) { + if (currentNucleus is Neuron currentNeuron1) { + GUIContent nameLabel = new("Output", currentNeuron1.outputValue.ToString()); + EditorGUILayout.FloatField(nameLabel, currentNeuron1.outputMagnitude); + } + else + EditorGUILayout.LabelField(" "); } else EditorGUILayout.LabelField(" "); + + // Memory cell + if (this.currentNucleus is MemoryCell memory) + MemoryCellInspector(memory, ref anythingChanged); + // Cluster + else if (cluster != null) + ClusterInspector(cluster, ref anythingChanged); + // Other + else + NucleusInspector(this.currentNucleus, ref anythingChanged); + + if (GUILayout.Button("Delete")) + DeleteNucleus(this.currentNucleus); } - else - EditorGUILayout.LabelField(" "); - - // Memory cell - if (this.currentNucleus is MemoryCell memory) - MemoryCellInspector(memory, ref anythingChanged); - // Cluster - else if (this.currentNucleus is Cluster cluster) - ClusterInspector(cluster, ref anythingChanged); - // Other - else - NucleusInspector(this.currentNucleus, ref anythingChanged); - - if (GUILayout.Button("Delete")) - DeleteNucleus(this.currentNucleus); serializedObject.ApplyModifiedProperties(); if (anythingChanged) { @@ -222,6 +221,24 @@ namespace NanoBrain { } } + protected void OutputsInspector(ref bool anythingChanged) { + GUIStyle headerStyle = new(EditorStyles.boldLabel) { + alignment = TextAnchor.MiddleLeft, + margin = new RectOffset(10, 0, 4, 4) + }; + GUILayout.Label("Outputs", headerStyle); + + bool connecting = GUILayout.Button("Add Output Neuron"); + if (connecting) { + Nucleus newOutput = new Neuron(this.prefab, "New Output"); + // Regenerate the temporary clsuter instance + // See also the constructor + this.currentCluster = new(this.prefab); + this.currentNucleus = newOutput; + this.selectedOutput = this.currentNucleus; + } + } + protected void MemoryCellInspector(MemoryCell memoryCell, ref bool anythingChanged) { memoryCell.staticMemory = EditorGUILayout.Toggle("Static Memory", memoryCell.staticMemory); NucleusInspector(memoryCell, ref anythingChanged); @@ -241,7 +258,6 @@ namespace NanoBrain { if (GUILayout.Button("Add")) { Undo.RecordObject(prefabAsset, "Array add " + prefabAsset.name); - //cluster.AddInstance(this.prefab); cluster.AddInstance(); anythingChanged = true; } @@ -284,14 +300,17 @@ namespace NanoBrain { EditorGUIUtility.labelWidth = 100; Vector3 newBias = EditorGUILayout.Vector3Field("Bias", this.currentNucleus.bias); - anythingChanged |= newBias != this.currentNucleus.bias; - this.currentNucleus.bias = newBias; + if (newBias != this.currentPrefabNucleus.bias) { + anythingChanged |= newBias != this.currentNucleus.bias; + this.currentPrefabNucleus.bias = newBias; + this.currentNucleus.bias = newBias; + } EditorGUIUtility.labelWidth = previousLabelWidth; Nucleus[] array = null; int elementIx = -1; - if (this.currentNucleus.synapses.Count > 0) { - Synapse[] synapses = this.currentNucleus.synapses.ToArray(); + if (this.currentPrefabNucleus.synapses.Count > 0) { + Synapse[] synapses = this.currentPrefabNucleus.synapses.ToArray(); foreach (Synapse synapse in synapses) { if (synapse.neuron == null) continue; @@ -330,24 +349,20 @@ namespace NanoBrain { EditorGUILayout.BeginHorizontal(); if (synapse.neuron.clusterPrefab != this.currentNucleus.clusterPrefab) { - // If it is a cluster + // If it is a different cluster GUIStyle labelStyle = new(GUI.skin.label); float labelWidth = 200; if (synapse.neuron.clusterPrefab != null) { labelWidth = labelStyle.CalcSize(new GUIContent($"{synapse.neuron.clusterPrefab.name}.")).x; GUILayout.Label($"{synapse.neuron.clusterPrefab.name}", GUILayout.Width(labelWidth)); } - //string[] options = synapse.neuron.parent.clusterNuclei.Select(n => n.name).ToArray(); string[] options = synapse.neuron.clusterPrefab.nuclei.Select(n => n.name).ToArray(); int selectedIndex = System.Array.IndexOf(options, synapse.neuron.name); int newIndex = EditorGUILayout.Popup(selectedIndex, options); - // if (newIndex != selectedIndex && synapse.neuron.clusterPrefab.nuclei[newIndex] is Neuron newNeuron) - // ChangeSynapse(synapse, newNeuron); if (newIndex != selectedIndex) { - // It shall be ensured that the parent.clusterNuclei and - // clusterPrefab.nuclei contain the same neurons in the same order.... - Nucleus selectedNucleus = synapse.neuron.parent.clusterNuclei[newIndex]; - Neuron newNeuron = selectedNucleus as Neuron; + // Nucleus selectedNucleus = synapse.neuron.parent.clusterNuclei[newIndex]; + // Neuron newNeuron = selectedNucleus as Neuron; + Neuron newNeuron = synapse.neuron.clusterPrefab.nuclei[newIndex] as Neuron; ChangeSynapse(synapse, newNeuron); } } @@ -367,14 +382,6 @@ namespace NanoBrain { EditorGUI.indentLevel++; float newWeight = EditorGUILayout.FloatField("Weight", synapse.weight); if (newWeight != synapse.weight) { - // if (synapse.neuron.parent is IReceptor receptor) { - // Nucleus[] receptorArray = receptor.nucleiArray; - // foreach (Synapse s in this.currentNucleus.synapses) { - // if (s.neuron.parent is IReceptor r && r.nucleiArray == receptorArray) - // s.weight = newWeight; - // } - // } - // else synapse.weight = newWeight; anythingChanged = true; } @@ -430,15 +437,6 @@ namespace NanoBrain { case Nucleus.Type.Cluster: AddClusterInput(nucleus); break; - // case Nucleus.Type.Receptor: - // AddReceptorInput(nucleus); - // break; - // case Nucleus.Type.ClusterReceptor: - // AddClusterReceptorInput(nucleus); - // break; - // case Nucleus.Type.ClusterArray: - // AddClusterArrayInput(nucleus); - // break; default: break; } @@ -535,17 +533,15 @@ namespace NanoBrain { } } } - this.prefab.nuclei.Remove(nucleus); + this.currentCluster.DeleteNucleus(nucleus);//clusterNuclei.Remove(nucleus); - // if (outputsPopup.value == nucleus.name) { - // this.prefab.RefreshOutputs(); - // outputsPopup.choices = this.prefab.outputs.Select(output => output.name).ToList(); - // outputsPopup.index = 0; - // } + // this.prefab.nuclei.Remove(nucleus); + // Neuron.Delete(nucleus); + this.prefab.RefreshOutputs(); - Neuron.Delete(nucleus); this.currentNucleus = this.prefab.output; + this.selectedOutput = this.currentNucleus; } Nucleus.Type selectedType = Nucleus.Type.None; @@ -570,7 +566,7 @@ namespace NanoBrain { } protected virtual void ChangeSynapse(Synapse synapse, Neuron newNucleus) { - Neuron synapseNeuron = synapse.neuron as Neuron; + Neuron synapseNeuron = synapse.neuron; if (synapse.neuron.parent is Cluster subCluster && subCluster.prefab != this.prefab) { // if (synapse.neuron.parent is ClusterReceptor receptor) { // // the new nucleus is part of a (cluster) receptor, @@ -600,8 +596,8 @@ namespace NanoBrain { // } // else { // it is a neuron in a subcluster - synapseNeuron.RemoveReceiver(this.currentNucleus); - newNucleus.AddReceiver(this.currentNucleus); + synapseNeuron.RemoveReceiver(this.currentPrefabNucleus); + newNucleus.AddReceiver(this.currentPrefabNucleus); // } } else { diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index 54df2e8..eda9701 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -15,8 +15,15 @@ namespace NanoBrain { //protected readonly ClusterPrefab prefab; protected Cluster currentCluster; protected SerializedObject serializedBrain; - protected Nucleus currentNucleus; + protected Nucleus _currentNucleus; + protected virtual Nucleus currentNucleus { + get => _currentNucleus; + set => _currentNucleus = value; + } + //protected Nucleus currentNucleus; protected Nucleus selectedOutput; + // Only used when selecting a synapse to a multi-cluster + protected Nucleus selectedSynapseNeuron; protected GameObject gameObject; private bool expandArray = false; @@ -324,7 +331,7 @@ namespace NanoBrain { if (nucleus is Neuron neuron) receivers = neuron.receivers; else if (nucleus is Cluster cluster) - receivers = cluster.CollectReceivers(); + receivers = cluster.CollectReceivers(true); else return; @@ -354,7 +361,6 @@ namespace NanoBrain { float margin = 10 + spacing / 2; int row = 0; - List drawnArrays = new(); foreach (Nucleus receiver in receivers) { Nucleus receiverNucleus = receiver; if (receiverNucleus == null) @@ -380,6 +386,10 @@ namespace NanoBrain { } protected void DrawSynapses(Nucleus nucleus, Vector3 parentPos, float size) { + if (this.selectedSynapseNeuron != null) { + DrawClusterSynapses(this.selectedSynapseNeuron, parentPos, size); + return; + } if (nucleus == null) return; @@ -387,15 +397,19 @@ namespace NanoBrain { // This is used to 'scale' the output value colors of the nuclei float maxValue = 0; int neuronCount = 0; - List drawnNeurons = new(); + List drawnNeuronNames = new(); foreach (Synapse synapse in nucleus.synapses) { if (synapse.neuron == null) continue; // Count multiple synapses to the same neuron only once - if (drawnNeurons.Contains(synapse.neuron)) + string neuronName = synapse.neuron.name; + if (synapse.neuron.parent != null) + neuronName = synapse.neuron.parent.baseName + "." + neuronName; + + if (drawnNeuronNames.Contains(neuronName)) continue; - drawnNeurons.Add(synapse.neuron); + drawnNeuronNames.Add(neuronName); float value = synapse.neuron.outputMagnitude * synapse.weight; if (value > maxValue) @@ -409,15 +423,20 @@ namespace NanoBrain { float margin = 10 + spacing / 2; int row = 0; - drawnNeurons = new(); + //List drawnNeurons = new(); + drawnNeuronNames = new(); foreach (Synapse synapse in nucleus.synapses) { if (synapse.neuron is null) continue; // Draw multiple synapses to the same neuron only once - if (drawnNeurons.Contains(synapse.neuron)) + string neuronName = synapse.neuron.name; + if (synapse.neuron.parent != null) + neuronName = synapse.neuron.parent.baseName + "." + neuronName; + + if (drawnNeuronNames.Contains(neuronName)) continue; - drawnNeurons.Add(synapse.neuron); + drawnNeuronNames.Add(neuronName); Vector3 pos = new(250, margin + row * spacing, 0.0f); DrawEdge(parentPos, pos); @@ -435,6 +454,50 @@ namespace NanoBrain { } } + protected void DrawClusterSynapses(Nucleus nucleus, Vector3 parentPos, float size) { + if (nucleus == null || nucleus.parent == null || nucleus.parent.siblingClusters == null) + return; + + // Hack to disable showing labels + expandArray = true; + + float maxValue = 0; + foreach (Cluster sibling in nucleus.parent.siblingClusters) { + Neuron siblingNeuron = sibling.GetNucleus(nucleus.name) as Neuron; + float value = siblingNeuron.outputMagnitude; // no need to add weight as they are all the same + if (value > maxValue) + maxValue = value; + } + + // Determine the spacing of the nuclei in the layer + float spacing = 400f / nucleus.parent.instanceCount; + float margin = 10 + spacing / 2; + + int row = 0; + foreach (Cluster sibling in nucleus.parent.siblingClusters) { + Neuron siblingNeuron = sibling.GetNucleus(nucleus.name) as Neuron; + Vector3 position = new(250, margin + row * spacing, 0.0f); + DrawEdge(parentPos, position); + Color color = Color.black; + if (Application.isPlaying) { + if (maxValue == 0 || !float.IsFinite(maxValue)) + maxValue = 1; + float brightness = siblingNeuron.outputMagnitude / maxValue; + color = new Color(brightness, brightness, brightness, 1f); + } DrawNucleus(siblingNeuron, position, size, color); + GUIStyle style = new(EditorStyles.label) { + alignment = TextAnchor.UpperCenter, + normal = { textColor = Color.white }, + fontStyle = FontStyle.Bold, + }; + Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron + string name = $"{sibling.baseName}.{nucleus.name}"; + Handles.Label(labelPos, name, style); + row++; + } + expandArray = false; + } + protected void DrawOutputs(Vector2 parentPos, float size) { // Determine the maximum value in this layer // This is used to 'scale' the output value colors of the nuclei @@ -534,7 +597,7 @@ namespace NanoBrain { else if (nucleus is Cluster cluster) DrawCluster(cluster, position, color, size); - if (expandArray == false || nucleus != currentNucleus) { + if (expandArray == false) {// || nucleus != currentNucleus) { // put name below nucleus Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron style.alignment = TextAnchor.UpperCenter; @@ -728,24 +791,33 @@ namespace NanoBrain { protected void OnNeuronClick(Nucleus nucleus) { if (nucleus == this.currentNucleus) { - if (Application.isPlaying) { - if (nucleus is Cluster) - expandArray = !expandArray; - else - expandArray = false; - } - else { - if (nucleus is Cluster cluster) - OnClusterClick(cluster); - } + this.selectedSynapseNeuron = null; + // if (Application.isPlaying) { + // if (nucleus is Cluster) + // expandArray = !expandArray; + // else + // expandArray = false; + // } + // else { + if (nucleus is Cluster cluster) + OnClusterClick(cluster); + // } } else if (nucleus.parent != null && this.currentNucleus != null && nucleus.parent != this.currentNucleus.parent) { // We go to a different cluster if (Application.isPlaying) { - this.currentNucleus = nucleus; - if (this.currentNucleus is Neuron neuron && neuron.receivers.Count == 0) - this.selectedOutput = this.currentNucleus; - expandArray = false; + if (this.selectedSynapseNeuron == null && nucleus.parent.instanceCount > 1) { + this.selectedSynapseNeuron = nucleus; + expandArray = false; + } + else { + this.currentNucleus = nucleus; + if (this.currentNucleus is Neuron neuron && neuron.receivers.Count == 0) + this.selectedOutput = this.currentNucleus; + this.selectedSynapseNeuron = null; + expandArray = false; + } + } else { // select the cluster, not the neuron in the cluster diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index 0b89721..87366f3 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -32,9 +32,11 @@ namespace NanoBrain { } // This should not be serialized - [SerializeReference] + //[SerializeReference] + [NonSerialized] public Cluster[] siblingClusters; // This serialization should be enough + [SerializeField] public int instanceCount = 1; public Dictionary thingClusters = new(); @@ -93,6 +95,8 @@ namespace NanoBrain { nucleus.ShallowCloneTo(this); } Nucleus[] clonedNuclei = this.clusterNuclei.ToArray(); + // foreach (Nucleus n in clonedNuclei) + // n.name += "(c)"; // Now clone the connections for (int nucleusIx = 0; nucleusIx < prefabNuclei.Length; nucleusIx++) { @@ -106,19 +110,15 @@ namespace NanoBrain { foreach (Synapse prefabSynapse in prefabNeuron.synapses) { Neuron synapseNeuron = prefabSynapse.neuron; - if (synapseNeuron.parent is not null && synapseNeuron.clusterPrefab != this.clusterPrefab) { + if (synapseNeuron.clusterPrefab != null && synapseNeuron.clusterPrefab != this.prefab) { // Neuron is in another cluster, find the cloned cluster first - Cluster prefabCluster = synapseNeuron.parent; - int clusterIx = GetNucleusIndex(prefabNuclei, prefabCluster); - if (clusterIx < 0) - // Could not find the cluster in the prefab - continue; - if (clonedNuclei[clusterIx] is not Cluster clonedCluster) - // Could not find the cloned cluster + ClusterPrefab prefabCluster = synapseNeuron.clusterPrefab; + Cluster clonedCluster = this.clusterNuclei.Find(n => n.name == prefabCluster.name) as Cluster; + if (clonedCluster == null) continue; // Now find the neuron in that cloned cluster - int neuronIx = GetNucleusIndex(prefabCluster.prefab.nuclei, prefabSynapse.neuron); + int neuronIx = GetNucleusIndex(prefabCluster.nuclei, prefabSynapse.neuron.name); if (neuronIx < 0) // Could not find the neuron in the prefab cluster continue; @@ -127,6 +127,7 @@ namespace NanoBrain { continue; clonedSender.AddReceiver(clonedNeuron, prefabSynapse.weight); + //Debug.Log($"Add synapse {clonedCluster.name}.{clonedSender.name} -> {clonedNeuron.name} [{clonedSender.receivers.Count}]"); } else { int ix = GetNucleusIndex(prefabNuclei, prefabSynapse.neuron); @@ -137,6 +138,7 @@ namespace NanoBrain { // Copy the receivers which will also create the synapse clonedSender.AddReceiver(clonedNeuron, prefabSynapse.weight); + // Debug.Log($"Add synapse {clonedSender.name} -> {clonedNeuron.name}"); } } @@ -162,99 +164,157 @@ namespace NanoBrain { // clonedNeuron.AddReceiver(clonedReceiver, weight); // } } - /* - // Copy the siblings for clusters - for (int nucleusIx = 0; nucleusIx < prefabNuclei.Length; nucleusIx++) { - Nucleus prefabNucleus = prefabNuclei[nucleusIx]; - if (prefabNucleus is not Cluster prefabCluster) - continue; - if (prefabCluster.siblingClusters == null || prefabCluster.siblingClusters.Length == 0) - continue; + if (Application.isPlaying) { + // Only create cluster siblings at runtime + foreach (Nucleus clonedNucleus in clonedNuclei) { + if (clonedNucleus is not Cluster clonedCluster) + continue; - Cluster clonedNucleus = clonedNuclei[nucleusIx] as Cluster; - if (prefabCluster == prefabCluster.siblingClusters[0]) { - // We clone the array only for the first entry - //NucleusArray clonedArray = new(prefabReceptor.nucleiArray.Length); - Cluster[] clonedArray = new Cluster[prefabCluster.siblingClusters.Length]; - int arrayIx = 0; - foreach (Cluster prefabArrayNucleus in prefabCluster.siblingClusters) { - int arrayNucleusIx = GetNucleusIndex(prefabNuclei, prefabArrayNucleus); - if (arrayNucleusIx >= 0) { - Cluster clonedArrayNucleus = clonedNuclei[arrayNucleusIx] as Cluster; - clonedArray[arrayIx] = clonedArrayNucleus; - } - else { - Debug.LogError($" Could not find prefab nucleus {prefabNucleus.name} in the clones"); - } - arrayIx++; - } - clonedNucleus.siblingClusters = clonedArray; - } - else { - // The others will refer to the array created for the first nucleus in the array - int firstNucleusIx = GetNucleusIndex(prefabNuclei, prefabCluster.siblingClusters[0]); - Cluster clonedFirstNucleus = clonedNuclei[firstNucleusIx] as Cluster; - clonedNucleus.siblingClusters = clonedFirstNucleus.siblingClusters; - } - } - - /* - // Collect the subclusters - List subClusters = new(); - foreach (Nucleus nucleus in prefabNuclei) { - foreach (Synapse synapse in nucleus.synapses) { - Nucleus synapseNucleus = synapse.neuron; - Cluster subCluster = synapseNucleus.parent; - if (subCluster is null || - synapseNucleus.clusterPrefab == this.clusterPrefab) { - - continue; - } - // if (synapseNucleus is not Cluster subCluster) - // continue; - if (subClusters.Contains(subCluster)) - continue; - subClusters.Add(subCluster); - } - } - // Create the subcluster instances - foreach (Cluster subCluster in subClusters) { - for (int ix = 0; ix < subCluster.instanceCount; ix++) { - // create the new instance - Cluster clusterInstance = new(subCluster.prefab); - // connect it - foreach ((Neuron sender, Nucleus receiver) in subCluster.CollectConnections()) { - int receiverIx = GetNucleusIndex(prefabNuclei, receiver); - if (receiverIx < 0) - continue; - - if (clonedNuclei[receiverIx] is not Nucleus clonedReceiver) - continue; - - // Find the synapse for the weight - float weight = 1; - foreach (Synapse synapse in receiver.synapses) { - // Find the weight for this synapse - if (synapse.neuron == sender) { - weight = synapse.weight; - break; - } - } - - if (clusterInstance.GetNucleus(sender.name) is not Neuron clonedSender) - continue; - - clonedSender.AddReceiver(clonedReceiver, weight); - } - } - } - */ - - foreach (Nucleus nucleus in this.clusterNuclei) { - if (nucleus is Cluster clonedSubCluster) - RestoreAllExternalReceivers(clonedSubCluster, this.prefab, this); + List siblings = new() { + clonedCluster + }; + for (int instanceIx = 1; instanceIx < clonedCluster.instanceCount; instanceIx++) { + // Create another sibling + Debug.Log($"create {clonedCluster.prefab.name} sibling"); + Cluster sibling = new(clonedCluster.prefab, this) { + name = $"{clonedCluster.baseName}: {instanceIx}", + clusterPrefab = this.clusterPrefab, + instanceCount = this.instanceCount, + }; + siblings.Add(sibling); + CopyAllExternalReceivers(clonedCluster, sibling, clonedCluster.prefab, this); + } + Cluster[] siblingClusters = siblings.ToArray(); + foreach (Cluster sibling in siblings) + sibling.siblingClusters = siblingClusters; + } } + /* + for (int nucleusIx = 0; nucleusIx < clonedNuclei.Length; nucleusIx++) { + Nucleus prefabNucleus = prefabNuclei[nucleusIx]; + if (prefabNucleus is not Cluster prefabCluster) + continue; + + if (prefabCluster.instanceCount <= 1) + continue; + + Cluster clonedNucleus = clonedNuclei[nucleusIx] as Cluster; + if (prefabCluster == prefabCluster.siblingClusters[0]) { + // We clone the array only for the first entry + //NucleusArray clonedArray = new(prefabReceptor.nucleiArray.Length); + Cluster[] clonedArray = new Cluster[prefabCluster.siblingClusters.Length]; + int arrayIx = 0; + foreach (Cluster prefabArrayNucleus in prefabCluster.siblingClusters) { + int arrayNucleusIx = GetNucleusIndex(prefabNuclei, prefabArrayNucleus); + if (arrayNucleusIx >= 0) { + Cluster clonedArrayNucleus = clonedNuclei[arrayNucleusIx] as Cluster; + clonedArray[arrayIx] = clonedArrayNucleus; + } + else { + Debug.LogError($" Could not find prefab nucleus {prefabNucleus.name} in the clones"); + } + arrayIx++; + } + clonedNucleus.siblingClusters = clonedArray; + } + else { + // The others will refer to the array created for the first nucleus in the array + int firstNucleusIx = GetNucleusIndex(prefabNuclei, prefabCluster.siblingClusters[0]); + Cluster clonedFirstNucleus = clonedNuclei[firstNucleusIx] as Cluster; + clonedNucleus.siblingClusters = clonedFirstNucleus.siblingClusters; + } + } + } + + /* + // Collect the subclusters + List subClusters = new(); + foreach (Nucleus nucleus in prefabNuclei) { + foreach (Synapse synapse in nucleus.synapses) { + Nucleus synapseNucleus = synapse.neuron; + Cluster subCluster = synapseNucleus.parent; + if (subCluster is null || + synapseNucleus.clusterPrefab == this.clusterPrefab) { + + continue; + } + // if (synapseNucleus is not Cluster subCluster) + // continue; + if (subClusters.Contains(subCluster)) + continue; + subClusters.Add(subCluster); + } + } + // Create the subcluster instances + foreach (Cluster subCluster in subClusters) { + for (int ix = 0; ix < subCluster.instanceCount; ix++) { + // create the new instance + Cluster clusterInstance = new(subCluster.prefab); + // connect it + foreach ((Neuron sender, Nucleus receiver) in subCluster.CollectConnections()) { + int receiverIx = GetNucleusIndex(prefabNuclei, receiver); + if (receiverIx < 0) + continue; + + if (clonedNuclei[receiverIx] is not Nucleus clonedReceiver) + continue; + + // Find the synapse for the weight + float weight = 1; + foreach (Synapse synapse in receiver.synapses) { + // Find the weight for this synapse + if (synapse.neuron == sender) { + weight = synapse.weight; + break; + } + } + + if (clusterInstance.GetNucleus(sender.name) is not Neuron clonedSender) + continue; + + clonedSender.AddReceiver(clonedReceiver, weight); + } + } + } + */ + + // foreach (Nucleus nucleus in this.clusterNuclei) { + // if (nucleus is Cluster clonedSubCluster) + // RestoreAllExternalReceivers(clonedSubCluster, this.prefab, this); + // } + } + + private void CloneSynapses(Neuron prefabNeuron, Neuron clonedNeuron) { + foreach (Synapse prefabSynapse in prefabNeuron.synapses) { + Neuron synapseNeuron = prefabSynapse.neuron; + if (synapseNeuron.clusterPrefab != null && synapseNeuron.clusterPrefab != this.prefab) { + // Neuron is in another cluster, find the cloned cluster first + ClusterPrefab prefabCluster = synapseNeuron.clusterPrefab; + Cluster clonedCluster = this.clusterNuclei.Find(n => n.name == prefabCluster.name) as Cluster; + if (clonedCluster == null) + continue; + + // Now find the neuron in that cloned cluster + int neuronIx = GetNucleusIndex(prefabCluster.nuclei, prefabSynapse.neuron.name); + if (neuronIx < 0) + // Could not find the neuron in the prefab cluster + continue; + if (clonedCluster.clusterNuclei[neuronIx] is not Neuron clonedSender) + // Could not find the neuron in the cloned cluster + continue; + + clonedSender.AddReceiver(clonedNeuron, prefabSynapse.weight); + //Debug.Log($"Add synapse {clonedCluster.name}.{clonedSender.name} -> {clonedNeuron.name} [{clonedSender.receivers.Count}]"); + } + else { + Neuron clonedSender = this.clusterNuclei.Find(n => n.name == prefabSynapse.neuron.name) as Neuron; + // Copy the receivers which will also create the synapse + clonedSender.AddReceiver(clonedNeuron, prefabSynapse.weight); + // Debug.Log($"Add synapse {clonedSender.name} -> {clonedNeuron.name}"); + } + } + } /// @@ -347,33 +407,29 @@ namespace NanoBrain { Cluster clone = new(this.prefab, parent) { name = this.name, clusterPrefab = this.clusterPrefab, + instanceCount = this.instanceCount, }; // Somehow siblingClusters should be cloned too. Believe I do this in ClonePrefab right now. return clone; } - private static void RestoreAllExternalReceivers(Cluster clonedCluster, ClusterPrefab prefabParent, Cluster clonedParent) { - int clonedClusterIx = GetNucleusIndex(clonedParent.clusterNuclei, clonedCluster); - if (prefabParent.nuclei[clonedClusterIx] is not Cluster sourceCluster) - return; + private static void CopyAllExternalReceivers(Cluster sourceCluster, Cluster sibling, ClusterPrefab prefabParent, Cluster clonedParent) { for (int nucleusIx = 0; nucleusIx < sourceCluster.clusterNuclei.Count; nucleusIx++) { Nucleus sourceNucleus = sourceCluster.clusterNuclei[nucleusIx]; if (sourceNucleus is not Neuron sourceNeuron) continue; - if (clonedCluster.clusterNuclei[nucleusIx] is not Neuron clonedNeuron) + if (sibling.clusterNuclei[nucleusIx] is not Neuron clonedNeuron) continue; - // copy the receivers (and thus synapses) from the source to the clone + // copy the receivers (and thus synapses) from the source to the sibling foreach (Nucleus receiver in sourceNeuron.receivers) { - int ix = GetNucleusIndex(prefabParent.nuclei, receiver); + int ix = GetNucleusIndex(clonedParent.clusterNuclei, receiver); if (ix < 0 || ix >= clonedParent.clusterNuclei.Count) continue; - Nucleus clonedReceiver = clonedParent.clusterNuclei[ix]; - // Find the synapse for the weight float weight = 1; foreach (Synapse synapse in receiver.synapses) { @@ -384,10 +440,11 @@ namespace NanoBrain { } } - clonedNeuron.AddReceiver(clonedReceiver, weight); - // Debug.Log($"external: {clonedReceiver.name} receives from {clonedNeuron.name} {clonedNeuron.GetHashCode()}"); + clonedNeuron.AddReceiver(receiver, weight); + Debug.Log($"external: {receiver.name} receives from {clonedNeuron.name} {clonedNeuron.GetHashCode()}"); } } + } protected int GetNucleusIndex(Nucleus[] nuclei, Nucleus nucleus) { @@ -402,13 +459,25 @@ namespace NanoBrain { int i = 0; foreach (Nucleus nucleiElement in nuclei) { //for (int i = 0; i < nuclei.Length; i++) { - if (nucleus == nucleiElement) + if (nucleiElement == nucleus) return i; i++; } return -1; } + public static int GetNucleusIndex(List nuclei, string nucleusName) { + int i = 0; + foreach (Nucleus nucleiElement in nuclei) { + //for (int i = 0; i < nuclei.Length; i++) { + if (nucleiElement.name == nucleusName) + return i; + i++; + } + return -1; + } + + #endregion Init #region Cluster Array @@ -470,6 +539,7 @@ namespace NanoBrain { return cluster; Cluster selectedCluster = SelectCluster(); + selectedCluster.name = baseName + ": " + thingName; thingClusters[thingId] = selectedCluster; return selectedCluster; } @@ -528,10 +598,6 @@ namespace NanoBrain { #endregion ClusterArray - - - //public Dictionary nucleiDict = new(); - public List _inputs = null; public virtual List inputs { get { @@ -550,22 +616,33 @@ namespace NanoBrain { public Dictionary> computeOrders = new(); private void ComputeOrders() { - foreach (Nucleus input in this._inputs) - computeOrders[input] = TopologicalSort2(input); + foreach (Nucleus nucleus in this.clusterNuclei) { + // if (nucleus is Cluster cluster) { + // List synapses = this.CollectSynapsesTo(cluster); + // foreach (Synapse synapse in synapses) { + // computeOrders[synapse.neuron] = TopologicalSort2(synapse.neuron); + // Debug.Log($"{this.baseName}: Order for {cluster.baseName}.{synapse.neuron.name}"); + // } + // // List receivers = cluster.CollectReceivers(); + // // foreach (Nucleus receiver in receivers) + // // computeOrders[receiver] = TopologicalSort2(receiver); + // } + // else { + computeOrders[nucleus] = TopologicalSort2(nucleus); + Debug.Log($"{this.baseName} Order for {nucleus.name}"); + // } + } } private List TopologicalSort2(Nucleus startNode) { Dictionary inDegree = new(); - HashSet visited = new(); - - // Initialize in-degrees and mark all nodes as unvisited - foreach (Nucleus node in this.clusterNuclei) - inDegree[node] = 0; + //HashSet visited = new(); // Calculate in-degrees for all nodes reachable from the start node - Queue queue = new Queue(); + Queue queue = new(); queue.Enqueue(startNode); - visited.Add(startNode); + //visited.Add(startNode); + inDegree[startNode] = 0; while (queue.Count > 0) { Nucleus current = queue.Dequeue(); @@ -575,25 +652,24 @@ namespace NanoBrain { else if (current is Cluster cluster) receivers = cluster.CollectReceivers(); - // if (current is Neuron neuron) { foreach (Nucleus receiver in receivers) { - if (!visited.Contains(receiver)) { - visited.Add(receiver); + if (!inDegree.ContainsKey(receiver)) { + //visited.Add(receiver); + inDegree[receiver] = 0; queue.Enqueue(receiver); } inDegree[receiver]++; } - // } } // Perform topological sort on all reachable nodes queue.Clear(); - foreach (Nucleus node in visited) { + foreach (Nucleus node in inDegree.Keys) { if (inDegree[node] == 0) queue.Enqueue(node); } - List sortedOrder = new List(); + List sortedOrder = new(); while (queue.Count > 0) { Nucleus current = queue.Dequeue(); sortedOrder.Add(current); // Process the node @@ -604,21 +680,18 @@ namespace NanoBrain { else if (current is Cluster cluster) receivers = cluster.CollectReceivers(); - //if (current is Neuron neuron) { - foreach (Nucleus receiver in receivers) { - if (visited.Contains(receiver)) { + if (inDegree.ContainsKey(receiver)) { inDegree[receiver]--; if (inDegree[receiver] == 0) // If all dependencies resolved queue.Enqueue(receiver); } } - //} } // Check for cycles in the graph - if (sortedOrder.Count != visited.Count) - throw new InvalidOperationException("Graph is not a DAG; a cycle exists."); + // if (sortedOrder.Count != visited.Count) + // throw new InvalidOperationException("Graph is not a DAG; a cycle exists."); return sortedOrder; } @@ -643,6 +716,9 @@ namespace NanoBrain { return this._outputs; } } + public void RefreshOutputs() { + this._outputs = null; + } public bool TryGetNucleus(string nucleusName, out Nucleus foundNucleus) { foreach (Nucleus receptor in this.clusterNuclei) { @@ -685,18 +761,39 @@ namespace NanoBrain { } } + public bool DeleteNucleus(Nucleus nucleus) { + if (this.clusterNuclei.Contains(nucleus) == false) { + // Try to find the nucleus by name + if (TryGetNucleus(nucleus.name, out nucleus) == false) + return false; + } + + Neuron.Delete(nucleus); + int nucleusIx = this.clusterNuclei.IndexOf(nucleus); + this.clusterNuclei.Remove(nucleus); + this.prefab.nuclei.RemoveAt(nucleusIx); + RefreshOutputs(); + + return true; + } + #region Receivers - public virtual List CollectReceivers() { + public virtual List CollectReceivers(bool removeDuplicates = false) { List receivers = new(); foreach (Nucleus outputNucleus in this.clusterNuclei) { if (outputNucleus is not Neuron output) continue; + // Debug.Log($"output {this.name} {outputNucleus.name}"); foreach (Nucleus receiver in output.receivers) { - // Only add receivers outside this cluster - if (receiver.clusterPrefab != this.prefab) - receivers.Add(receiver); + // Debug.Log($"output {receiver.name}"); + // Only add receivers outside this cluster + if (receiver.clusterPrefab != this.prefab) { + if (removeDuplicates == false || receivers.Contains(receiver) == false) + // Debug.Log($" YES"); + receivers.Add(receiver); + } } } return receivers; @@ -717,6 +814,19 @@ namespace NanoBrain { } return connections; } + public List CollectSynapsesTo(Cluster otherCluster) { + List collectedSynapses = new(); + + foreach (Nucleus nucleus in this.clusterNuclei) { + if (nucleus is not Neuron neuron) + continue; + foreach (Synapse synapse in nucleus.synapses) { + if (synapse.neuron.parent == otherCluster) + collectedSynapses.Add(synapse); + } + } + return collectedSynapses; + } public void MoveReceivers(Cluster newCluster) { Debug.Log($"Move receivers for {this.name} to {newCluster.name}"); @@ -752,23 +862,31 @@ namespace NanoBrain { // no bias+synapse input state calculation for now... if (this.computeOrders.ContainsKey(startNucleus) == false) { - //Debug.LogError($"{this.name} compute orders does not contain an order for {startNucleus.name}"); + Debug.LogError($"{this.name} compute orders does not contain an order for {startNucleus.name}"); return; } List computeOrder = this.computeOrders[startNucleus]; - if (startNucleus.trace) - Debug.Log($"Update from {startNucleus.name}"); + //if (startNucleus.trace) + Debug.Log($"Update from {startNucleus.name}"); foreach (Nucleus nucleus in computeOrder) { if (nucleus is not Cluster) { nucleus.UpdateStateIsolated(); - if (startNucleus.trace && nucleus is Neuron neuron) - Debug.Log($" {nucleus.name}[{nucleus.GetHashCode()}]"); // = {neuron.outputValue}"); + //if (startNucleus.trace && nucleus is Neuron neuron) + Debug.Log($" {nucleus.name}"); + if (nucleus is Neuron neuron) { + foreach (Nucleus receiver in neuron.receivers) { + if (receiver.parent != this) { + Debug.Log($" External: {receiver.parent.name}.{receiver.name}"); + receiver.parent.UpdateFromNucleus(receiver); + } + } + } } } // continue in parent - this.parent?.UpdateFromNucleus(this); + //this.parent?.UpdateFromNucleus(this); UpdateNuclei(); } diff --git a/Runtime/Scripts/Core/Neuron.cs b/Runtime/Scripts/Core/Neuron.cs index d0f9473..a4bbed8 100644 --- a/Runtime/Scripts/Core/Neuron.cs +++ b/Runtime/Scripts/Core/Neuron.cs @@ -33,8 +33,10 @@ namespace NanoBrain { public Neuron(ClusterPrefab prefab, string name) { this.clusterPrefab = prefab; this.name = name; - if (this.clusterPrefab != null) + if (this.clusterPrefab != null) { this.clusterPrefab.nuclei.Add(this); + this.clusterPrefab.RefreshOutputs(); + } else Debug.LogError("No prefab when adding neuron to prefab"); } @@ -264,15 +266,19 @@ namespace NanoBrain { } public static void Delete(Nucleus nucleus) { - foreach (Synapse synapse in nucleus.synapses) { - if (synapse.neuron is Neuron synapse_nucleus) { - if (synapse_nucleus.receivers.Count > 1) { - // there is another nucleus feeding into this input nucleus - synapse_nucleus.receivers.RemoveAll(r => r == nucleus); - } - else { - // No other links, delete it. - Neuron.Delete(synapse_nucleus); + if (nucleus == null) + return; + if (nucleus.synapses != null) { + foreach (Synapse synapse in nucleus.synapses) { + if (synapse.neuron is Neuron synapse_nucleus) { + if (synapse_nucleus.receivers.Count > 1) { + // there is another nucleus feeding into this input nucleus + synapse_nucleus.receivers.RemoveAll(r => r == nucleus); + } + else { + // No other links, delete it. + Neuron.Delete(synapse_nucleus); + } } } } @@ -520,6 +526,8 @@ namespace NanoBrain { public virtual void AddReceiver(Nucleus receiverToAdd, float weight = 1) { this._receivers.Add(receiverToAdd); receiverToAdd.AddSynapse(this, weight); + //Debug.Log($"Add synapse {this.clusterPrefab.name}.{this.name} -> {receiverToAdd.name} --- [{this.receivers.Count}]"); + } public virtual void RemoveReceiver(Nucleus receiverToRemove) { diff --git a/Runtime/Scripts/Core/Synapse.cs b/Runtime/Scripts/Core/Synapse.cs index e71130d..9e685c4 100644 --- a/Runtime/Scripts/Core/Synapse.cs +++ b/Runtime/Scripts/Core/Synapse.cs @@ -29,11 +29,7 @@ namespace NanoBrain { this.weight = weight; } - public bool isSleeping { - get { - return this.neuron.isSleeping; - } - } + public bool isSleeping => this.neuron.isSleeping; } } \ No newline at end of file