using System; using System.IO; using System.Collections.Generic; using UnityEngine; #if UNITY_MATHEMATICS using Unity.Mathematics; using static Unity.Mathematics.math; #endif using NanoBrain.Unity; 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.... /// /// The prefab used to create this cluster /// /// Cluster should always be created from prefabs public ClusterPrefab prefab; public ClusterJson json; public TextAsset jsonAsset; //[HideInInspector] /// /// The version number of this cluster instance /// /// This is used to detect changes in the prefab which require this Cluster instance to be updated public int version; /// /// 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]; } } /// /// All cluster instance of a multi-cluster /// /// A cluster is a multi-cluster when there is more than one instance. /// The actual instances are only created at runtime. /// The value instanceCount determines how many instances will be present at runtime. //[NonSerialized] [SerializeReference] [HideInInspector] public Cluster[] instances; /// /// The number of cluster instances in a multi-cluster /// /// A cluster is a multi-clsuter when there is more than one instance. [SerializeField] public uint instanceCount = 1; /// /// The mapping from things to cluster instances /// /// In a multi-cluster each instance can be used for a thing. /// Cluster instance may also not (yet) be mapped to a thing. [NonSerialized] public Dictionary thingClusters = new(); /// /// All nuclei in this cluster /// [SerializeReference] [HideInInspector] public List nuclei = new(); #region Init public Cluster() { this.name = "Empty Cluster"; } /// /// 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, uint instanceCount = 1) { this.prefab = prefab; this.version = prefab.version; this.name = prefab.name; this.parent = parent; this.parent?.nuclei.Add(this); ClonePrefab(); if (instanceCount > 1) { List siblings = new() { this }; for (int instanceIx = 1; instanceIx < instanceCount; instanceIx++) { // Create another sibling Cluster sibling = new(prefab, this) { name = $"{this.baseName}: {instanceIx}", parent = this.parent, instanceCount = this.instanceCount, }; siblings.Add(sibling); CopyAllExternalReceivers(this, sibling, this); } Cluster[] siblingClusters = siblings.ToArray(); foreach (Cluster sibling in siblings) { sibling.instanceCount = instanceCount; sibling.instances = siblingClusters; } } } /// /// 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.version = prefab.version; this.name = prefab.name; if (parent != null) { TextAsset jsonFile = Resources.Load(parent.name); string json = jsonFile.text; ClusterData clusterData = JsonUtility.FromJson(json); //ClusterData clusterData = Cluster.Import(jsonPath); parent.cluster = new(clusterData); this.parent = parent.cluster; } ClonePrefab(); } public Cluster(ClusterJson json) { ClusterData clusterData = JsonUtility.FromJson(json.jsonFile.text); this.name = clusterData.name; // First create all neurons foreach (NeuronData neuronData in clusterData.neurons) { Neuron newNeuron = new(this, neuronData.name); } // Now create the synapses between them foreach (NeuronData neuronData in clusterData.neurons) { Neuron receiver = this.GetNeuron(neuronData.name); foreach (SynapseData synapseData in neuronData.synapses) { Neuron sender = this.GetNeuron(synapseData.neuronName); sender?.AddReceiver(receiver, synapseData.weight, synapseData.trainable); } } } public Cluster(ClusterData clusterData) { this.name = clusterData.name; // First create all neurons foreach (NeuronData neuronData in clusterData.neurons) { if (neuronData.type == Type.MemoryCell) new MemoryCell(this, neuronData.name) { bias = neuronData.bias, combinator = neuronData.combinatorType, activator = neuronData.activationType }; else new Neuron(this, neuronData.name) { bias = neuronData.bias, combinator = neuronData.combinatorType, activator = neuronData.activationType }; } // Now create the synapses between them foreach (NeuronData neuronData in clusterData.neurons) { Neuron receiver = this.GetNeuron(neuronData.name); foreach (SynapseData synapseData in neuronData.synapses) { // If the synapse points to another cluster if (synapseData.clusterName != this.name) { // Add reference to external cluster // Do we know the external cluster already? Cluster extCluster = this.GetCluster(synapseData.clusterName); if (extCluster == null) { // if not: create new external cluster Debug.Log("New ext cluster"); ExternalClusterData externalClusterData = clusterData.GetCluster(synapseData.clusterName); ClusterPrefab extPrefab = Resources.Load(externalClusterData.prefabName) as ClusterPrefab; extCluster = new(extPrefab, this, instanceCount) { name = externalClusterData.name }; } // Do we have the synapse already? Neuron extNeuron = extCluster.GetNeuron(synapseData.neuronName); Synapse synapse = receiver.GetSynapse(extNeuron); //extCluster, synapseData.neuronName); if (synapse == null) { Debug.Log("new receiver for external"); // If not: create new synapse extNeuron.AddReceiver(receiver); } } else { Neuron sender = this.GetNeuron(synapseData.neuronName); sender?.AddReceiver(receiver, synapseData.weight, synapseData.trainable); } } } } /// /// Clone a prefab. /// /// Strange that this does not take any parameters or return values. /// Where which the clone be found??? private void ClonePrefab() { //Debug.Log($"Clone Prefab {this.prefab.name} -> {this.name}"); if (this.prefab == null || this.prefab.cluster == null || this.prefab.cluster.nuclei == null) return; 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, prefabSynapse.trainable); //Debug.Log($"Add synapse {clonedCluster.name}.{clonedSender.name} -> {clonedNeuron.name} [{clonedSender.receivers.Count}]"); } else { int ix = GetNucleusIndex(this.prefab.cluster.nuclei, 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, prefabSynapse.trainable); // Debug.Log($"Add synapse {clonedSender.name} -> {clonedNeuron.name}"); } } } 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 Cluster sibling = new(clonedCluster.prefab, this) { name = $"{clonedCluster.baseName}: {instanceIx}", parent = this.parent, instanceCount = this.instanceCount, }; siblings.Add(sibling); CopyAllExternalReceivers(clonedCluster, sibling, this); } Cluster[] siblingClusters = siblings.ToArray(); foreach (Cluster sibling in siblings) sibling.instances = siblingClusters; } // Ensure that all neurons are computed to initialize bias foreach (Nucleus clonedNucleus in clonedNuclei) { if (clonedNucleus is not Cluster) clonedNucleus.UpdateStateIsolated(); } } /// \copydoc NanoBrain::Nucleus::ShallowCloneTo 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, }; return clone; } private static void CopyAllExternalReceivers(Cluster sourceCluster, Cluster sibling, 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()}"); } } } /// /// Get the index of a nucleus in a list of nuclei /// /// The list of nuclei to search /// The nucleus to find /// The index of the nucleus in the list or -1 when it has not been found 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; } /// /// Get the index of a nucleus with the given name in a list of nuclei /// /// The list of nuclei to search /// The name of the nucleus to find /// The index of the nucleus in the list or -1 when it has not been found 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; } /// /// Initializes all trainable weights to random values /// public void InitializeRandom() { System.Random randomGenerator = new(); foreach (Nucleus nucleus in this.nuclei) { if (nucleus is Neuron neuron) { foreach (Synapse synapse in neuron.synapses) { if (synapse.trainable) { synapse.weight = (float)randomGenerator.NextDouble() * 2.0f - 1.0f; // Debug.Log($"{neuron.name}-{synapse.neuron.name} weight = {synapse.weight}"); } } } } } public void GaussianAdditiveMutation(float sigma) { foreach (Nucleus nucleus in this.nuclei) { if (nucleus is Neuron neuron) { foreach (Synapse synapse in neuron.synapses) { if (synapse.trainable) { synapse.GaussianAdditiveMutation(sigma); //synapse.weight = (float)randomGenerator.NextDouble() * 2.0f - 1.0f; // Debug.Log($"{neuron.name}-{synapse.neuron.name} weight = {synapse.weight}"); } } } } } #endregion Init #region Cluster Array /// /// Increase the number of instances in an multi-cluster /// /// /remark Note this does not create the instances. /// This is only intended to be used for prefabs. public void AddInstance() { this.instanceCount++; } /// /// Create an new instance in a multi-cluster /// /// The prefab to use to create the new instance /// /remark This does not change the instanceCount. /// It should only be used at runtime. public void AddInstance(ClusterPrefab prefab) { // Ensure siblingClusters exists if (this.instances == null || this.instances.Length == 0) this.instances = new Cluster[1] { this }; // Prepare the new array int newLength = this.instances.Length + 1; Cluster[] newSiblings = new Cluster[newLength]; for (int i = 0; i < newSiblings.Length - 1; i++) newSiblings[i] = this.instances[i]; //Cluster newCluster = this.Clone(prefab) as Cluster; Cluster newCluster = new(prefab); 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.instances = newSiblings; } /// /// Decrease the number of instance in a multi-cluster /// public void RemoveInstance() { if (instanceCount > 1) instanceCount--; else { // It is not clear to me why we update the siblingClusters when the // instanceCount <= 1.... if (this.instances == null || this.instances.Length <= 1) return; // Prepare the new array int newLength = this.instances.Length - 1; Cluster[] newClusters = new Cluster[newLength]; for (int i = 0; i < newLength; i++) newClusters[i] = this.instances[i]; Neuron.Delete(this.instances[^1]); this.instances = newClusters; } } /// /// Remove a mapping from a thing to a cluster such that it becomes available for new things /// /// The multi-cluster instance which not no longer be mapped 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 List GetAllInstances(Neuron nucleus) { List allInstances = new(); int ix = -1; //foreach (Nucleus myNucleus in this.nuclei) { for (; ix < this.nuclei.Count; ix++) { if (this.nuclei[ix] == nucleus) break; } if (ix == this.nuclei.Count) return allInstances; foreach (Cluster instance in this.instances) { List otherNuclei = instance.nuclei; if (otherNuclei[ix] is Neuron otherNeuron) allInstances.Add(otherNeuron); } return allInstances; } // public void Backpropagation(Func Observer, float target, float learningRate) { // foreach (Nucleus nucleus in this.instances[0].nuclei) { // if (nucleus is not Neuron neuron) // continue; // foreach (Synapse synapse in neuron.synapses) { // List allSynapseNeurons = GetAllInstances(synapse.neuron); // Vector3 dSSRdW = Vector3.zero; // for (int clusterIx = 0; clusterIx < this.instances.Length; clusterIx++) { // Cluster clusterInstance = this.instances[clusterIx]; // Neuron neuronInstance = allSynapseNeurons[clusterIx]; // // Simple case, without receivers... // dSSRdW += (Vector3)(-2 * (Observer(clusterInstance) - target) * neuronInstance.activation); // } // synapse.weight += learningRate * dSSRdW.magnitude; // } // } // } #endregion ClusterArray /// /// This gives the order in which nuclei should be computed when a nucleus is updated /// [NonSerialized] private Dictionary> _computeOrders; /// /// This gives the order in which nuclei should be computed when a nucleus is updated /// public Dictionary> computeOrders { get { if (_computeOrders == null || _computeOrders.Count == 0) { _computeOrders = new(); foreach (Nucleus nucleus in this.nuclei) _computeOrders[nucleus] = TopologicalSort2(nucleus); } return _computeOrders; } } /// /// Refresh the order in which neurons should be computed /// 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; } /// /// The first nucleus in a cluster is the default output /// public virtual Neuron defaultOutput {//=> this.nuclei[0] as Nucleus; get { if (this.nuclei != null && this.nuclei.Count > 0) return this.nuclei[0] as Neuron; return null; } } /// /// The neurons without outgoing connections /// /// These neurons can potentially be connected to neurons in other clusters [NonSerialized] protected List _outputs = null; /// /// The neurons without outgoing connections /// /// These neurons can potentially be connected to neurons in other clusters public List outputs { get { if (this._outputs == null || this._outputs.Count == 0) { this._outputs = new(); if (this.nuclei == null) return this._outputs; foreach (Nucleus nucleus in this.nuclei) { if (nucleus is Neuron neuron && neuron.receivers.Count == 0) this._outputs.Add(neuron); } } return this._outputs; } } /// /// Reset the list of outputs such that they will be re-determined /// public void RefreshOutputs() { this._outputs = null; } /// /// Try to find a nucleus in this cluster /// /// The name of the nucleus to find /// The found nucleus or null if it is not found /// True when the nucleus is found, false otherwise public bool TryGetNucleus(string nucleusName, out Nucleus foundNucleus) { foreach (Nucleus receptor in this.nuclei) { if (receptor is Nucleus nucleus) if (nucleus.name == nucleusName) { // if (nucleus is Cluster cluster) // cluster.CheckInstances(); foundNucleus = nucleus; return true; } } foundNucleus = null; return false; } /// /// Get a neuron in this cluster /// /// The name of the neuron to find /// The found neuron or null when it is not found public Neuron GetNeuron(string neuronName) { if (this.nuclei == null) return null; foreach (Nucleus nucleus in this.nuclei) { if (nucleus is Neuron neuron && neuron.name == neuronName) return neuron; } return null; } /// /// Get a subcluster in this cluster /// /// The name of the cluster to find /// The found cluster or null when it is not found public Cluster GetCluster(string clusterName) { int dotPosition = clusterName.IndexOf('.'); if (dotPosition >= 0) { string clusterBaseName = clusterName[..dotPosition]; string clusterName0 = clusterBaseName + ": 0"; foreach (Nucleus nucleus in this.nuclei) { if (nucleus is Cluster cluster) { if (cluster.name == clusterBaseName || cluster.name == clusterName0) { // cluster.CheckInstances(); string subNucleusName = clusterName[(dotPosition + 1)..]; return cluster.GetCluster(subNucleusName); } } } return null; } else { string nucleusName0 = clusterName + ": 0"; foreach (Nucleus nucleus in this.nuclei) { if (nucleus is Cluster cluster) { if (nucleus.name == clusterName || nucleus.name == nucleusName0) { // cluster.CheckInstances(); return cluster; } } } return null; } } /// /// Get a neuron in an instance of a multi-cluster /// /// The id of the thing mapped to the cluster instance /// The name of the neuron to find /// The name of the thing mapped to the cluster instance /// The found neuron or null when it is not found /// The cluster instance mapped to the thing will be neuron.parent if a neuron is found. public Neuron GetNeuron(int thingId, string neuronName, string thingName = null) { if (this.instances == null || this.instances.Length <= 1) return this.GetNeuron(neuronName); // See if we are already using a cluster for thingId thingClusters ??= new(); 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.instances) { 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; } /// /// Delete a nucleus from this clsuter /// /// The nucleus to delete /// True if a nucleus was deleted, false if the nucleus could not be found 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 /// /// Collect all receiving nuclei of signals from this cluster /// /// Ensure that a receiver is only listed once in the result /// The list of 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; } /// /// Collect all synapses of senders in another cluster of signals to this cluster /// /// The other cluster with sending neurons /// A list of synapses to the neurons in the other clusters 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; } #endregion Receivers #region Update /// /// Update the state of the nucleus and all nuclei receiving from it /// /// The nucleus to start updating 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 Cluster) continue; 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); } } } } } /// \copydoc NanoBrain::Nucleus::UpdateStateIsolated public override void UpdateStateIsolated() { throw new Exception("Cluster should not be updated!"); } #endregion Update /// /// Recalculate derived properties /// /// This can be used to recalculate derived properties after the set of nuclei has been changed public void Refresh() { Cleanup(); // This should not be needed, but somehow somewhere the parent is changed... foreach (Nucleus nucleus in this.nuclei) { nucleus.parent = this; } RefreshOutputs(); RefreshComputeOrders(); } public void Cleanup() { foreach (Nucleus nucleus in this.nuclei.ToArray()) { if (nucleus is not Cluster cluster) continue; List receivers = cluster.CollectReceivers(); //Debug.Log($"cluster receiver count = {receivers.Count}"); if (receivers.Count == 0) this.nuclei.Remove(nucleus); } } #region Serialization public void Export(string path) { ClusterData data = new(this); //this.ToJSON(); string json = JsonUtility.ToJson(data, prettyPrint: true); // Debug.Log($"Exporting json to {path}"); File.WriteAllText(path, json); } public static ClusterData Import(string path) { string json = File.ReadAllText(path); // Debug.Log($"Importing json from {path}"); return JsonUtility.FromJson(json); } #endregion } [Serializable] public class ClusterData { public string name; public List neurons = new(); public List clusters = new(); public ClusterData(Cluster cluster) { this.name = cluster.name; foreach (Nucleus nucleus in cluster.nuclei) { if (nucleus is Neuron neuron) { NeuronData neuronData = new(neuron); this.neurons.Add(neuronData); foreach (Synapse synapse in neuron.synapses) { if (synapse.neuron.parent.name != cluster.name) { ExternalClusterData clusterData = new(synapse.neuron.parent); if (GetCluster(clusterData.name) == null) { //if (this.clusters.Find(data => data.name == clusterData.name) == null) { this.clusters.Add(clusterData); //Debug.Log("Add cluster"); } } } } } } public ExternalClusterData GetCluster(string clusterName) { foreach (ExternalClusterData cluster in this.clusters) { if (cluster.name == clusterName) return cluster; } return null; } } [Serializable] public class ExternalClusterData { public string name; public string prefabName; public uint instanceCount; public ExternalClusterData(Cluster cluster) { this.name = cluster.name; this.prefabName = cluster.prefab.name; this.instanceCount = (uint)cluster.instanceCount; } } }