using System; using System.Collections.Generic; using UnityEngine; #if UNITY_MATHEMATICS using Unity.Mathematics; using static Unity.Mathematics.math; #endif namespace NanoBrain { /// /// A Cluster combines a collection of Nuclei to implement reusable behaviour /// /// A Cluster is an instantiation of a ClusterPrefab. /// Clusters can be nested inside other clusters. [Serializable] public class Cluster : Nucleus { // It may be that clusters will not be nuclei anymore in the future.... public ClusterPrefab prefab; /// /// The base name of the cluster. I don't think this is actively used at this moment /// public string baseName { get { int colonPositon = this.name.IndexOf(':'); if (colonPositon < 0) return this.name; return this.name[..colonPositon]; } } // This should not be serialized //[SerializeReference] [NonSerialized] public Cluster[] siblingClusters; // This serialization should be enough [SerializeField] public int instanceCount = 1; public Dictionary thingClusters = new(); [SerializeReference] public List nuclei = new(); // the nuclei sorted using topological sorting // to ensure that the cluster is computer in the right order public List sortedNuclei; #region Init /// /// Instantiate a new copy of a ClusterPrefab in the given parent /// /// The prefab to use /// The cluster in which this new cluster will be placed public Cluster(ClusterPrefab prefab, Cluster parent) { this.prefab = prefab; this.name = prefab.name; this.parent = parent; this.parent?.nuclei.Add(this); ClonePrefab(); _ = this.inputs; this.sortedNuclei = TopologicalSort(this.nuclei); } /// /// Add a new cluster to a ClusterPrefab /// /// The prefab to copy /// The prefab in which the new copy is placed public Cluster(ClusterPrefab prefab, ClusterPrefab parent = null) { this.prefab = prefab; this.name = prefab.name; if (parent != null) this.parent = parent.cluster; // if (this.parent.prefab != null) // this.parent.prefab.cluster.nuclei.Add(this); ClonePrefab(); _ = this.inputs; this.sortedNuclei = TopologicalSort(this.nuclei); } /// /// Clone a prefab. /// /// Strange that this does not take any parameters or return values. /// Where which the clone be found??? private void ClonePrefab() { Nucleus[] prefabNuclei = this.prefab.cluster.nuclei.ToArray(); // first clone the nuclei without their connections foreach (Nucleus nucleus in prefabNuclei) { nucleus.ShallowCloneTo(this); } Nucleus[] clonedNuclei = this.nuclei.ToArray(); // Now clone the connections for (int nucleusIx = 0; nucleusIx < prefabNuclei.Length; nucleusIx++) { Nucleus prefabNucleus = prefabNuclei[nucleusIx]; if (prefabNucleus is not Neuron prefabNeuron) continue; Nucleus clonedNucleus = clonedNuclei[nucleusIx]; if (clonedNucleus == null || clonedNucleus is not Neuron clonedNeuron) continue; foreach (Synapse prefabSynapse in prefabNeuron.synapses) { Neuron synapseNeuron = prefabSynapse.neuron; if (synapseNeuron.parent.prefab != null && synapseNeuron.parent.prefab != this.prefab) { // Neuron is in another cluster, find the cloned cluster first Cluster prefabCluster = synapseNeuron.parent; Cluster clonedCluster = this.nuclei.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.nuclei[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 { int ix = GetNucleusIndex(prefabNuclei, prefabSynapse.neuron); if (ix < 0) continue; if (clonedNuclei[ix] is not Neuron clonedSender) continue; // Copy the receivers which will also create the synapse clonedSender.AddReceiver(clonedNeuron, prefabSynapse.weight); // Debug.Log($"Add synapse {clonedSender.name} -> {clonedNeuron.name}"); } } } if (Application.isPlaying) { // Only create cluster siblings at runtime foreach (Nucleus clonedNucleus in clonedNuclei) { if (clonedNucleus is not Cluster clonedCluster) continue; 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}", parent = this.parent, instanceCount = this.instanceCount, }; siblings.Add(sibling); CopyAllExternalReceivers(clonedCluster, sibling, clonedCluster.prefab, this); } Cluster[] siblingClusters = siblings.ToArray(); foreach (Cluster sibling in siblings) sibling.siblingClusters = siblingClusters; } // Ensure that all neurons are computed to initialize bias foreach (Nucleus clonedNucleus in clonedNuclei) { if (clonedNucleus is not Cluster) clonedNucleus.UpdateStateIsolated(); } } } // private void CloneSynapses(Neuron prefabNeuron, Neuron clonedNeuron) { // foreach (Synapse prefabSynapse in prefabNeuron.synapses) { // Neuron synapseNeuron = prefabSynapse.neuron; // if (synapseNeuron.parent.prefab != null && synapseNeuron.parent.prefab != this.prefab) { // // Neuron is in another cluster, find the cloned cluster first // ClusterPrefab prefabCluster = synapseNeuron.parent.prefab; // Cluster clonedCluster = this.nuclei.Find(n => n.name == prefabCluster.name) as Cluster; // if (clonedCluster == null) // continue; // // Now find the neuron in that cloned cluster // int neuronIx = GetNucleusIndex(prefabCluster.cluster.nuclei, prefabSynapse.neuron.name); // if (neuronIx < 0) // // Could not find the neuron in the prefab cluster // continue; // if (clonedCluster.nuclei[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.nuclei.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 /// /// /// /// private List TopologicalSort(List nodes) { Dictionary inDegree = new(); foreach (Nucleus node in nodes) inDegree[node] = 0; // Initialize in-degree to zero // Calculate in-degrees foreach (Nucleus node in nodes) { if (node is Cluster cluster) { foreach (Nucleus receiver in cluster.CollectReceivers()) inDegree[receiver]++; } else if (node is Neuron neuron) { foreach (Nucleus receiver in neuron.receivers) inDegree[receiver]++; } } Queue queue = new(); foreach (Nucleus node in nodes) { if (inDegree[node] == 0) // Nodes with no dependencies queue.Enqueue(node); } // The queue basically stores all input nuclei? List sortedOrder = new(); while (queue.Count > 0) { Nucleus current = queue.Dequeue(); sortedOrder.Add(current); // Process the node if (current is Neuron neuron) { foreach (Nucleus receiver in neuron.receivers) { inDegree[receiver]--; if (inDegree[receiver] == 0) // If all dependencies resolved queue.Enqueue(receiver); } } else if (current is Cluster cluster) { foreach (Nucleus receiver in cluster.CollectReceivers()) { inDegree[receiver]--; if (inDegree[receiver] == 0) // If all dependencies resolved queue.Enqueue(receiver); } } } // Check for cycles in the graph if (sortedOrder.Count != nodes.Count) throw new InvalidOperationException("Graph is not a DAG; a cycle exists."); return sortedOrder; } public override Nucleus Clone(ClusterPrefab parent) { Cluster clone = new(this.prefab, parent); // foreach (Synapse synapse in this.synapses) { // Synapse clonedSynapse = clone.AddSynapse(synapse.neuron); // clonedSynapse.weight = synapse.weight; // } foreach (Nucleus nucleus in this.nuclei) { if (nucleus is Neuron output) { foreach (Nucleus receiver in output.receivers) { int ix = GetNucleusIndex(this.nuclei, output); Debug.Log($"{output.name} -> {receiver.name}: {ix}"); if (ix < 0) continue; if (clone.nuclei[ix] is not Neuron clonedOutput) continue; clonedOutput.AddReceiver(receiver); } } } return clone; } public override Nucleus ShallowCloneTo(Cluster parent) { // Clusters should not be cloned, but instantiated from the prefab.... Cluster clone = new(this.prefab, parent) { name = this.name, parent = this.parent, instanceCount = this.instanceCount, }; // Somehow siblingClusters should be cloned too. Believe I do this in ClonePrefab right now. return clone; } private static void CopyAllExternalReceivers(Cluster sourceCluster, Cluster sibling, ClusterPrefab prefabParent, Cluster clonedParent) { for (int nucleusIx = 0; nucleusIx < sourceCluster.nuclei.Count; nucleusIx++) { Nucleus sourceNucleus = sourceCluster.nuclei[nucleusIx]; if (sourceNucleus is not Neuron sourceNeuron) continue; if (sibling.nuclei[nucleusIx] is not Neuron clonedNeuron) continue; // copy the receivers (and thus synapses) from the source to the sibling foreach (Nucleus receiver in sourceNeuron.receivers) { if (receiver is not Neuron receiverNeuron) continue; int ix = GetNucleusIndex(clonedParent.nuclei, receiver); if (ix < 0 || ix >= clonedParent.nuclei.Count) continue; // Find the synapse for the weight float weight = 1; foreach (Synapse synapse in receiverNeuron.synapses) { // Find the weight for this synapse if (synapse.neuron == sourceNucleus) { weight = synapse.weight; break; } } clonedNeuron.AddReceiver(receiver, weight); Debug.Log($"external: {receiver.name} receives from {clonedNeuron.name} {clonedNeuron.GetHashCode()}"); } } } protected int GetNucleusIndex(Nucleus[] nuclei, Nucleus nucleus) { for (int i = 0; i < nuclei.Length; i++) { if (nucleus == nuclei[i]) return i; } return -1; } public static int GetNucleusIndex(List nuclei, Nucleus nucleus) { int i = 0; foreach (Nucleus nucleiElement in nuclei) { //for (int i = 0; i < nuclei.Length; i++) { 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 public void AddInstance() { this.instanceCount++; } public void AddInstance(ClusterPrefab prefab) { // Ensure siblingClusters exists if (this.siblingClusters == null || this.siblingClusters.Length == 0) this.siblingClusters = new Cluster[1] { this }; // Prepare the new array int newLength = this.siblingClusters.Length + 1; Cluster[] newSiblings = new Cluster[newLength]; for (int i = 0; i < newSiblings.Length - 1; i++) newSiblings[i] = this.siblingClusters[i]; Cluster newCluster = this.Clone(prefab) as Cluster; string baseName = this.name; int colonPos = baseName.IndexOf(":"); if (colonPos > 0) baseName = baseName[..colonPos]; newCluster.name = $"{baseName}: {newLength - 1}"; newSiblings[newLength - 1] = newCluster; // All siblingClusters need to user this array! foreach (Cluster sibling in newSiblings) sibling.siblingClusters = newSiblings; } public void RemoveInstance() { if (instanceCount > 1) instanceCount--; else { if (this.siblingClusters == null || this.siblingClusters.Length <= 1) return; // Prepare the new array int newLength = this.siblingClusters.Length - 1; Cluster[] newClusters = new Cluster[newLength]; for (int i = 0; i < newLength; i++) newClusters[i] = this.siblingClusters[i]; Neuron.Delete(this.siblingClusters[^1]); this.siblingClusters = newClusters; } } // public virtual Cluster GetThingCluster() { // Cluster selectedCluster = SelectCluster(); // return selectedCluster; // } // public virtual Cluster GetThingCluster(int thingId, string thingName = null) { // if (thingClusters.TryGetValue(thingId, out Cluster cluster)) // return cluster; // Cluster selectedCluster = SelectCluster(); // selectedCluster.name = baseName + ": " + thingName; // thingClusters[thingId] = selectedCluster; // return selectedCluster; // } // private Cluster SelectCluster() { // if (this.siblingClusters == null) // return this; // // Find a sleeping cluster // // foreach (Cluster cluster in this.siblingClusters) { // // if (cluster.defaultOutput.isSleeping) { // // RemoveThingCluster(cluster); // // return cluster; // // } // // } // // Find longest unused cluster // // Note this uses the default output... // Cluster unusedCluster = this.siblingClusters[0]; // for (int ix = 1; ix < this.siblingClusters.Length; ix++) { // if (this.siblingClusters[ix].defaultOutput.lastUpdate < unusedCluster.defaultOutput.lastUpdate) // unusedCluster = this.siblingClusters[ix]; // } // RemoveThingCluster(unusedCluster); // return unusedCluster; // } private void RemoveThingCluster(Cluster cluster) { List keysToRemove = new(); foreach (KeyValuePair kvp in thingClusters) { if (kvp.Value == cluster) keysToRemove.Add(kvp.Key); } foreach (int thingId in keysToRemove) thingClusters.Remove(thingId); } public bool SameSiblingsAs(Cluster[] otherSiblingClusters) { if (this.siblingClusters == null) return false; for (int ix = 0; ix < this.siblingClusters.Length; ix++) { if (this.siblingClusters[ix] != otherSiblingClusters[ix]) return false; } return true; } public void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1) { this.defaultOutput.AddReceiver(receiverToAdd, weight); // foreach (Cluster cluster in this.siblingClusters) { // cluster.defaultOutput.AddReceiver(receiverToAdd, weight); // } } #endregion ClusterArray public List _inputs = null; public virtual List inputs { get { if (this._inputs == null) { this._inputs = new(); foreach (Nucleus nucleus in this.nuclei) { if (nucleus is not Neuron neuron) continue; // inputs have no synapses if (neuron.synapses.Count == 0) this._inputs.Add(nucleus); } RefreshComputeOrders(); } return this._inputs; } } private Dictionary> _computeOrders; public Dictionary> computeOrders { get { if (_computeOrders == null || _computeOrders.Count == 0) { _computeOrders = new(); foreach (Nucleus nucleus in this.nuclei) _computeOrders[nucleus] = TopologicalSort2(nucleus); } return _computeOrders; } } public void RefreshComputeOrders() { this._computeOrders = null; } private List TopologicalSort2(Nucleus startNode) { Dictionary inDegree = new(); //HashSet visited = new(); // Calculate in-degrees for all nodes reachable from the start node Queue queue = new(); queue.Enqueue(startNode); //visited.Add(startNode); inDegree[startNode] = 0; while (queue.Count > 0) { Nucleus current = queue.Dequeue(); List receivers = null; if (current is Neuron neuron) receivers = neuron.receivers; else if (current is Cluster cluster) receivers = cluster.CollectReceivers(); foreach (Nucleus receiver in receivers) { 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 inDegree.Keys) { if (inDegree[node] == 0) queue.Enqueue(node); } List sortedOrder = new(); while (queue.Count > 0) { Nucleus current = queue.Dequeue(); sortedOrder.Add(current); // Process the node List receivers = null; if (current is Neuron neuron) receivers = neuron.receivers; else if (current is Cluster cluster) receivers = cluster.CollectReceivers(); foreach (Nucleus receiver in receivers) { 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."); return sortedOrder; } public virtual Neuron defaultOutput {//=> this.nuclei[0] as Nucleus; get { if (this.nuclei.Count > 0) return this.nuclei[0] as Neuron; return null; } } protected List _outputs = null; public List outputs { get { if (this._outputs == null || this._outputs.Count == 0) { this._outputs = new(); foreach (Nucleus nucleus in this.nuclei) { if (nucleus is Neuron neuron && neuron.receivers.Count == 0) this._outputs.Add(neuron); } } return this._outputs; } } public void RefreshOutputs() { this._outputs = null; } public bool TryGetNucleus(string nucleusName, out Nucleus foundNucleus) { foreach (Nucleus receptor in this.nuclei) { if (receptor is Nucleus nucleus) if (nucleus.name == nucleusName) { foundNucleus = nucleus; return true; } } foundNucleus = null; return false; } public Nucleus GetNucleus(string nucleusName) { int dotPosition = nucleusName.IndexOf('.'); if (dotPosition >= 0) { string clusterName = nucleusName[..dotPosition]; string clusterName0 = clusterName + ": 0"; foreach (Nucleus nucleus in this.nuclei) { if (nucleus is Cluster cluster) { if (cluster.name == clusterName || cluster.name == clusterName0) { string subNucleusName = nucleusName[(dotPosition + 1)..]; return cluster.GetNucleus(subNucleusName); } } } return null; } else { string nucleusName0 = nucleusName + ": 0"; foreach (Nucleus nucleus in this.nuclei) { if (nucleus is Cluster) { if (nucleus.name == nucleusName || nucleus.name == nucleusName0) return nucleus; } else if (nucleus.name == nucleusName) return nucleus; } return null; } } public Neuron GetNeuron(string neuronName) { foreach (Nucleus nucleus in this.nuclei) { if (nucleus is Neuron neuron && neuron.name == neuronName) return neuron; } return null; } public Neuron GetNeuron(int thingId, string neuronName, string thingName = null) { if (this.siblingClusters == null || this.siblingClusters.Length <= 1) return this.GetNeuron(neuronName); // See if we are already using a cluster for thingId if (thingClusters.TryGetValue(thingId, out Cluster cluster)) return cluster.GetNeuron(neuronName); // Find the cluster with the lowest value neuron Neuron lowestNeuron = null; foreach (Cluster sibling in this.siblingClusters) { Neuron neuron = sibling.GetNeuron(neuronName); if (lowestNeuron == null || neuron.outputMagnitude < lowestNeuron.outputMagnitude) lowestNeuron = neuron; } Cluster selectedCluster = lowestNeuron.parent; RemoveThingCluster(selectedCluster); selectedCluster.name = baseName + ": " + thingName; thingClusters[thingId] = selectedCluster; return lowestNeuron; /* // Find a sleeping cluster // foreach (Cluster cluster in this.siblingClusters) { // if (cluster.defaultOutput.isSleeping) { // RemoveThingCluster(cluster); // return cluster; // } // } // Find longest unused cluster // Note this uses the default output... Cluster unusedCluster = this.siblingClusters[0]; for (int ix = 1; ix < this.siblingClusters.Length; ix++) { if (this.siblingClusters[ix].defaultOutput.lastUpdate < unusedCluster.defaultOutput.lastUpdate) unusedCluster = this.siblingClusters[ix]; } RemoveThingCluster(unusedCluster); //return unusedCluster; Cluster cluster = GetThingCluster(thingId, thingName); Neuron neuron = cluster?.GetNeuron(neuronName); return neuron; */ } public bool DeleteNucleus(Nucleus nucleus) { if (this.nuclei.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.nuclei.IndexOf(nucleus); this.nuclei.Remove(nucleus); //this.prefab.cluster.nuclei.RemoveAt(nucleusIx); RefreshOutputs(); return true; } #region Receivers public virtual List CollectReceivers(bool removeDuplicates = false) { List receivers = new(); foreach (Nucleus outputNucleus in this.nuclei) { 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.parent.prefab != this.prefab) { if (removeDuplicates == false || receivers.Contains(receiver) == false) // Debug.Log($" YES"); receivers.Add(receiver); } } } return receivers; } public List<(Neuron, Nucleus)> CollectConnections() { List<(Neuron, Nucleus)> connections = new(); foreach (Nucleus outputNucleus in this.nuclei) { if (outputNucleus is not Neuron output) continue; foreach (Nucleus receiver in output.receivers) { // Only add receivers outside this cluster if (receiver.parent.prefab != this.prefab) connections.Add((output, receiver)); } } return connections; } public List CollectSynapsesTo(Cluster otherCluster) { List collectedSynapses = new(); foreach (Nucleus nucleus in this.nuclei) { if (nucleus is not Neuron neuron) continue; foreach (Synapse synapse in neuron.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}"); foreach (Nucleus outputNucleus in this.nuclei) { if (outputNucleus is not Neuron output) continue; // Find the existing output in the new cluster if (newCluster.GetNucleus(output.name) is not Neuron newOutput) { Debug.LogWarning($"Could not find output {this.name}.{output.name} in {newCluster.name}"); continue; } Debug.Log($"Check {this.name}.{output.name} receivers"); Nucleus[] receivers = output.receivers.ToArray(); foreach (Nucleus receiver in receivers) { if (receiver.parent.prefab != this.prefab) { // Replace synapse with new synapse // to the new cluster Debug.Log($"move {receiver.name} from {this.name}.{output.name} to {newCluster.name}.{newOutput.name}"); if (receiver is not Neuron receiverNeuron) continue; Synapse synapse = receiverNeuron.GetSynapse(output); newOutput.AddReceiver(receiver, synapse.weight); output.RemoveReceiver(receiver); } } } } #endregion Receivers #region Update public void UpdateFromNucleus(Nucleus startNucleus) { // 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}"); return; } List computeOrder = this.computeOrders[startNucleus]; foreach (Nucleus nucleus in computeOrder) { if (nucleus is not Cluster) { nucleus.UpdateStateIsolated(); 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); } } } } } UpdateNuclei(); } public override void UpdateStateIsolated() { throw new Exception("Cluster should not be updated!"); } // Don't think this does anything anymore... public override void UpdateNuclei() { foreach (Nucleus nucleus in this.nuclei) nucleus.UpdateNuclei(); } #endregion Update public void Refresh() { // This should not be needed, but somehow somewhere the parent is changed... foreach (Nucleus nucleus in this.nuclei) { // if (nucleus is not Neuron neuron) // continue; nucleus.parent = this; } RefreshOutputs(); RefreshComputeOrders(); } } }