From ecab0b05c55a379f4dcd7bb6633246848522c455 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Tue, 28 Apr 2026 09:56:48 +0200 Subject: [PATCH 01/10] Fix neuron name editor --- Editor/ClusterEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Editor/ClusterEditor.cs b/Editor/ClusterEditor.cs index 1498a79..44ac86f 100644 --- a/Editor/ClusterEditor.cs +++ b/Editor/ClusterEditor.cs @@ -169,7 +169,7 @@ namespace NanoBrain { GUILayout.Label(nucleusType, headerStyle); // Nucleus name - if (this.currentNucleus.parent is Cluster parentCluster) { + if (this.currentNucleus is Cluster parentCluster) { EditorGUILayout.BeginHorizontal(); if (GUILayout.Button(this.currentNucleus.parent.name)) OnClusterClick(parentCluster); From 805b0f8489e64236b7430cb69f05e11365493a2e Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Tue, 28 Apr 2026 10:44:46 +0200 Subject: [PATCH 02/10] Fix broken outputpop references --- Editor/ClusterEditor.cs | 17 +++++++++-------- Editor/ClusterViewer.cs | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Editor/ClusterEditor.cs b/Editor/ClusterEditor.cs index 44ac86f..96f1b75 100644 --- a/Editor/ClusterEditor.cs +++ b/Editor/ClusterEditor.cs @@ -84,8 +84,8 @@ namespace NanoBrain { 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; + // outputsPopup.choices = this.prefab.outputs.Select(output => output.name).ToList(); + // outputsPopup.value = newOutput.name; this.currentNucleus = newOutput; } @@ -185,7 +185,7 @@ namespace NanoBrain { if (newName != this.currentNucleus.name) { this.currentNucleus.name = newName; this.prefab.RefreshOutputs(); - outputsPopup.choices = this.prefab.outputs.Select(output => output.name).ToList(); + // outputsPopup.choices = this.prefab.outputs.Select(output => output.name).ToList(); anythingChanged = true; } } @@ -537,15 +537,16 @@ namespace NanoBrain { } this.prefab.nuclei.Remove(nucleus); - if (outputsPopup.value == nucleus.name) { - this.prefab.RefreshOutputs(); - outputsPopup.choices = this.prefab.outputs.Select(output => output.name).ToList(); - outputsPopup.index = 0; - } + // if (outputsPopup.value == nucleus.name) { + // this.prefab.RefreshOutputs(); + // // outputsPopup.choices = this.prefab.outputs.Select(output => output.name).ToList(); + // // outputsPopup.index = 0; + // } Neuron.Delete(nucleus); this.currentNucleus = this.prefab.output; + this.selectedOutput = this.currentNucleus; } Nucleus.Type selectedType = Nucleus.Type.None; diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index af0eb48..11013d3 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -25,7 +25,7 @@ namespace NanoBrain { protected VisualElement topMenuContainer; protected ScrollView scrollView; protected IMGUIContainer graphContainer; - protected readonly PopupField outputsPopup; + //protected readonly PopupField outputsPopup; public enum Mode { Focus, From fc8caa8a293f37ef30aaf538e0f67e129417464a Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Tue, 28 Apr 2026 11:30:40 +0200 Subject: [PATCH 03/10] Fix adding/removing cluster outputs --- Editor/ClusterEditor.cs | 162 +++++++++++++++----------------- Runtime/Scripts/Core/Cluster.cs | 23 ++++- Runtime/Scripts/Core/Neuron.cs | 4 +- 3 files changed, 100 insertions(+), 89 deletions(-) diff --git a/Editor/ClusterEditor.cs b/Editor/ClusterEditor.cs index 96f1b75..b3b3aad 100644 --- a/Editor/ClusterEditor.cs +++ b/Editor/ClusterEditor.cs @@ -70,24 +70,7 @@ namespace NanoBrain { // 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 +134,74 @@ 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 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.currentNucleus 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) { + 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) //(this.currentNucleus is Cluster cluster) + 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 +210,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); @@ -430,15 +436,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,15 +532,12 @@ 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; - // } - - Neuron.Delete(nucleus); + // this.prefab.nuclei.Remove(nucleus); + // Neuron.Delete(nucleus); + this.prefab.RefreshOutputs(); + this.currentNucleus = this.prefab.output; this.selectedOutput = this.currentNucleus; diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index 0b89721..9e905f9 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -528,10 +528,6 @@ namespace NanoBrain { #endregion ClusterArray - - - //public Dictionary nucleiDict = new(); - public List _inputs = null; public virtual List inputs { get { @@ -643,6 +639,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,6 +684,22 @@ 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() { diff --git a/Runtime/Scripts/Core/Neuron.cs b/Runtime/Scripts/Core/Neuron.cs index d0f9473..934fb63 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"); } From 335dae788bde23d3c239934ab321a479cd836d20 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Tue, 28 Apr 2026 11:34:41 +0200 Subject: [PATCH 04/10] Fix deleting neuron without synapses --- Runtime/Scripts/Core/Neuron.cs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Runtime/Scripts/Core/Neuron.cs b/Runtime/Scripts/Core/Neuron.cs index 934fb63..88fae5b 100644 --- a/Runtime/Scripts/Core/Neuron.cs +++ b/Runtime/Scripts/Core/Neuron.cs @@ -266,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); + } } } } From b2bc92b05f7c5ca8dbaf8005b9c6418915f0590c Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Tue, 28 Apr 2026 14:28:01 +0200 Subject: [PATCH 05/10] Multiple players working-ish --- Editor/ClusterEditor.cs | 68 +++++++++++++++++++-------------- Editor/ClusterViewer.cs | 7 +++- Runtime/Scripts/Core/Cluster.cs | 32 ++++++++++------ 3 files changed, 66 insertions(+), 41 deletions(-) diff --git a/Editor/ClusterEditor.cs b/Editor/ClusterEditor.cs index b3b3aad..9b33553 100644 --- a/Editor/ClusterEditor.cs +++ b/Editor/ClusterEditor.cs @@ -64,6 +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 = this.prefab.GetNucleus(value.name); + // int nucleusIx = this.prefab.nuclei.IndexOf(base.currentNucleus); + // if (nucleusIx >= 0) + // this.currentPrefabNucleus = this.prefab.nuclei[nucleusIx]; + // else { + // Debug.LogWarning("Could not find nucleus in prefab"); + // this.currentPrefabNucleus = this.prefab.GetNucleus(value.name); + // } + } + } public GraphEditor(ClusterPrefab prefab) : base(prefab.output.parent) { this.prefab = prefab; @@ -146,10 +162,10 @@ namespace NanoBrain { return; } else { - GUIStyle headerStyle = new(EditorStyles.boldLabel) { - alignment = TextAnchor.MiddleLeft, - margin = new RectOffset(10, 0, 4, 4) - }; + 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); @@ -170,6 +186,9 @@ namespace NanoBrain { 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(); @@ -222,7 +241,7 @@ namespace NanoBrain { Nucleus newOutput = new Neuron(this.prefab, "New Output"); // Regenerate the temporary clsuter instance // See also the constructor - this.currentCluster = new (this.prefab); + this.currentCluster = new(this.prefab); this.currentNucleus = newOutput; this.selectedOutput = this.currentNucleus; } @@ -290,14 +309,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; @@ -336,24 +358,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); } } @@ -373,14 +391,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; } @@ -537,7 +547,7 @@ namespace NanoBrain { // this.prefab.nuclei.Remove(nucleus); // Neuron.Delete(nucleus); this.prefab.RefreshOutputs(); - + this.currentNucleus = this.prefab.output; this.selectedOutput = this.currentNucleus; @@ -565,7 +575,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, @@ -595,8 +605,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 11013d3..46e303c 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -15,7 +15,12 @@ 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; protected GameObject gameObject; diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index 9e905f9..52ef65c 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -93,6 +93,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 +108,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; @@ -402,13 +400,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 @@ -690,7 +700,7 @@ namespace NanoBrain { if (TryGetNucleus(nucleus.name, out nucleus) == false) return false; } - + Neuron.Delete(nucleus); int nucleusIx = this.clusterNuclei.IndexOf(nucleus); this.clusterNuclei.Remove(nucleus); From ffcf4207150f80854834ba02acc13700b0c1bf41 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Tue, 28 Apr 2026 17:18:31 +0200 Subject: [PATCH 06/10] Fix cloned external connections --- Editor/ClusterEditor.cs | 15 +++------------ Runtime/Scripts/Core/Cluster.cs | 20 ++++++++++++++------ Runtime/Scripts/Core/Neuron.cs | 2 ++ 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/Editor/ClusterEditor.cs b/Editor/ClusterEditor.cs index 9b33553..9ca0a95 100644 --- a/Editor/ClusterEditor.cs +++ b/Editor/ClusterEditor.cs @@ -70,14 +70,7 @@ namespace NanoBrain { get => base.currentNucleus; set { base.currentNucleus = value; - this.currentPrefabNucleus = this.prefab.GetNucleus(value.name); - // int nucleusIx = this.prefab.nuclei.IndexOf(base.currentNucleus); - // if (nucleusIx >= 0) - // this.currentPrefabNucleus = this.prefab.nuclei[nucleusIx]; - // else { - // Debug.LogWarning("Could not find nucleus in prefab"); - // this.currentPrefabNucleus = this.prefab.GetNucleus(value.name); - // } + this.currentPrefabNucleus = this.prefab.GetNucleus(value.name); } } @@ -150,7 +143,6 @@ namespace NanoBrain { if (serializedObject == null || serializedObject.targetObject == null) return; - serializedObject.Update(); GUIStyle boldTextFieldStyle = new(EditorStyles.textField) { @@ -171,7 +163,7 @@ namespace NanoBrain { GUILayout.Label(nucleusType, headerStyle); // Nucleus name - Cluster cluster = this.currentNucleus as Cluster; + Cluster cluster = this.currentPrefabNucleus as Cluster; if (cluster != null) { EditorGUILayout.BeginHorizontal(); if (GUILayout.Button(this.currentNucleus.parent.name)) @@ -212,7 +204,7 @@ namespace NanoBrain { if (this.currentNucleus is MemoryCell memory) MemoryCellInspector(memory, ref anythingChanged); // Cluster - else if (cluster != null) //(this.currentNucleus is Cluster cluster) + else if (cluster != null) ClusterInspector(cluster, ref anythingChanged); // Other else @@ -266,7 +258,6 @@ namespace NanoBrain { if (GUILayout.Button("Add")) { Undo.RecordObject(prefabAsset, "Array add " + prefabAsset.name); - //cluster.AddInstance(this.prefab); cluster.AddInstance(); anythingChanged = true; } diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index 52ef65c..2f061ab 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(); @@ -125,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); @@ -135,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}"); } } @@ -249,10 +253,10 @@ namespace NanoBrain { } */ - foreach (Nucleus nucleus in this.clusterNuclei) { - if (nucleus is Cluster clonedSubCluster) - RestoreAllExternalReceivers(clonedSubCluster, this.prefab, this); - } + // foreach (Nucleus nucleus in this.clusterNuclei) { + // if (nucleus is Cluster clonedSubCluster) + // RestoreAllExternalReceivers(clonedSubCluster, this.prefab, this); + // } } /// @@ -718,10 +722,14 @@ namespace NanoBrain { if (outputNucleus is not Neuron output) continue; + // Debug.Log($"output {this.name} {outputNucleus.name}"); foreach (Nucleus receiver in output.receivers) { + // Debug.Log($"output {receiver.name}"); // Only add receivers outside this cluster - if (receiver.clusterPrefab != this.prefab) + if (receiver.clusterPrefab != this.prefab) { + // Debug.Log($" YES"); receivers.Add(receiver); + } } } return receivers; diff --git a/Runtime/Scripts/Core/Neuron.cs b/Runtime/Scripts/Core/Neuron.cs index 88fae5b..a4bbed8 100644 --- a/Runtime/Scripts/Core/Neuron.cs +++ b/Runtime/Scripts/Core/Neuron.cs @@ -526,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) { From 36f876c0d89554d6c137239fc7f741e57be2bd8f Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Tue, 28 Apr 2026 17:22:51 +0200 Subject: [PATCH 07/10] draw external receivers only once --- Editor/ClusterViewer.cs | 5 ++--- Runtime/Scripts/Core/Cluster.cs | 9 +++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index 46e303c..41dee0c 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -327,7 +327,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; @@ -357,7 +357,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) @@ -733,7 +732,7 @@ namespace NanoBrain { if (nucleus == this.currentNucleus) { if (Application.isPlaying) { if (nucleus is Cluster) - expandArray = !expandArray; + expandArray = !expandArray; else expandArray = false; } diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index 2f061ab..c358f14 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -716,7 +716,7 @@ namespace NanoBrain { #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) @@ -725,10 +725,11 @@ namespace NanoBrain { // Debug.Log($"output {this.name} {outputNucleus.name}"); foreach (Nucleus receiver in output.receivers) { // Debug.Log($"output {receiver.name}"); - // Only add receivers outside this cluster + // Only add receivers outside this cluster if (receiver.clusterPrefab != this.prefab) { - // Debug.Log($" YES"); - receivers.Add(receiver); + if (removeDuplicates && receivers.Contains(receiver) == false) + // Debug.Log($" YES"); + receivers.Add(receiver); } } } From 04010903f512d2c171ce5afe2721224bb0abc399 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Tue, 28 Apr 2026 17:46:06 +0200 Subject: [PATCH 08/10] Created runtime sibling, but not the synapses yet --- Runtime/Scripts/Core/Cluster.cs | 270 +++++++++++++++++++------------- 1 file changed, 165 insertions(+), 105 deletions(-) diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index c358f14..c027f90 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -164,94 +164,117 @@ namespace NanoBrain { // clonedNeuron.AddReceiver(clonedReceiver, weight); // } } + + if (Application.isPlaying) { + // Only create cluster siblings at runtime + foreach (Nucleus clonedNucleus in clonedNuclei) { + if (clonedNucleus is not Cluster clonedCluster) + continue; + + // if (clonedCluster.instanceCount <= 1) + // continue; + + for (int instanceIx = 1; instanceIx < clonedCluster.instanceCount; instanceIx++) { + //ClusterPrefab prefabCluster = clonedCluster.prefab; + // Create another sibling + Debug.Log($"create {clonedCluster.prefab.name} sibling"); + Cluster sibling = new(clonedCluster.prefab, this) { + name = this.name, + clusterPrefab = this.clusterPrefab, + instanceCount = this.instanceCount, + }; + RestoreAllExternalReceivers(clonedCluster, clonedCluster.prefab, this); + } + } + } /* - // Copy the siblings for clusters - for (int nucleusIx = 0; nucleusIx < prefabNuclei.Length; nucleusIx++) { - Nucleus prefabNucleus = prefabNuclei[nucleusIx]; - if (prefabNucleus is not Cluster prefabCluster) + 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 (prefabCluster.siblingClusters == null || prefabCluster.siblingClusters.Length == 0) + } + // if (synapseNucleus is not Cluster subCluster) + // continue; + if (subClusters.Contains(subCluster)) 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; - } + subClusters.Add(subCluster); } - - /* - // 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) { - + } + // 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 (synapseNucleus is not Cluster subCluster) - // continue; - if (subClusters.Contains(subCluster)) + + if (clonedNuclei[receiverIx] is not Nucleus clonedReceiver) 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; - } + // 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); } + + 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) @@ -259,6 +282,38 @@ namespace NanoBrain { // } } + 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}"); + } + } + + } + /// /// Sort the nuclei in a correct evaluation order /// @@ -349,6 +404,7 @@ 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. @@ -356,40 +412,44 @@ namespace NanoBrain { } 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; - 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) - continue; + // int clonedClusterIx = GetNucleusIndex(clonedParent.clusterNuclei, clonedCluster); + // if (prefabParent.nuclei[clonedClusterIx] is not Cluster sourceCluster) + // return; - // copy the receivers (and thus synapses) from the source to the clone - foreach (Nucleus receiver in sourceNeuron.receivers) { - int ix = GetNucleusIndex(prefabParent.nuclei, receiver); - if (ix < 0 || ix >= clonedParent.clusterNuclei.Count) - continue; + // for (int nucleusIx = 0; nucleusIx < sourceCluster.clusterNuclei.Count; nucleusIx++) { + // Nucleus sourceNucleus = sourceCluster.clusterNuclei[nucleusIx]; + // if (sourceNucleus is not Neuron sourceNeuron) + // continue; - Nucleus clonedReceiver = clonedParent.clusterNuclei[ix]; + // if (clonedCluster.clusterNuclei[nucleusIx] is not Neuron clonedNeuron) + // 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 == sourceNucleus) { - weight = synapse.weight; - break; - } - } + // // copy the receivers (and thus synapses) from the source to the clone + // foreach (Nucleus receiver in sourceNeuron.receivers) { + // int ix = GetNucleusIndex(prefabParent.nuclei, receiver); + // if (ix < 0 || ix >= clonedParent.clusterNuclei.Count) + // continue; - clonedNeuron.AddReceiver(clonedReceiver, weight); - // Debug.Log($"external: {clonedReceiver.name} receives from {clonedNeuron.name} {clonedNeuron.GetHashCode()}"); - } - } + // Nucleus clonedReceiver = clonedParent.clusterNuclei[ix]; + + // // Find the synapse for the weight + // float weight = 1; + // foreach (Synapse synapse in receiver.synapses) { + // // Find the weight for this synapse + // if (synapse.neuron == sourceNucleus) { + // weight = synapse.weight; + // break; + // } + // } + + // clonedNeuron.AddReceiver(clonedReceiver, weight); + // Debug.Log($"external: {clonedReceiver.name} receives from {clonedNeuron.name} {clonedNeuron.GetHashCode()}"); + // } + // } + + } protected int GetNucleusIndex(Nucleus[] nuclei, Nucleus nucleus) { From af0ba68bd81c7a2c8ab3614cd8c8450ebdb01b3e Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Mon, 4 May 2026 14:08:49 +0200 Subject: [PATCH 09/10] Viewer for multi-clusters --- Editor/ClusterViewer.cs | 93 ++++++++++++++++++++++++++------- Runtime/Scripts/Core/Cluster.cs | 75 +++++++++++++------------- 2 files changed, 109 insertions(+), 59 deletions(-) diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index 41dee0c..d6c4c9f 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -22,6 +22,8 @@ namespace NanoBrain { } //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; @@ -382,6 +384,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; @@ -389,15 +395,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) @@ -411,15 +421,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); @@ -437,6 +452,37 @@ 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; + + // 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) { + Nucleus siblingNucleus = sibling.GetNucleus(nucleus.name); + Vector3 position = new(250, margin + row * spacing, 0.0f); + DrawEdge(parentPos, position); + Color color = Color.black; + DrawNucleus(siblingNucleus, 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 @@ -536,7 +582,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; @@ -730,24 +776,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 { + 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 c027f90..def7228 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -171,20 +171,23 @@ namespace NanoBrain { if (clonedNucleus is not Cluster clonedCluster) continue; - // if (clonedCluster.instanceCount <= 1) - // continue; - + List siblings = new() { + clonedCluster + }; for (int instanceIx = 1; instanceIx < clonedCluster.instanceCount; instanceIx++) { - //ClusterPrefab prefabCluster = clonedCluster.prefab; // Create another sibling Debug.Log($"create {clonedCluster.prefab.name} sibling"); Cluster sibling = new(clonedCluster.prefab, this) { - name = this.name, + name = $"{clonedCluster.baseName}: {instanceIx}", clusterPrefab = this.clusterPrefab, instanceCount = this.instanceCount, }; - RestoreAllExternalReceivers(clonedCluster, clonedCluster.prefab, this); + siblings.Add(sibling); + CopyAllExternalReceivers(clonedCluster, sibling, clonedCluster.prefab, this); } + Cluster[] siblingClusters = siblings.ToArray(); + foreach (Cluster sibling in siblings) + sibling.siblingClusters = siblingClusters; } } /* @@ -411,45 +414,37 @@ namespace NanoBrain { return clone; } - private static void RestoreAllExternalReceivers(Cluster clonedCluster, ClusterPrefab prefabParent, Cluster clonedParent) { + 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; - // int clonedClusterIx = GetNucleusIndex(clonedParent.clusterNuclei, clonedCluster); - // if (prefabParent.nuclei[clonedClusterIx] is not Cluster sourceCluster) - // return; + if (sibling.clusterNuclei[nucleusIx] is not Neuron clonedNeuron) + continue; - // for (int nucleusIx = 0; nucleusIx < sourceCluster.clusterNuclei.Count; nucleusIx++) { - // Nucleus sourceNucleus = sourceCluster.clusterNuclei[nucleusIx]; - // if (sourceNucleus is not Neuron sourceNeuron) - // continue; + // copy the receivers (and thus synapses) from the source to the sibling + foreach (Nucleus receiver in sourceNeuron.receivers) { + int ix = GetNucleusIndex(clonedParent.clusterNuclei, receiver); + if (ix < 0 || ix >= clonedParent.clusterNuclei.Count) + continue; - // if (clonedCluster.clusterNuclei[nucleusIx] is not Neuron clonedNeuron) - // 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 == sourceNucleus) { + weight = synapse.weight; + break; + } + } - // // copy the receivers (and thus synapses) from the source to the clone - // foreach (Nucleus receiver in sourceNeuron.receivers) { - // int ix = GetNucleusIndex(prefabParent.nuclei, 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) { - // // Find the weight for this synapse - // if (synapse.neuron == sourceNucleus) { - // weight = synapse.weight; - // break; - // } - // } - - // 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) { From 7ef8e42e091cd5a46bf77dfbf9b1a3b3a4422752 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Mon, 4 May 2026 16:47:58 +0200 Subject: [PATCH 10/10] multi-cluster calculation fix --- Editor/ClusterEditor.cs | 2 +- Editor/ClusterViewer.cs | 25 ++++++--- Runtime/Scripts/Core/Cluster.cs | 89 ++++++++++++++++++++++----------- Runtime/Scripts/Core/Synapse.cs | 6 +-- 4 files changed, 80 insertions(+), 42 deletions(-) diff --git a/Editor/ClusterEditor.cs b/Editor/ClusterEditor.cs index 9ca0a95..725b27d 100644 --- a/Editor/ClusterEditor.cs +++ b/Editor/ClusterEditor.cs @@ -70,7 +70,7 @@ namespace NanoBrain { get => base.currentNucleus; set { base.currentNucleus = value; - this.currentPrefabNucleus = this.prefab.GetNucleus(value.name); + this.currentPrefabNucleus = value != null ? this.prefab.GetNucleus(value.name) : null; } } diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index d6c4c9f..5f11269 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -402,7 +402,7 @@ namespace NanoBrain { // Count multiple synapses to the same neuron only once string neuronName = synapse.neuron.name; - if (synapse.neuron.parent != null) + if (synapse.neuron.parent != null) neuronName = synapse.neuron.parent.baseName + "." + neuronName; if (drawnNeuronNames.Contains(neuronName)) @@ -429,7 +429,7 @@ namespace NanoBrain { // Draw multiple synapses to the same neuron only once string neuronName = synapse.neuron.name; - if (synapse.neuron.parent != null) + if (synapse.neuron.parent != null) neuronName = synapse.neuron.parent.baseName + "." + neuronName; if (drawnNeuronNames.Contains(neuronName)) @@ -459,17 +459,30 @@ namespace NanoBrain { // 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) { - Nucleus siblingNucleus = sibling.GetNucleus(nucleus.name); + Neuron siblingNeuron = sibling.GetNucleus(nucleus.name) as Neuron; Vector3 position = new(250, margin + row * spacing, 0.0f); DrawEdge(parentPos, position); Color color = Color.black; - DrawNucleus(siblingNucleus, position, size, color); + 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 }, @@ -784,8 +797,8 @@ namespace NanoBrain { // expandArray = false; // } // else { - if (nucleus is Cluster cluster) - OnClusterClick(cluster); + if (nucleus is Cluster cluster) + OnClusterClick(cluster); // } } else if (nucleus.parent != null && this.currentNucleus != null && nucleus.parent != this.currentNucleus.parent) { diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index def7228..87366f3 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -414,7 +414,7 @@ namespace NanoBrain { return clone; } - private static void CopyAllExternalReceivers(Cluster sourceCluster, Cluster sibling, ClusterPrefab prefabParent, Cluster clonedParent) { + 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]; @@ -444,7 +444,7 @@ namespace NanoBrain { Debug.Log($"external: {receiver.name} receives from {clonedNeuron.name} {clonedNeuron.GetHashCode()}"); } } - + } protected int GetNucleusIndex(Nucleus[] nuclei, Nucleus nucleus) { @@ -539,6 +539,7 @@ namespace NanoBrain { return cluster; Cluster selectedCluster = SelectCluster(); + selectedCluster.name = baseName + ": " + thingName; thingClusters[thingId] = selectedCluster; return selectedCluster; } @@ -615,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(); @@ -640,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 @@ -669,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; } @@ -782,7 +790,7 @@ namespace NanoBrain { // Debug.Log($"output {receiver.name}"); // Only add receivers outside this cluster if (receiver.clusterPrefab != this.prefab) { - if (removeDuplicates && receivers.Contains(receiver) == false) + if (removeDuplicates == false || receivers.Contains(receiver) == false) // Debug.Log($" YES"); receivers.Add(receiver); } @@ -806,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}"); @@ -841,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/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