Compare commits

..

No commits in common. "d2b5d2feacbd81c0b1526d8159681e7f68e222da" and "aab6f59934f901affad3162879dd20fc23cbcd5f" have entirely different histories.

52 changed files with 3188 additions and 3693 deletions

View File

@ -1,12 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Unity.Mathematics;
using static Unity.Mathematics.math;
[Serializable]
public class Cluster : Nucleus {
public class Cluster : INucleus {
// The ScriptableObject asset from which the runtime object has been created
[SerializeField]
protected string _name;
public virtual string name {
get => _name;
set => _name = value;
}
#region Init
@ -15,137 +22,98 @@ public class Cluster : Nucleus {
this.name = prefab.name;
this.parent = parent;
this.parent?.clusterNuclei.Add(this);
this.parent?.nuclei.Add(this);
ClonePrefab();
_ = this.inputs;
this.sortedNuclei = TopologicalSort(this.clusterNuclei);
this.sortedNuclei = TopologicalSort(this.nuclei);
}
public Cluster(ClusterPrefab prefab, ClusterPrefab parent = null) {
this.prefab = prefab;
this.name = prefab.name;
this.clusterPrefab = parent;
this.cluster = parent;
if (this.clusterPrefab != null)
this.clusterPrefab.nuclei.Add(this);
if (this.cluster != null)
this.cluster.nuclei.Add(this);
ClonePrefab();
_ = this.inputs;
this.sortedNuclei = TopologicalSort(this.clusterNuclei);
this.sortedNuclei = TopologicalSort(this.nuclei);
}
// public Cluster(ClusterPrefab parent, ClusterPrefab realPrefab) {
// this.prefab = realPrefab;
// this.name = realPrefab.name;
// this.cluster = parent;
// if (this.cluster != null)
// this.cluster.nuclei.Add(this);
// ClonePrefab();
// }
private void ClonePrefab() {
Nucleus[] prefabNuclei = this.prefab.nuclei.ToArray();
IReceptor[] nucleiArray = this.prefab.nuclei.ToArray();
// first clone the nuclei without their connections
foreach (Nucleus nucleus in this.prefab.nuclei) {
// Debug.Log($"prefab clone {nucleus.name}");
foreach (IReceptor nucleus in this.prefab.nuclei)
nucleus.ShallowCloneTo(this);
}
Nucleus[] clonedNuclei = this.clusterNuclei.ToArray();
IReceptor[] 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)
for (int nucleusIx = 0; nucleusIx < nucleiArray.Length; nucleusIx++) {
IReceptor receptor = nucleiArray[nucleusIx];
IReceptor clonedSender = clonedNuclei[nucleusIx];
if (clonedSender == null)
continue;
// Copy the receivers, which will also create the synapses
// Clusters do not have receivers...
foreach (Nucleus receiver in prefabNeuron.receivers.ToArray()) {
int ix = GetNucleusIndex(prefabNuclei, receiver);
foreach (INucleus receiver in receptor.receivers) {
int ix = GetNucleusIndex(nucleiArray, receiver);
if (ix < 0)
continue;
if (clonedNuclei[ix] is not Nucleus clonedReceiver)
if (clonedNuclei[ix] is not INucleus 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.nucleus == prefabNucleus) {
if (synapse.nucleus == receptor) {
weight = synapse.weight;
break;
}
}
clonedNeuron.AddReceiver(clonedReceiver, weight);
}
}
// Copy nucleus arrays
for (int nucleusIx = 0; nucleusIx < prefabNuclei.Length; nucleusIx++) {
Nucleus prefabReceptor = prefabNuclei[nucleusIx];
if (prefabReceptor is not Receptor prefabNucleus)
continue;
if (prefabNucleus.nucleiArray == null || prefabNucleus.nucleiArray.Length == 0)
continue;
Receptor clonedNucleus = clonedNuclei[nucleusIx] as Receptor;
if (prefabNucleus == prefabNucleus.nucleiArray[0]) {
// We clone the array only for the first entry
NucleusArray clonedArray = new(prefabNucleus.nucleiArray.Length, "array");
int arrayIx = 0;
foreach (Nucleus prefabArrayNucleus in prefabNucleus.nucleiArray) {
int arrayNucleusIx = GetNucleusIndex(prefabNuclei, prefabArrayNucleus);
if (arrayNucleusIx >= 0) {
Nucleus clonedArrayNucleus = clonedNuclei[arrayNucleusIx];
clonedArray.nuclei[arrayIx] = clonedArrayNucleus;
}
else {
Debug.LogError($" Could not find prefab nuclues {prefabNucleus.name} in the clones");
}
arrayIx++;
}
clonedNucleus.array = clonedArray;
}
else {
// The others will refer to the array created for the first nucleus in the array
int firstNucleusIx = GetNucleusIndex(prefabNuclei, prefabNucleus.nucleiArray[0]);
Receptor clonedFirstNucleus = clonedNuclei[firstNucleusIx] as Receptor;
clonedNucleus.nucleiArray = clonedFirstNucleus.nucleiArray;
clonedSender.AddReceiver(clonedReceiver, weight);
}
}
}
// Sort the nuclei in a correct evaluation order
private List<Nucleus> TopologicalSort(List<Nucleus> nodes) {
Dictionary<Nucleus, int> inDegree = new();
foreach (Nucleus node in nodes)
private List<IReceptor> TopologicalSort(List<IReceptor> nodes) {
Dictionary<IReceptor, int> inDegree = new();
foreach (IReceptor node in nodes)
inDegree[node] = 0; // Initialize in-degree to zero
// Calculate in-degrees
foreach (Nucleus node in nodes) {
if (node is Neuron neuron) {
foreach (Nucleus receiver in neuron.receivers)
inDegree[receiver]++;
}
foreach (IReceptor node in nodes) {
foreach (INucleus receiver in node.receivers)
inDegree[receiver]++;
}
Queue<Nucleus> queue = new();
foreach (Nucleus node in nodes) {
Queue<IReceptor> queue = new();
foreach (IReceptor node in nodes) {
if (inDegree[node] == 0) // Nodes with no dependencies
queue.Enqueue(node);
}
// The queue basically stores all input nuclei?
List<Nucleus> sortedOrder = new();
List<IReceptor> sortedOrder = new();
while (queue.Count > 0) {
Nucleus current = queue.Dequeue();
IReceptor 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);
}
foreach (INucleus receiver in current.receivers) {
inDegree[receiver]--;
if (inDegree[receiver] == 0) // If all dependencies resolved
queue.Enqueue(receiver);
}
}
@ -156,92 +124,31 @@ public class Cluster : Nucleus {
return sortedOrder;
}
public override Nucleus Clone(ClusterPrefab parent) {
Cluster clone = new(this.prefab, parent);
public virtual IReceptor Clone() {
Neuron clone = new(this.cluster, this.name) {
array = this.array,
};
foreach (Synapse synapse in this.synapses) {
Synapse clonedSynapse = clone.AddSynapse(synapse.nucleus);
clonedSynapse.weight = synapse.weight;
}
foreach (Neuron output in this.outputs) {
foreach (Nucleus receiver in output.receivers) {
int ix = GetNucleusIndex(this.clusterNuclei.ToArray(), output);
if (ix < 0)
continue;
if (clone.clusterNuclei[ix] is not Neuron clonedOutput)
continue;
clonedOutput.AddReceiver(receiver);
}
foreach (INucleus receiver in this.receivers) {
clone.AddReceiver(receiver);
}
return clone;
}
public override Nucleus ShallowCloneTo(Cluster parent) {
public IReceptor ShallowCloneTo(Cluster parent) {
Cluster clone = new(this.prefab, parent) {
name = this.name,
clusterPrefab = this.clusterPrefab,
};
// This cloned the prefab with the clusternuclei,
// but did not clone the receivers outside the cluster
RestoreExternalReceivers(clone, this.clusterPrefab, parent);
return clone;
}
protected void RestoreExternalReceivers(Cluster clone, ClusterPrefab prefabParent, Cluster clonedParent) {
for (int nucleusIx = 0; nucleusIx < this.clusterNuclei.Count; nucleusIx++) {
Nucleus prefabNucleus = this.clusterNuclei[nucleusIx];
if (prefabNucleus is not Neuron prefabNeuron)
continue;
Nucleus clonedNucleus = clone.clusterNuclei[nucleusIx];
if (clonedNucleus == null || clonedNucleus is not Neuron clonedNeuron)
continue;
// Copy the receivers, which will also create the synapses
foreach (Nucleus receiver in prefabNeuron.receivers) {
int ix = GetNucleusIndex(prefabParent.nuclei, receiver);
if (ix < 0)
continue;
//if (clone.clusterNuclei[ix] is not Nucleus clonedReceiver)
if (clonedParent.clusterNuclei[ix] 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.nucleus == prefabNucleus) {
weight = synapse.weight;
break;
}
}
clonedNeuron.AddReceiver(clonedReceiver, weight);
}
}
}
protected int GetNucleusIndex(Nucleus[] nuclei, Nucleus nucleus) {
for (int i = 0; i < nuclei.Length; i++) {
if (nucleus == nuclei[i])
return i;
}
return -1;
}
protected int GetNucleusIndex(List<Nucleus> nuclei, Nucleus nucleus) {
int i = 0;
foreach (Nucleus nucleiElement in nuclei) {
//for (int i = 0; i < nuclei.Length; i++) {
if (nucleus == nucleiElement)
private int GetNucleusIndex(IReceptor[] nucleiArray, IReceptor nucleus) {
for (int i = 0; i < nucleiArray.Length; i++) {
if (nucleus == nucleiArray[i])
return i;
}
return -1;
@ -251,243 +158,172 @@ public class Cluster : Nucleus {
public ClusterPrefab prefab;
public ClusterPrefab cluster { get; set; }
public Cluster parent { get; set; }
[SerializeReference]
public List<Nucleus> clusterNuclei = new();
public List<IReceptor> nuclei = new();
// the nuclei sorted using topological sorting
// to ensure that the cluster is computer in the right order
public List<Nucleus> sortedNuclei;
//public Dictionary<string, Nucleus> nucleiDict = new();
public List<IReceptor> sortedNuclei;
public List<Nucleus> _inputs = null;
public virtual List<Nucleus> inputs {
public List<INucleus> _inputs = null;
public virtual List<INucleus> inputs {
get {
if (this._inputs == null) {
this._inputs = new();
foreach (Nucleus nucleus in this.clusterNuclei) {
// inputs have no synapses
if (nucleus.synapses.Count == 0)
this._inputs.Add(nucleus);
foreach (IReceptor receptor in this.nuclei) {
if (receptor is INucleus nucleus) {
// inputs have no incoming synapses yet.
if (nucleus.synapses.Count == 0)
this._inputs.Add(nucleus);
}
}
ComputeOrders();
}
return this._inputs;
}
}
public Dictionary<Nucleus, List<Nucleus>> computeOrders = new();
private void ComputeOrders() {
foreach (Nucleus input in this._inputs) {
computeOrders[input] = TopologicalSort2(input);
}
}
private List<Nucleus> TopologicalSort2(Nucleus startNode) {
Dictionary<Nucleus, int> inDegree = new Dictionary<Nucleus, int>();
HashSet<Nucleus> visited = new HashSet<Nucleus>();
// Initialize in-degrees and mark all nodes as unvisited
foreach (Nucleus node in this.clusterNuclei) {
inDegree[node] = 0;
}
// Calculate in-degrees for all nodes reachable from the start node
Queue<Nucleus> queue = new Queue<Nucleus>();
queue.Enqueue(startNode);
visited.Add(startNode);
while (queue.Count > 0) {
Nucleus current = queue.Dequeue();
if (current is Neuron neuron) {
foreach (Nucleus receiver in neuron.receivers) {
if (!visited.Contains(receiver)) {
visited.Add(receiver);
queue.Enqueue(receiver);
}
inDegree[receiver]++;
}
}
}
// Perform topological sort on all reachable nodes
queue.Clear();
foreach (var node in visited) {
if (inDegree[node] == 0) {
queue.Enqueue(node);
}
}
List<Nucleus> sortedOrder = new List<Nucleus>();
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) {
if (visited.Contains(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;
}
private List<Nucleus> TopologicalSort3(Nucleus startNode) {
Dictionary<Nucleus, int> inDegree = new();
foreach (Nucleus node in this.clusterNuclei)
inDegree[node] = 0; // Initialize in-degree to zero
// Calculate in-degrees
foreach (Nucleus node in this.clusterNuclei) {
if (node is Neuron neuron) {
foreach (Nucleus receiver in neuron.receivers)
inDegree[receiver]++;
}
}
Queue<Nucleus> queue = new();
queue.Enqueue(startNode);
List<Nucleus> 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);
}
}
}
Debug.Log($"Compute order for {startNode.name} length = {sortedOrder.Count}");
// Check for cycles in the graph
// if (sortedOrder.Count != this.nuclei.Count)
// throw new InvalidOperationException("Graph is not a DAG; a cycle exists.");
return sortedOrder;
}
public virtual Neuron defaultOutput {//=> this.nuclei[0] as Nucleus;
public virtual INucleus output {//=> this.nuclei[0] as INucleus;
get {
if (this.clusterNuclei.Count > 0)
return this.clusterNuclei[0] as Neuron;
if (this.nuclei.Count > 0)
return this.nuclei[0] as INucleus;
return null;
}
}
protected List<Neuron> _outputs = null;
public List<Neuron> outputs {
get {
if (this._outputs == null) {
this._outputs = new();
foreach (Nucleus nucleus in this.clusterNuclei) {
if (nucleus is Neuron neuron) // && neuron.receivers.Count == 0)
this._outputs.Add(neuron);
}
}
return this._outputs;
}
// Not sure if this belongs here...
[SerializeReference]
private NucleusArray _array;
public NucleusArray array {
get { return _array; }
set { _array = value; }
}
public bool TryGetNucleus(string nucleusName, out Nucleus foundNucleus) {
foreach (Nucleus receptor in this.clusterNuclei) {
if (receptor is Nucleus nucleus)
if (nucleus.name == nucleusName) {
foundNucleus = nucleus;
return true;
}
}
foundNucleus = null;
return false;
#region Synapses
[SerializeField]
private List<Synapse> _synapses = new();
public List<Synapse> synapses => _synapses;
public Synapse AddSynapse(IReceptor sendingNucleus, float weight = 1.0f) {
Synapse synapse = new(sendingNucleus, weight);
this._synapses.Add(synapse);
return synapse;
}
public Nucleus GetNucleus(string nucleusName) {
foreach (Nucleus nucleus in this.clusterNuclei) {
if (nucleus.name == nucleusName)
return nucleus;
}
return null;
// Does this even exist already?
public void RemoveSynapse() {
}
public IReceptor GetReceptor(string receptorName) {
string receptorName0 = receptorName + ": 0";
foreach (Nucleus nucleus in this.clusterNuclei) {
if (nucleus is IReceptor receptor) {
if (nucleus.name == receptorName | nucleus.name == receptorName0)
//if (receptor.GetName() == receptorName)
return receptor;
}
}
return null;
}
#endregion Synapses
#region Receivers
public virtual List<Nucleus> CollectReceivers() {
List<Nucleus> receivers = new();
foreach (Neuron output in this.outputs) {
receivers.AddRange(output.receivers);
}
return receivers;
[SerializeReference]
private List<INucleus> _receivers = new();
public List<INucleus> receivers {
get { return _receivers; }
set { _receivers = value; }
}
public virtual void AddReceiver(INucleus receivingNucleus, float weight = 1) {
this._receivers.Add(receivingNucleus);
receivingNucleus.AddSynapse(this, weight);
}
public void RemoveReceiver(INucleus receiverNucleus) {
this._receivers.RemoveAll(receiver => receiver == receiverNucleus);
receiverNucleus.synapses.RemoveAll(synapse => synapse.nucleus == this);
}
#endregion Receivers
#region Update
#region Runtime
public void UpdateFromNucleus(Nucleus startNucleus) {
// no bias+synapse input state calculation for now...
[NonSerialized]
private int stale = 1000;
public bool isSleeping => lengthsq(this.outputValue) == 0;
List<Nucleus> computeOrder = this.computeOrders[startNucleus];
if (startNucleus.trace)
Debug.Log($"Update from {startNucleus.name}");
foreach (Nucleus nucleus in computeOrder) {
nucleus.UpdateStateIsolated();
if (startNucleus.trace)
Debug.Log($" {nucleus.name} = {nucleus.outputValue}");
[NonSerialized]
protected float3 _outputValue;
public virtual float3 outputValue {
get { return _outputValue; }
set {
this.stale = 0;
_outputValue = value;
}
this.outputValue = this.defaultOutput.outputValue;
this.stale = 0;
UpdateNuclei();
}
public override void UpdateStateIsolated() {
float3 sum = this.bias;
#region Update
public virtual void UpdateState() {
UpdateState(new float3(0, 0, 0));
}
public void UpdateState(float3 bias) {
float3 sum = bias; // new(0, 0, 0);
//Applying the weight factors
foreach (Synapse synapse in this.synapses) {
if (lengthsq(synapse.nucleus.outputValue) > 0) {
sum += synapse.weight * synapse.nucleus.outputValue;
this.stale = 0;
}
sum += synapse.weight * synapse.nucleus.outputValue;
}
foreach (Nucleus nucleus in this.sortedNuclei)
nucleus.UpdateStateIsolated();
//this.inputs[0].UpdateState(sum);
this.inputs[0].UpdateStateIsolated(sum);
foreach (IReceptor receptor in this.sortedNuclei) {
if (receptor is INucleus nucleus && nucleus != this.inputs[0])
nucleus.UpdateStateIsolated();
}
this.outputValue = this.defaultOutput.outputValue;
this.stale = 0;
UpdateNuclei();
UpdateResult(this.output.outputValue);
}
public override void UpdateNuclei() {
foreach (Nucleus nucleus in this.clusterNuclei)
public void UpdateStateIsolated() {
float3 bias = new(0,0,0);
UpdateStateIsolated(bias);
}
public void UpdateStateIsolated(float3 bias) {
float3 sum = bias; // new(0, 0, 0);
//Applying the weight factors
foreach (Synapse synapse in this.synapses) {
sum += synapse.weight * synapse.nucleus.outputValue;
}
//this.inputs[0].UpdateState(sum);
this.inputs[0].UpdateStateIsolated(sum);
foreach (IReceptor receptor in this.sortedNuclei) {
if (receptor is INucleus nucleus && nucleus != this.inputs[0])
nucleus.UpdateStateIsolated();
}
this.outputValue = this.output.outputValue;
}
public virtual void UpdateResult(Vector3 result) {
// float d = Vector3.Distance(result, this.outputValue);
// if (d < 0.5f) {
// //Debug.Log($"insignificant update: {d}");
// return;
// }
this.outputValue = result;
foreach (INucleus receiver in this.receivers)
receiver.UpdateState();
}
public void UpdateNuclei() {
this.stale++;
if (this.stale > 2)
_outputValue = Vector3.zero;
//foreach (IReceptor nucleus in this.prefab.nuclei)
foreach (IReceptor nucleus in this.nuclei)
nucleus.UpdateNuclei();
}
#endregion Update
#endregion Runtime
}

View File

@ -3,21 +3,22 @@ using UnityEngine;
[CreateAssetMenu(menuName = "Passer/Cluster")]
public class ClusterPrefab : ScriptableObject {
// The ScriptableObject asset from which the runtime object has been created
[SerializeReference]
public List<Nucleus> nuclei = new();
public List<IReceptor> nuclei = new();
public virtual Nucleus output => this.nuclei[0] as Nucleus;
public virtual INucleus output => this.nuclei[0] as INucleus;
public List<Nucleus> _inputs = null;
public virtual List<Nucleus> inputs {
public List<INucleus> _inputs = null;
public virtual List<INucleus> inputs {
get {
if (this._inputs == null) {
this._inputs = new();
foreach (Nucleus receptor in this.nuclei) {
if (receptor is Nucleus nucleus) {
foreach (IReceptor receptor in this.nuclei) {
if (receptor is INucleus nucleus) {
// inputs have no incoming synapses yet.
if (nucleus.synapses.Count == 0)
this._inputs.Add(nucleus);
@ -27,91 +28,53 @@ public class ClusterPrefab : ScriptableObject {
return this._inputs;
}
}
private List<Nucleus> _outputs = null;
public List<Nucleus> outputs {
get {
if (this._outputs == null)
RefreshOutputs();
return this._outputs;
}
}
public void RefreshOutputs() {
this._outputs = new();
foreach (Nucleus nucleus in this.nuclei) {
if (nucleus is Neuron neuron && neuron.receivers.Count == 0)
this._outputs.Add(nucleus);
}
}
public Nucleus GetNucleus(string nucleusName) {
foreach (Nucleus nucleus in this.nuclei) {
if (nucleus.name == nucleusName)
return nucleus;
}
return null;
}
// Call this function to ensure that there is at least one nucleus
// This is an invariant and should be ensured before the nucleus is used
// because output requires it.
public void EnsureInitialization() {
nuclei ??= new List<Nucleus>();
nuclei ??= new List<IReceptor>();
if (nuclei.Count == 0)
new Neuron(this, "Output"); // Every cluster should have at least 1 neuron
}
public void GarbageCollection() {
HashSet<Nucleus> visitedNuclei = new();
foreach (Nucleus output in this.outputs)
MarkNuclei(visitedNuclei, output);
//MarkNuclei(visitedNuclei, this.output);
HashSet<INucleus> visitedNuclei = new();
MarkNuclei(visitedNuclei, this.output);
//Debug.Log($"Garbage collection found {visitedNuclei.Count} Nuclei");
this.nuclei.RemoveAll(nucleus => visitedNuclei.Contains(nucleus) == false);
this.nuclei.RemoveAll(nucleus => nucleus is INucleus n && visitedNuclei.Contains(n) == false);
}
public void MarkNuclei(HashSet<Nucleus> visitedNuclei, Nucleus nucleus) {
public void MarkNuclei(HashSet<INucleus> visitedNuclei, INucleus nucleus) {
if (nucleus is null)
return;
if (nucleus.parent != null && nucleus.parent.prefab != this)
visitedNuclei.Add(nucleus.parent);
else
visitedNuclei.Add(nucleus);
visitedNuclei.Add(nucleus);
if (nucleus.synapses != null) {
HashSet<Synapse> visitedSynapses = new();
foreach (Synapse synapse in nucleus.synapses) {
if (synapse != null && synapse.nucleus != null) {
visitedSynapses.Add(synapse);
if (synapse.nucleus is Nucleus synapse_nucleus)
if (synapse.nucleus is INucleus synapse_nucleus)
MarkNuclei(visitedNuclei, synapse_nucleus);
}
}
nucleus.synapses.RemoveAll(synapse => visitedSynapses.Contains(synapse) == false);
}
if (nucleus is Neuron neuron && neuron.receivers != null) {
HashSet<Nucleus> visitedReceivers = new();
foreach (Nucleus receiver in neuron.receivers) {
if (nucleus.receivers != null) {
HashSet<INucleus> visitedReceivers = new();
foreach (INucleus receiver in nucleus.receivers) {
if (receiver != null && receiver != null) {
visitedReceivers.Add(receiver);
visitedNuclei.Add(receiver);
}
}
neuron.receivers.RemoveAll(receiver => visitedReceivers.Contains(receiver) == false);
nucleus.receivers.RemoveAll(receiver => visitedReceivers.Contains(receiver) == false);
}
}
public virtual void UpdateNuclei() {
foreach (Nucleus nucleus in this.nuclei)
foreach (IReceptor nucleus in this.nuclei)
nucleus.UpdateNuclei();
}
public int GetNucleusIndex(Nucleus receiver) {
int ix = 0;
foreach (Nucleus nucleus in this.nuclei) {
if (receiver == nucleus)
return ix;
ix++;
}
return -1;
}
}

View File

@ -1,110 +0,0 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using Unity.Mathematics;
using static Unity.Mathematics.math;
[Serializable]
public class ClusterReceptor : Cluster, IReceptor {
public ClusterReceptor(ClusterPrefab prefab, Cluster parent, string name) : base(prefab, parent) {
this.name = name;
this.array ??= new NucleusArray(this);
}
public ClusterReceptor(ClusterPrefab prefabToInstantiate, ClusterPrefab parent, string name) : base(prefabToInstantiate, parent) {
this.name = name;
this.array = new NucleusArray(this);
}
public string GetName() {
return this.name;
}
public override Nucleus ShallowCloneTo(Cluster parent) {
ClusterReceptor clone = new(this.prefab, parent, this.name) {
clusterPrefab = this.clusterPrefab,
array = this.array
};
// This cloned the prefab with the clusternuclei,
// but did not clone the receivers outside the cluster
RestoreExternalReceivers(clone, this.clusterPrefab, parent);
return clone;
}
public override Nucleus Clone(ClusterPrefab parent) {
ClusterReceptor clone = new(prefab, parent, this.name) {
array = this.array
};
foreach (Synapse synapse in this.synapses) {
Synapse clonedSynapse = clone.AddSynapse(synapse.nucleus);
clonedSynapse.weight = synapse.weight;
}
// foreach (Nucleus receiver in this.receivers) {
// clone.AddReceiver(receiver);
// }
this._outputs = null; // Make sure the output are regenerated
foreach (Neuron output in this.outputs) {
int ix = GetNucleusIndex(this.clusterNuclei, output);
if (ix < 0 || clone.clusterNuclei[ix] is not Neuron clonedOutput)
continue;
foreach (Nucleus receiver in output.receivers)
clonedOutput.AddReceiver(receiver);
}
return clone;
}
[SerializeReference]
private NucleusArray _array;
public NucleusArray array {
get { return _array; }
set { _array = value; }
}
//[SerializeReference]
//private Nucleus[] _nucleusArray;
public Nucleus[] nucleiArray {
get { return _array.nuclei; }
set { _array.nuclei = value; }
}
public void AddReceptorElement(ClusterPrefab prefab) {
this.nucleiArray = IReceptorHelpers.AddReceptorElement(this.nucleiArray, prefab);
}
public void RemoveReceptorElement() {
this.nucleiArray = IReceptorHelpers.RemoveReceptorElement(this.nucleiArray);
}
public override void UpdateStateIsolated() {
float3 sum = this.bias;
foreach (Nucleus nucleus in this.sortedNuclei)
nucleus.UpdateStateIsolated();
this.outputValue = this.defaultOutput.outputValue;
this.stale = 0;
UpdateNuclei();
}
public override void UpdateNuclei() {
this.stale++;
if (this.stale > staleValueForSleep && lengthsq(this.bias) > 0) {
this.bias = new float3(0, 0, 0);
this.parent.UpdateFromNucleus(this);
}
foreach (Nucleus nucleus in this.clusterNuclei)
nucleus.UpdateNuclei();
}
public virtual void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) {
this.array ??= new NucleusArray(this.parent);
this.array.ProcessStimulus(thingId, inputValue, thingName);
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 4f64f5d72a422a7c8bb9ace598432aad

View File

@ -1,365 +0,0 @@
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.Linq;
// Simple DAG data model
[System.Serializable]
public class DagNode {
public int id;
public string title;
public Vector2 position;
public float radius = 20f; // circle radius
}
[System.Serializable]
public class DagEdge {
public int fromId;
public int toId;
}
public class BrainEditorWindow : EditorWindow {
readonly List<DagNode> nodes = new();
readonly List<DagEdge> edges = new();
Vector2 pan = Vector2.zero;
float zoom = 1.0f;
const float minZoom = 0.5f;
const float maxZoom = 2.0f;
// Vector2 dragStart;
// bool draggingNode = false;
// int draggingNodeId = -1;
private readonly System.Type acceptedType = typeof(ClusterPrefab);
[MenuItem("Window/Brain Viewer")]
public static void ShowWindow() {
var w = GetWindow<BrainEditorWindow>("Brain Viewer");
w.minSize = new Vector2(500, 300);
}
void OnEnable() {
// if (nodes.Count == 0)
// CreateSampleGraph();
// Register callback so window updates when selection changes
Selection.selectionChanged += OnSelectionChanged;
RefreshSelection();
ComputeLeftToRightLayout();
}
private void OnDisable() {
Selection.selectionChanged -= OnSelectionChanged;
}
private void OnSelectionChanged() {
RefreshSelection();
ComputeLeftToRightLayout();
Repaint();
}
private void RefreshSelection() {
ClusterPrefab prefab = Selection.activeObject as ClusterPrefab;
if (prefab != null && acceptedType.IsAssignableFrom(prefab.GetType())) {
GenerateGraph(prefab);
}
}
private void GenerateGraph(ClusterPrefab prefab) {
nodes.Clear();
edges.Clear();
int ix = 0;
foreach (Nucleus nucleus in prefab.nuclei) {
nodes.Add(new DagNode() { id = ix, title = nucleus.name });
if (nucleus is Neuron neuron) {
foreach (Nucleus receiver in neuron.receivers) {
int receiverIx = prefab.GetNucleusIndex(receiver);
edges.Add(new DagEdge() { fromId = ix, toId = receiverIx });
}
}
ix++;
}
}
// void CreateSampleGraph() {
// nodes.Clear();
// edges.Clear();
// nodes.Add(new DagNode() { id = 0, title = "In1" });
// nodes.Add(new DagNode() { id = 1, title = "In2" });
// nodes.Add(new DagNode() { id = 2, title = "A" });
// nodes.Add(new DagNode() { id = 3, title = "B" });
// nodes.Add(new DagNode() { id = 4, title = "C" });
// nodes.Add(new DagNode() { id = 5, title = "Out1" });
// nodes.Add(new DagNode() { id = 6, title = "Out2" });
// edges.Add(new DagEdge() { fromId = 0, toId = 2 });
// edges.Add(new DagEdge() { fromId = 1, toId = 2 });
// edges.Add(new DagEdge() { fromId = 2, toId = 3 });
// edges.Add(new DagEdge() { fromId = 2, toId = 4 });
// edges.Add(new DagEdge() { fromId = 3, toId = 5 });
// edges.Add(new DagEdge() { fromId = 4, toId = 6 });
// }
void OnGUI() {
HandleInput();
Rect rect = new(0, 0, position.width, position.height);
EditorGUI.DrawRect(rect, new Color(0.11f, 0.11f, 0.11f));
// compute window center
Vector2 windowCenter = new(position.width / 2f, position.height / 2f);
// compute graph bounds center (in graph space)
Rect bounds = GetGraphBounds();
Vector2 graphCenter = bounds.center;
// compute autoPan that recenters the graph (does not modify node positions)
Vector2 autoPan = -graphCenter; // moves graph center to origin
// total translation = windowCenter + autoPan + user pan
Matrix4x4 oldMatrix = GUI.matrix;
GUI.matrix = Matrix4x4.TRS(windowCenter + autoPan + pan, Quaternion.identity, Vector3.one * zoom) *
Matrix4x4.TRS(-windowCenter, Quaternion.identity, Vector3.one);
// Draw edges first
foreach (DagEdge e in edges) {
DagNode from = GetNodeById(e.fromId);
DagNode to = GetNodeById(e.toId);
if (from == null || to == null) continue;
DrawEdgeCircleNodes(from, to);
}
// Draw nodes (circles)
foreach (DagNode n in nodes)
DrawNucleus(n);
GUI.matrix = oldMatrix;
// Footer toolbar
GUILayout.FlexibleSpace();
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
if (GUILayout.Button("Fit", EditorStyles.toolbarButton)) FitToView();
if (GUILayout.Button("Layout LR", EditorStyles.toolbarButton)) ComputeLeftToRightLayout();
EditorGUILayout.EndHorizontal();
}
void HandleInput() {
Event e = Event.current;
// Zoom with scroll
if (e.type == EventType.ScrollWheel) {
float oldZoom = zoom;
float delta = -e.delta.y * 0.01f;
zoom = Mathf.Clamp(zoom + delta, minZoom, maxZoom);
Vector2 mouse = e.mousePosition;
pan += (mouse - new Vector2(position.width / 2, position.height / 2)) * (1 - zoom / oldZoom);
e.Use();
}
// Pan with middle or right+ctrl drag
if (e.type == EventType.MouseDrag && (e.button == 2 || (e.button == 1 && e.control))) {
pan += e.delta;
e.Use();
}
}
DagNode GetNodeById(int id) => nodes.FirstOrDefault(x => x.id == id);
List<DagEdge> GetIncomingEdges(DagNode node) {
List<DagEdge> incoming = new();
foreach (DagEdge e in edges) {
if (e.toId == node.id)
incoming.Add(e);
}
return incoming;
}
List<DagEdge> GetOutgoingEdges(DagNode node) {
List<DagEdge> outgoing = new();
foreach (DagEdge e in edges) {
if (e.fromId == node.id)
outgoing.Add(e);
}
return outgoing;
}
void DrawNucleus(DagNode n) {
Vector3 position = n.position;
Handles.color = Color.white * 0.9f;
Handles.DrawSolidDisc(n.position, Vector3.forward, n.radius);
if (GetIncomingEdges(n).Count == 0)
DrawArrowHead(n.position - new Vector2(n.radius + 10, 0), n.position - new Vector2(n.radius + 5, 0), 10f / zoom, 12f / zoom, Color.white);
if (GetOutgoingEdges(n).Count == 0)
DrawArrowHead(n.position + new Vector2(n.radius + 10, 0), n.position + new Vector2(n.radius + 15, 0), 10f / zoom, 12f / zoom, Color.white);
Handles.color = Color.white;
GUIStyle style = new(EditorStyles.label) {
alignment = TextAnchor.UpperCenter,
normal = { textColor = Color.white },
fontStyle = FontStyle.Bold,
};
Vector3 labelPos = position - Vector3.down * (n.radius + 10f); // below disc along up axis
Handles.Label(labelPos, n.title, style);
}
void DrawEdgeCircleNodes(DagNode from, DagNode to) {
Vector2 a = from.position;
Vector2 b = to.position;
if (a == b) return;
Handles.color = Color.white * 0.9f;
Handles.DrawLine(from.position, to.position);
// Vector2 dir = (b - a).normalized;
// Vector2 start = a + dir * from.radius;
// Vector2 end = b - dir * to.radius;
//DrawArrowHead(end - dir * 2f, end, 10f / zoom, 12f / zoom, Color.white);
}
void DrawArrowHead(Vector2 from, Vector2 to, float headWidth, float headLength, Color color) {
Vector2 dir = (to - from).normalized;
if (dir == Vector2.zero) return;
Vector2 right = new Vector2(-dir.y, dir.x);
Vector3 p1 = to;
Vector3 p2 = to - dir * headLength + right * headWidth * 0.5f;
Vector3 p3 = to - dir * headLength - right * headWidth * 0.5f;
Handles.color = color;
Handles.DrawAAConvexPolygon(p1, p2, p3);
}
// Left-to-right layered layout (sources on the left, sinks on the right)
void ComputeLeftToRightLayout() {
// build adjacency and indegree
var adj = nodes.ToDictionary(n => n.id, n => new List<int>());
var indeg = nodes.ToDictionary(n => n.id, n => 0);
foreach (var e in edges) {
if (!adj.ContainsKey(e.fromId) || !adj.ContainsKey(e.toId)) continue;
adj[e.fromId].Add(e.toId);
indeg[e.toId]++;
}
// Kahn's algorithm to compute topological layers (horizontal layers)
Dictionary<int, int> layer = new();
Queue<int> q = new(indeg.Where(kv => kv.Value == 0).Select(kv => kv.Key));
foreach (var id in q) layer[id] = 0;
while (q.Count > 0) {
int u = q.Dequeue();
int l = layer[u];
foreach (var v in adj[u]) {
// prefer placing v at least one layer after u
if (!layer.ContainsKey(v) || layer[v] < l + 1) layer[v] = l + 1;
indeg[v]--;
if (indeg[v] == 0) q.Enqueue(v);
}
}
// Any unreachable nodes -> assign next layers
int maxLayer = layer.Count > 0 ? layer.Values.Max() : 0;
foreach (var n in nodes) {
if (!layer.ContainsKey(n.id)) {
maxLayer++;
layer[n.id] = maxLayer;
}
}
// Group nodes by layer (left to right)
var layers = layer.GroupBy(kv => kv.Value).OrderBy(g => g.Key).Select(g => g.Select(x => x.Key).ToList()).ToList();
// Layout parameters (horizontal spacing drives left->right)
float hSpacing = 150f;
float vSpacing = 100f;
// Place nodes: x increases with layer index, y spaced within layer
for (int li = 0; li < layers.Count; li++) {
var lst = layers[li];
float totalHeight = (lst.Count - 1) * vSpacing;
for (int i = 0; i < lst.Count; i++) {
int id = lst[i];
var n = GetNodeById(id);
if (n == null) continue;
float x = hSpacing + li * hSpacing;
float y = 400 - totalHeight / 2f + i * vSpacing;
// Debug.Log($"({li}, {i}) -> {x}, {y}");
n.position = new Vector2(x, y);
}
}
Repaint();
}
void FitToView() {
if (nodes.Count == 0) return;
// compute bounds including radii
Rect bounds = new Rect(nodes[0].position - Vector2.one * nodes[0].radius, Vector2.one * nodes[0].radius * 2f);
foreach (var n in nodes)
bounds = RectUnion(bounds, new Rect(n.position - Vector2.one * n.radius, Vector2.one * n.radius * 2f));
// center graph at origin (0,0) then set pan so it appears centered in window
Vector2 graphCenter = bounds.center;
// move nodes so center is at origin
for (int i = 0; i < nodes.Count; i++)
nodes[i].position -= graphCenter;
// reset pan/zoom so centered
pan = Vector2.zero;
zoom = 1.0f;
Repaint();
}
static Rect RectUnion(Rect a, Rect b) {
float xMin = Mathf.Min(a.xMin, b.xMin);
float xMax = Mathf.Max(a.xMax, b.xMax);
float yMin = Mathf.Min(a.yMin, b.yMin);
float yMax = Mathf.Max(a.yMax, b.yMax);
return Rect.MinMaxRect(xMin, yMin, xMax, yMax);
}
Vector2 ScreenToGraph_old(Vector2 screenPos) {
Vector2 origin = new Vector2(position.width / 2, position.height / 2);
// invert the GUI.matrix transform (approx for current simple transforms)
return (screenPos - (origin + pan)) / zoom + origin * (1 - 1 / zoom);
}
Vector2 ScreenToGraph(Vector2 screenPos) {
Vector2 windowCenter = new Vector2(position.width / 2f, position.height / 2f);
Rect bounds = GetGraphBounds();
Vector2 graphCenter = bounds.center;
Vector2 autoPan = -graphCenter;
// inverse of: screen -> translate by -(windowCenter+autoPan+pan), scale by 1/zoom, translate by windowCenter
return (screenPos - (windowCenter + autoPan + pan)) / zoom + windowCenter;
}
Rect GetGraphBounds() {
if (nodes == null || nodes.Count == 0) return new Rect(Vector2.zero, Vector2.one);
Rect bounds = new(
nodes[0].position - Vector2.one * nodes[0].radius,
2f * nodes[0].radius * Vector2.one);
foreach (var n in nodes)
bounds = RectUnion(bounds,
new Rect(n.position - Vector2.one * n.radius, 2f * n.radius * Vector2.one));
return bounds;
}
int HitTestNode(Vector2 graphPos) {
// returns node id under point or -1
for (int i = nodes.Count - 1; i >= 0; i--) {
var n = nodes[i];
if ((graphPos - n.position).sqrMagnitude <= n.radius * n.radius) return n.id;
}
return -1;
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: f041740900808273ab006e7d276a78e9

File diff suppressed because it is too large Load Diff

View File

@ -1,393 +0,0 @@
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.Linq;
// Simple DAG data model
// [System.Serializable]
// public class DagNode
// {
// public int id;
// public string title;
// public Vector2 position;
// public float radius = 36f; // circle radius
// }
// [System.Serializable]
// public class DagEdge
// {
// public int fromId;
// public int toId;
// }
public class DAGEditorWindow : EditorWindow
{
List<DagNode> nodes = new List<DagNode>();
List<DagEdge> edges = new List<DagEdge>();
Vector2 pan = Vector2.zero;
float zoom = 1.0f;
const float minZoom = 0.5f;
const float maxZoom = 2.0f;
GUIStyle labelStyle;
int selectedNodeId = -1;
Vector2 dragStart;
bool draggingNode = false;
int draggingNodeId = -1;
[MenuItem("Window/DAG Viewer (LR, Circles)")]
public static void ShowWindow()
{
var w = GetWindow<DAGEditorWindow>("DAG Viewer (LR)");
w.minSize = new Vector2(500, 300);
}
void OnEnable()
{
labelStyle = new GUIStyle(EditorStyles.label);
labelStyle.alignment = TextAnchor.MiddleCenter;
labelStyle.normal.textColor = Color.white;
labelStyle.fontStyle = FontStyle.Bold;
if (nodes.Count == 0)
CreateSampleGraph();
ComputeLeftToRightLayout();
}
void CreateSampleGraph()
{
nodes.Clear();
edges.Clear();
nodes.Add(new DagNode() { id = 0, title = "In1" });
nodes.Add(new DagNode() { id = 1, title = "In2" });
nodes.Add(new DagNode() { id = 2, title = "A" });
nodes.Add(new DagNode() { id = 3, title = "B" });
nodes.Add(new DagNode() { id = 4, title = "C" });
nodes.Add(new DagNode() { id = 5, title = "Out1" });
nodes.Add(new DagNode() { id = 6, title = "Out2" });
edges.Add(new DagEdge() { fromId = 0, toId = 2 });
edges.Add(new DagEdge() { fromId = 1, toId = 2 });
edges.Add(new DagEdge() { fromId = 2, toId = 3 });
edges.Add(new DagEdge() { fromId = 2, toId = 4 });
edges.Add(new DagEdge() { fromId = 3, toId = 5 });
edges.Add(new DagEdge() { fromId = 4, toId = 6 });
}
void OnGUI()
{
HandleInput();
Rect rect = new Rect(0, 0, position.width, position.height);
EditorGUI.DrawRect(rect, new Color(0.11f, 0.11f, 0.11f));
Matrix4x4 oldMatrix = GUI.matrix;
Vector2 origin = new Vector2(position.width / 2, position.height / 2);
GUI.matrix = Matrix4x4.TRS(origin + pan, Quaternion.identity, Vector3.one * zoom) *
Matrix4x4.TRS(-origin, Quaternion.identity, Vector3.one);
// Draw edges first
foreach (var e in edges)
{
var from = GetNodeById(e.fromId);
var to = GetNodeById(e.toId);
if (from == null || to == null) continue;
DrawEdgeCircleNodes(from, to);
}
// Draw nodes (circles)
foreach (var n in nodes)
{
DrawNodeCircle(n);
}
GUI.matrix = oldMatrix;
// Footer toolbar
GUILayout.FlexibleSpace();
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
if (GUILayout.Button("Fit", EditorStyles.toolbarButton)) FitToView();
if (GUILayout.Button("Layout LR", EditorStyles.toolbarButton)) ComputeLeftToRightLayout();
if (GUILayout.Button("Add Node", EditorStyles.toolbarButton))
{
AddNode("N" + nodes.Count);
ComputeLeftToRightLayout();
}
if (GUILayout.Button("Add Edge (selected->new)", EditorStyles.toolbarButton))
{
if (selectedNodeId != -1)
{
var newNode = AddNode("N" + nodes.Count);
edges.Add(new DagEdge() { fromId = selectedNodeId, toId = newNode.id });
ComputeLeftToRightLayout();
}
}
EditorGUILayout.EndHorizontal();
}
void HandleInput()
{
Event e = Event.current;
// Zoom with scroll
if (e.type == EventType.ScrollWheel)
{
float oldZoom = zoom;
float delta = -e.delta.y * 0.01f;
zoom = Mathf.Clamp(zoom + delta, minZoom, maxZoom);
Vector2 mouse = e.mousePosition;
pan += (mouse - new Vector2(position.width / 2, position.height / 2)) * (1 - zoom / oldZoom);
e.Use();
}
// Pan with middle or right+ctrl drag
if (e.type == EventType.MouseDrag && (e.button == 2 || (e.button == 1 && e.control)))
{
pan += e.delta;
e.Use();
}
// Node dragging & selection (convert mouse to graph space)
Vector2 graphMouse = ScreenToGraph(e.mousePosition);
if (e.type == EventType.MouseDown && e.button == 0)
{
int hit = HitTestNode(graphMouse);
if (hit != -1)
{
selectedNodeId = hit;
draggingNode = true;
draggingNodeId = hit;
dragStart = graphMouse;
e.Use();
}
else
{
selectedNodeId = -1;
}
}
if (draggingNode && draggingNodeId != -1)
{
if (e.type == EventType.MouseDrag && e.button == 0)
{
Vector2 graphDelta = e.delta / zoom;
var n = GetNodeById(draggingNodeId);
if (n != null)
{
n.position += graphDelta;
Repaint();
e.Use();
}
}
if (e.type == EventType.MouseUp && e.button == 0)
{
draggingNode = false;
draggingNodeId = -1;
e.Use();
}
}
}
DagNode AddNode(string title)
{
int nextId = nodes.Count > 0 ? nodes.Max(n => n.id) + 1 : 0;
var n = new DagNode() { id = nextId, title = title, position = Vector2.zero };
nodes.Add(n);
return n;
}
DagNode GetNodeById(int id) => nodes.FirstOrDefault(x => x.id == id);
void DrawNodeCircle(DagNode n)
{
Vector2 center = n.position;
float r = n.radius;
Rect nodeRect = new Rect(center.x - r, center.y - r, r * 2, r * 2);
// circle background
Color bg = (n.id == selectedNodeId) ? new Color(0.15f, 0.5f, 0.9f) : new Color(0.2f, 0.2f, 0.2f);
EditorGUI.DrawRect(nodeRect, bg);
// anti-aliased circle outline
Handles.color = Color.white * 0.9f;
Handles.DrawAAPolyLine(3f / zoom, GetCircleOutlinePoints(center, r, 48).ToArray());
// label
Vector2 labelPos = center - new Vector2(0, 8);
GUI.Label(new Rect(labelPos.x - r, labelPos.y - 8, r * 2, 18), n.title, labelStyle);
}
List<Vector3> GetCircleOutlinePoints(Vector2 center, float radius, int segments)
{
var pts = new List<Vector3>(segments + 1);
for (int i = 0; i <= segments; i++)
{
float a = (float)i / segments * Mathf.PI * 2f;
pts.Add(new Vector3(center.x + Mathf.Cos(a) * radius, center.y + Mathf.Sin(a) * radius, 0));
}
return pts;
}
void DrawEdgeCircleNodes(DagNode from, DagNode to)
{
Vector2 a = from.position;
Vector2 b = to.position;
if (a == b) return;
// Compute edge line that starts/ends at circle circumferences
Vector2 dir = (b - a).normalized;
Vector2 start = a + dir * from.radius;
Vector2 end = b - dir * to.radius;
// Use a simple curved line: start -> control -> end (bezier)
Vector2 control = new Vector2((start.x + end.x) / 2f, (start.y + end.y) / 2f);
// Slight vertical offset to separate overlapping lines based on node ids
float offset = ((from.id * 7 + to.id * 11) % 7 - 3) * 6f / zoom;
control += new Vector2(0, offset);
Handles.color = Color.white * 0.9f;
Handles.DrawAAPolyLine(3f / zoom, 20, GetBezierPoints(start, control, end, 24).ToArray());
// Arrow at end pointing towards 'b'
DrawArrowHead(end - dir * 2f, end, 10f / zoom, 12f / zoom, Color.white);
}
List<Vector3> GetBezierPoints(Vector2 p0, Vector2 p1, Vector2 p2, int seg)
{
var pts = new List<Vector3>(seg + 1);
for (int i = 0; i <= seg; i++)
{
float t = (float)i / seg;
Vector2 p = (1 - t) * (1 - t) * p0 + 2 * (1 - t) * t * p1 + t * t * p2;
pts.Add(new Vector3(p.x, p.y, 0));
}
return pts;
}
void DrawArrowHead(Vector2 from, Vector2 to, float headWidth, float headLength, Color color)
{
Vector2 dir = (to - from).normalized;
if (dir == Vector2.zero) return;
Vector2 right = new Vector2(-dir.y, dir.x);
Vector3 p1 = to;
Vector3 p2 = to - dir * headLength + right * headWidth * 0.5f;
Vector3 p3 = to - dir * headLength - right * headWidth * 0.5f;
Handles.color = color;
Handles.DrawAAConvexPolygon(p1, p2, p3);
}
// Left-to-right layered layout (sources on the left, sinks on the right)
void ComputeLeftToRightLayout()
{
// build adjacency and indegree
var adj = nodes.ToDictionary(n => n.id, n => new List<int>());
var indeg = nodes.ToDictionary(n => n.id, n => 0);
foreach (var e in edges)
{
if (!adj.ContainsKey(e.fromId) || !adj.ContainsKey(e.toId)) continue;
adj[e.fromId].Add(e.toId);
indeg[e.toId]++;
}
// Kahn's algorithm to compute topological layers (horizontal layers)
Dictionary<int, int> layer = new Dictionary<int, int>();
Queue<int> q = new Queue<int>(indeg.Where(kv => kv.Value == 0).Select(kv => kv.Key));
foreach (var id in q) layer[id] = 0;
while (q.Count > 0)
{
int u = q.Dequeue();
int l = layer[u];
foreach (var v in adj[u])
{
// prefer placing v at least one layer after u
if (!layer.ContainsKey(v) || layer[v] < l + 1) layer[v] = l + 1;
indeg[v]--;
if (indeg[v] == 0) q.Enqueue(v);
}
}
// Any unreachable nodes -> assign next layers
int maxLayer = layer.Count > 0 ? layer.Values.Max() : 0;
foreach (var n in nodes)
{
if (!layer.ContainsKey(n.id))
{
maxLayer++;
layer[n.id] = maxLayer;
}
}
// Group nodes by layer (left to right)
var layers = layer.GroupBy(kv => kv.Value).OrderBy(g => g.Key).Select(g => g.Select(x => x.Key).ToList()).ToList();
// Layout parameters (horizontal spacing drives left->right)
float hSpacing = 220f;
float vSpacing = 120f;
// Place nodes: x increases with layer index, y spaced within layer
for (int li = 0; li < layers.Count; li++)
{
var lst = layers[li];
float totalHeight = (lst.Count - 1) * vSpacing;
for (int i = 0; i < lst.Count; i++)
{
int id = lst[i];
var n = GetNodeById(id);
if (n == null) continue;
float x = li * hSpacing;
float y = -totalHeight / 2f + i * vSpacing;
n.position = new Vector2(x, y);
}
}
Repaint();
}
void FitToView()
{
if (nodes.Count == 0) return;
Rect bounds = new Rect(nodes[0].position - Vector2.one * nodes[0].radius, Vector2.one * nodes[0].radius * 2f);
foreach (var n in nodes)
bounds = RectUnion(bounds, new Rect(n.position - Vector2.one * n.radius, Vector2.one * n.radius * 2f));
Vector2 center = bounds.center;
pan = -center;
zoom = 1.0f;
Repaint();
}
static Rect RectUnion(Rect a, Rect b)
{
float xMin = Mathf.Min(a.xMin, b.xMin);
float xMax = Mathf.Max(a.xMax, b.xMax);
float yMin = Mathf.Min(a.yMin, b.yMin);
float yMax = Mathf.Max(a.yMax, b.yMax);
return Rect.MinMaxRect(xMin, yMin, xMax, yMax);
}
Vector2 ScreenToGraph(Vector2 screenPos)
{
Vector2 origin = new Vector2(position.width / 2, position.height / 2);
// invert the GUI.matrix transform (approx for current simple transforms)
return (screenPos - (origin + pan)) / zoom + origin * (1 - 1 / zoom);
}
int HitTestNode(Vector2 graphPos)
{
// returns node id under point or -1
for (int i = nodes.Count - 1; i >= 0; i--)
{
var n = nodes[i];
if ((graphPos - n.position).sqrMagnitude <= n.radius * n.radius) return n.id;
}
return -1;
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 95393aed582b8b30d965400672aec4d8

View File

@ -1,47 +0,0 @@
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
[CustomEditor(typeof(NanoBrain))]
public class NanoBrainComponent_Editor : Editor {
protected static VisualElement mainContainer;
protected static VisualElement inspectorContainer;
protected NanoBrain component;
private SerializedProperty brainProp;
ClusterInspector.GraphView board;
public void OnEnable() {
component = target as NanoBrain;
if (Application.isPlaying == false)
brainProp = serializedObject.FindProperty(nameof(NanoBrain.defaultBrain));
}
public override VisualElement CreateInspectorGUI() {
Cluster brain = component.brain;
if (Application.isPlaying == false)
serializedObject.Update();
VisualElement root = new();
if (Application.isPlaying == false) {
PropertyField brainField = new(brainProp) {
label = "Nano Brain"
};
root.Add(brainField);
}
if (brain != null)
ClusterInspector.CreateInspector(root, brain.prefab, brain.defaultOutput, component.gameObject);
if (Application.isPlaying == false)
serializedObject.ApplyModifiedProperties();
return root;
}
}

302
Editor/NeuroidWindow.cs Normal file
View File

@ -0,0 +1,302 @@
/*
using UnityEditor;
using UnityEngine;
using System.Linq;
using System.Collections.Generic;
public class NeuroidLayer {
public int ix = 0;
public List<Nucleus> neuroids = new();
}
public class GraphEditorWindow : EditorWindow {
private Nucleus currentNucleus;
private List<Neuroid> allNeuroids;
private Dictionary<Nucleus, Vector2Int> neuroidPositions = new();
private List<NeuroidLayer> layers = new();
private void OnEnable() {
EditorApplication.update += EditorUpdate;
Selection.selectionChanged += OnSelectionChange;
SelectNeuron();
}
private void AddToLayer(NeuroidLayer layer, Nucleus nucleus) {
layer.neuroids.Add(nucleus);
nucleus.layerIx = layer.ix;
// Store its position
Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1);
neuroidPositions[nucleus] = neuroidPosition;
}
private void BuildLayers() {
// A temporary list to track what's been added to layers
this.layers = new();
int layerIx = 0;
Nucleus selectedNucleus = this.currentNucleus;
if (selectedNucleus == null)
return;
NeuroidLayer currentLayer = new() { ix = layerIx };
//foreach (Nucleus outputNeuroid in selectedNucleus.receivers) {
foreach (Receiver receiver in selectedNucleus.receivers) {
Nucleus outputNeuroid = receiver.nucleus;
if (outputNeuroid != null) {
AddToLayer(currentLayer, outputNeuroid);
Debug.Log($"layer {layerIx} nucleus {outputNeuroid.name}");
}
}
if (currentLayer.neuroids.Count > 0) {
this.layers.Add(currentLayer);
layerIx++;
currentLayer = new() { ix = layerIx };
}
AddToLayer(currentLayer, selectedNucleus);
this.layers.Add(currentLayer);
Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}");
layerIx++;
currentLayer = new() { ix = layerIx };
int six = 0;
// foreach (Synapse synapse in selectedNucleus.synapses.Values) {
// Debug.Log($"Synapse {six}");
// Nucleus input = synapse.neuroid;
//foreach ((Nucleus input, Synapse synapse) in selectedNucleus.synapses) {
//foreach ((Nucleus input, float weight) in selectedNucleus.synapses) {
foreach (Synapse synapse in selectedNucleus.synapses) {
Nucleus input = synapse.nucleus;
if (input != null) {
AddToLayer(currentLayer, input);
Debug.Log($"layer {layerIx} nucleus {input.name}");
}
six++;
}
if (currentLayer.neuroids.Count > 0) {
this.layers.Add(currentLayer);
}
}
private void BuildLayers_old(List<Neuroid> neuroids) {
if (neuroids == null)
return;
// A temporary list to track what's been added to layers
this.layers = new();
HashSet<Neuroid> neuronVisited = new();
int layerIx = 0;
// While there are unvisited neuroid
while (neuroids.Any(neuroid => !neuronVisited.Contains(neuroid))) {
// Create the next layer
NeuroidLayer currentLayer = new() { ix = layerIx };
int neuroidIx = 0;
foreach (Neuroid neuroid in neuroids) {
// Skip neurons we already processed
if (neuronVisited.Contains(neuroid))
continue;
// if (neuroid.IsStale()) {
// Debug.Log($"neuron {neuroid.name} is stale {neuroid.stale}");
// neuronVisited.Add(neuroid);
// continue;
// }
// If the output neuroid is visited
// Note: this does not yet work for multiple outputs yet (see the use of First())
// if (neuroid.receivers.Count == 0 // make sure the root neuroids are processed directly
// || (neuronVisited.Contains(neuroid.receivers.First()) && neuroid.receivers.First().layerIx == layerIx - 1)) {
if (neuroid.receivers.Count == 0 // make sure the root neuroids are processed directly
|| (neuronVisited.Contains(neuroid.receivers.First().nucleus) && neuroid.receivers.First().nucleus.layerIx == layerIx - 1)) {
// Add it to the next layer
currentLayer.neuroids.Add(neuroid);
neuroid.layerIx = layerIx;
// Register it as visited
neuronVisited.Add(neuroid);
// Store its position
Vector2Int neuroidPosition = new(layerIx, neuroidIx);
neuroidPositions[neuroid] = neuroidPosition;
neuroidIx++;
Debug.Log($"Layer {layerIx} neuron {neuroidIx} name {neuroid.name}");
}
}
if (currentLayer.neuroids.Count > 0) {
this.layers.Add(currentLayer);
layerIx++;
}
}
}
private void OnDisable() {
EditorApplication.update -= EditorUpdate;
Selection.selectionChanged -= OnSelectionChange;
}
private void OnSelectionChange() {
SelectNeuron();
Repaint();
}
private void EditorUpdate() {
if (EditorApplication.isPlaying)
Repaint();
}
private void OnGUI() {
GUILayout.Label("Graph Visualizer", EditorStyles.boldLabel);
DrawGraph();
}
private void DrawGraph() {
if (currentNucleus == null)
return;
foreach (NeuroidLayer layer in layers)
DrawLayer(layer);
}
private void DrawLayer(NeuroidLayer layer) {
int column = layer.ix * 100;
int nodeCount = layer.neuroids.Count;
float maxValue = 0;
foreach (Nucleus nucleus in layer.neuroids) {
if (nucleus is Neuroid neuroid) {
float value = neuroid.outputValue.magnitude;
if (value > maxValue)
maxValue = value;
}
}
float spacing = 400f / nodeCount;
float margin = 100 + spacing / 2;
foreach (Nucleus layerNucleus in layer.neuroids) {
if (layerNucleus is Neuroid layerNeuroid) {
Vector2Int layerNeuroidPos = this.neuroidPositions[layerNeuroid];
Vector3 parentPos = new(100 + layerNeuroidPos.x * 100, margin + layerNeuroidPos.y * spacing, 0.1f);
int i = 0;
float inputSpacing = 400f / layerNeuroid.synapses.Count;
float inputMargin = 100 + inputSpacing / 2;
// foreach (Synapse synapse in layerNeuroid.synapses.Values) {
// if (synapse.neuroid != null) {
// if (this.neuroidPositions.ContainsKey(synapse.neuroid)) {
// Vector2Int inputNeuroidPos = this.neuroidPositions[synapse.neuroid];
//foreach ((Nucleus neuroid, Synapse synapse) in layerNeuroid.synapses) {
//foreach ((Nucleus neuroid, float weight) in layerNeuroid.synapses) {
foreach (Synapse synapse in layerNeuroid.synapses) {
Nucleus neuroid = synapse.nucleus;
float weight = synapse.weight;
if (neuroid != null) {
if (this.neuroidPositions.ContainsKey(neuroid)) {
Vector2Int inputNeuroidPos = this.neuroidPositions[neuroid];
if (inputNeuroidPos.x == layerNeuroidPos.x + 1) {
Vector3 pos = new(100 + inputNeuroidPos.x * 100, inputMargin + inputNeuroidPos.y * inputSpacing, 0.0f);
//float brightness = synapse.weight / 10.0f;
float brightness = weight / 10.0f;
Handles.color = new Color(brightness, brightness, brightness);
Handles.DrawLine(parentPos, pos);
}
}
}
}
float size = 20;
if (layerNeuroid.isSleeping)
Handles.color = Color.black;
else {
float brightness = layerNeuroid.outputValue.magnitude / maxValue;
Handles.color = new Color(brightness, brightness, brightness);
}
Handles.DrawSolidDisc(parentPos, Vector3.forward, size);
Vector3 labelPos = parentPos - Vector3.down * (size + 0.2f); // below disc along up axis
GUIStyle style = new GUIStyle(EditorStyles.label) {
alignment = TextAnchor.UpperCenter,
normal = { textColor = Color.white },
fontStyle = FontStyle.Bold
};
Handles.Label(labelPos, layerNeuroid.name, style);
Rect neuronRect = new(parentPos.x - size, parentPos.y - size, size * 2, size * 2);
Event e = Event.current;
if (e != null && neuronRect.Contains(e.mousePosition)) {
HandleMouseHover(layerNeuroid, neuronRect);
// Process click
if (e.type == EventType.MouseDown && e.button == 0) {
// Consume the event so the scene doesn't also handle it
e.Use();
HandleDiscClicked(layerNeuroid);
}
}
i++;
}
}
}
private void HandleMouseHover(Neuroid neuroid, Rect rect) {
GUIContent tooltip;
// if (neuroid is SensoryNeuroid sensoryNeuroid) {
// tooltip = new(
// $"{sensoryNeuroid.name}" +
// $"\nThing {sensoryNeuroid.receptor.thingType}" +
// $"\nValue: {neuroid.outputValue}");
// }
// else {
tooltip = new(
$"{neuroid.name}" +
$"\nsynapse count {neuroid.synapses.Count}" +
$"\nValue: {neuroid.outputValue}");
// }
Vector2 mousePosition = Event.current.mousePosition;
// Display tooltip with some offset
Vector2 tooltipSize = GUI.skin.box.CalcSize(tooltip);
Rect tooltipRect = new Rect(mousePosition.x + 10, mousePosition.y + 10, tooltipSize.x, tooltipSize.y);
GUI.Box(tooltipRect, tooltip);
}
private void HandleDiscClicked(Nucleus nucleus) {
this.currentNucleus = nucleus;
BuildLayers();
}
// Update node colors based on selected GameObjects
private void SelectNeuron() {
GameObject[] selectedObjects = Selection.gameObjects;
if (selectedObjects.Length == 0)
return;
GameObject selectedObject = selectedObjects[0];
Boid boid = selectedObject.GetComponent<Boid>();
if (boid == null)
return;
// Nucleus neuroid = boid.behaviour;
// this.currentNucleus = neuroid;
// if (neuroid == null)
// this.allNeuroids = new();
// else
// this.allNeuroids = neuroid.brain.neuroids;
// Debug.Log($"Neuroncount = {this.allNeuroids.Count}");
// BuildLayers();
// Debug.Log($"Layercount = {this.layers.Count}");
}
[MenuItem("Window/Neuroid Visualizer")]
public static void ShowWindow() {
GetWindow<GraphEditorWindow>("Neuroid Visualizer");
}
}
*/

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 26e68838038ea5243ae57bc81f4db8a8

55
INucleus.cs Normal file
View File

@ -0,0 +1,55 @@
using System.Collections.Generic;
using Unity.Mathematics;
public interface INucleus : IReceptor {
#region static struct
// Cluster
public ClusterPrefab cluster { get; }
// Senders
public List<Synapse> synapses { get; }
public Synapse AddSynapse(IReceptor sender, float weight = 1.0f);
public NucleusArray array { get; set; }
#endregion static struct
#region dynamic state
public void UpdateState();
public void UpdateState(float3 inputValue);
public void UpdateStateIsolated();
public void UpdateStateIsolated(float3 inputValue);
#endregion dynamic state
}
public interface IReceptor {
#region static
public string name { get; set; }
// Receivers
public List<INucleus> receivers { get; set; }
public void AddReceiver(INucleus receiver, float weight = 1);
public void RemoveReceiver(INucleus receiverNucleus);
#endregion static
#region dynamic
// float3 to prepare for SIMD
public float3 outputValue { get; }
public void UpdateNuclei();
public bool isSleeping { get; }
#endregion dynamic
public IReceptor ShallowCloneTo(Cluster parent);
public IReceptor Clone();
}

2
INucleus.cs.meta Normal file
View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6a8a0e8965cea660abff254cab8a4723

View File

@ -1,51 +0,0 @@
using UnityEngine;
public interface IReceptor {
public string GetName();
// public NucleusArray array {
// get; set;
// }
public Nucleus[] nucleiArray { get; set; }
public void AddReceptorElement(ClusterPrefab prefab);
public void RemoveReceptorElement();
public void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null);
}
public static class IReceptorHelpers {
public static Nucleus[] AddReceptorElement(Nucleus[] nucleiArray, ClusterPrefab prefab) {
if (nucleiArray.Length == 0) {
Debug.LogError("Empty perceptoid array, cannot add");
return null;
}
int newLength = nucleiArray.Length + 1;
Nucleus[] newArray = new Nucleus[newLength];
for (int i = 0; i < nucleiArray.Length; i++)
newArray[i] = nucleiArray[i];
if (nucleiArray[0] is Nucleus nucleus) {
newArray[newLength - 1] = nucleus.Clone(prefab);
newArray[newLength - 1].name += $": {newLength - 1}";
}
return newArray;
}
public static Nucleus[] RemoveReceptorElement(Nucleus[] nucleiArray) {
int newLength = nucleiArray.Length - 1;
if (newLength == 0) {
Debug.LogWarning("Perceptoid array cannot be empty");
return null;
}
Nucleus[] newPerceptei = new Nucleus[newLength];
for (int i = 0; i < newLength; i++)
newPerceptei[i] = nucleiArray[i];
// Delete the last perception
if (nucleiArray[newLength] is Nucleus nucleus)
Neuron.Delete(nucleus); //this._nuclei[newLength]);
return newPerceptei;
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 73f052292ad16bb53a3c07aa1694c705

View File

@ -4,57 +4,90 @@ using Unity.Mathematics;
using static Unity.Mathematics.math;
[Serializable]
public class MemoryCell : Neuron {
public class MemoryCell : Neuron, INucleus {
public MemoryCell(ClusterPrefab cluster, string name) : base(cluster, name) { }
public MemoryCell(Cluster parent, string name) : base(parent, name) { }
// this.parent = parent;
// this.name = name;
// this.parent?.nuclei.Add(this);
// }
public bool staticMemory = false;
public override bool isSleeping {
get {
if (staticMemory)
return false;
return base.isSleeping;
}
}
public override Nucleus ShallowCloneTo(Cluster newParent) {
MemoryCell clone = new(newParent, this.name);
CloneFields(clone);
clone.staticMemory = this.staticMemory;
public override IReceptor ShallowCloneTo(Cluster newParent) {
MemoryCell clone = new(newParent, this.name) {
array = this.array,
curve = this.curve,
curvePreset = this.curvePreset,
curveMax = this.curveMax,
average = this.average
};
return clone;
}
#region State
private bool initialized = false;
private float3 _memorizedValue;
private float _memorizedTime;
public override void UpdateState(float3 bias) {
// A memorycell does not have an activation function
float3 result = bias;
int n = 0;
//Applying the weight factgors
foreach (Synapse synapse in this.synapses) {
result += synapse.weight * synapse.nucleus.outputValue;
if (lengthsq(synapse.nucleus.outputValue) != 0)
n++;
}
if (this.average)
result /= n;
UpdateResult(result);
}
public override void UpdateStateIsolated() {
float3 bias = new(0, 0, 0);
UpdateStateIsolated(bias);
}
public override void UpdateStateIsolated(float3 bias) {
// A memorycell does not have an activation function
float3 result = Combinator();
float3 result = bias;
int n = 0;
if (initialized)
// Output the previous, memorized value
this.outputValue = this._memorizedValue;
else {
// The first time, the result is directly set in output
this.outputValue = result;
this.initialized = true;
//Applying the weight factgors
foreach (Synapse synapse in this.synapses) {
result += synapse.weight * synapse.nucleus.outputValue;
if (lengthsq(synapse.nucleus.outputValue) != 0)
n++;
}
if (this.average)
result /= n;
this.outputValue = this._memorizedValue;
// Store the result for the next time
this._memorizedValue = result;
this._memorizedTime = Time.time;
}
public override void UpdateNuclei() {
if (staticMemory)
// Static memory does not get stale or go to sleep
return;
public override void UpdateResult(Vector3 result) {
// output value is the previous value
// if (this.deltaValue) {
// float deltaTime = Time.time - this._memorizedTime;
// this._outputValue = this._memorizedValue * deltaTime;
// }
//else
this.outputValue = this._memorizedValue;
base.UpdateNuclei();
// Store the result for the next time
this._memorizedValue = result;
this._memorizedTime = Time.time;
foreach (INucleus receiver in this.receivers)
receiver.UpdateState();
}
#endregion State

407
Neuron.cs
View File

@ -7,31 +7,35 @@ using Unity.Mathematics;
using static Unity.Mathematics.math;
[Serializable]
public class Neuron : Nucleus {
public class Neuron : INucleus {
public Neuron(Cluster parent, string name) {
this.parent = parent;
this.name = name;
this.parent?.clusterNuclei.Add(this);
[SerializeField]
protected string _name;
public virtual string name {
get => _name;
set => _name = value;
}
public Neuron(ClusterPrefab prefab, string name) {
this.clusterPrefab = prefab;
this.name = name;
if (this.clusterPrefab != null)
this.clusterPrefab.nuclei.Add(this);
else
Debug.LogError("No prefab when adding neuron to prefab");
[SerializeField]
private List<Synapse> _synapses = new();
public List<Synapse> synapses => _synapses;
[SerializeReference]
private List<INucleus> _receivers = new();
public List<INucleus> receivers {
get { return _receivers; }
set { _receivers = value; }
}
[SerializeReference]
private NucleusArray _array;
public NucleusArray array {
get { return _array; }
set { _array = value; }
}
#region Serialization
public enum CombinatorType {
Sum,
Product,
Max
}
public CombinatorType combinator = CombinatorType.Sum;
public enum CurvePresets {
Linear,
Power,
@ -40,7 +44,7 @@ public class Neuron : Nucleus {
Custom
}
[SerializeField]
public CurvePresets _curvePreset;
private CurvePresets _curvePreset;
public CurvePresets curvePreset {
get { return _curvePreset; }
set {
@ -51,6 +55,12 @@ public class Neuron : Nucleus {
public AnimationCurve curve;
public float curveMax = 1.0f;
#region Parameters
public bool average = false;
#endregion Parameters
public AnimationCurve GenerateCurve() {
switch (this.curvePreset) {
case CurvePresets.Linear:
@ -71,6 +81,17 @@ public class Neuron : Nucleus {
}
}
public virtual void Deserialize(Neuron nucleus) { }
#endregion Serialization
#region Runtime state (not serialized)
public ClusterPrefab cluster { get; set; }
public Cluster parent { get; set; }
#region Activation
public static class Presets {
private const int samples = 32;
public static AnimationCurve Linear(float weight) {
@ -115,43 +136,127 @@ public class Neuron : Nucleus {
}
}
#endregion Serialization
#endregion Activation
protected float3 _outputValue;
public virtual float3 outputValue {
get { return _outputValue; }
set {
this.stale = 0;
// this._isSleeping = false;
_outputValue = value;
}
}
[NonSerialized]
private int stale = 1000;
// private bool _isSleeping = false;
// public bool isSleeping => _isSleeping;
public bool isSleeping => lengthsq(this.outputValue) == 0;
public void UpdateNuclei() {
this.stale++;
// this._isSleeping = this.stale > 2;
// if (isSleeping)
if (this.stale > 2)
_outputValue = Vector3.zero;
}
#endregion Runtime state
public Neuron(Cluster parent, string name) {
this.parent = parent;
this.name = name;
this.parent?.nuclei.Add(this);
}
public Neuron(ClusterPrefab parent, string name) {
this.cluster = parent;
this.name = name;
if (this.cluster != null) {
this.cluster.nuclei.Add(this);
}
// else
// Debug.LogError("No neuroid network");
}
// this clone the nucleus without the synapses and receivers
public override Nucleus ShallowCloneTo(Cluster newParent) {
Neuron clone = new(newParent, this.name);
CloneFields(clone);
public virtual IReceptor ShallowCloneTo(Cluster newParent) {
Neuron clone = new(newParent, this.name) {
array = this.array,
curve = this.curve,
curvePreset = this.curvePreset,
curveMax = this.curveMax,
average = this.average
};
return clone;
}
public virtual IReceptor ShallowCloneTo(ClusterPrefab newParent) {
Neuron clone = new(newParent, this.name) {
array = this.array,
curve = this.curve,
curvePreset = this.curvePreset,
curveMax = this.curveMax,
average = this.average
};
return clone;
}
public override Nucleus Clone(ClusterPrefab prefab) {
Neuron clone = new(prefab, this.name);
CloneFields(clone);
public virtual IReceptor CloneTo(ClusterPrefab parent) {
Neuron clone = new(parent, this.name) {
array = this.array,
curve = this.curve,
curvePreset = this.curvePreset,
curveMax = this.curveMax,
average = this.average
};
foreach (Synapse synapse in this.synapses) {
Synapse clonedSynapse = clone.AddSynapse(synapse.nucleus);
clonedSynapse.weight = synapse.weight;
}
foreach (Nucleus receiver in this.receivers) {
foreach (INucleus receiver in this.receivers) {
clone.AddReceiver(receiver);
}
return clone;
}
public virtual IReceptor Clone() {
Neuron clone = new(this.cluster, this.name) {
array = this.array,
curve = this.curve,
curvePreset = this.curvePreset,
curveMax = this.curveMax,
average = this.average
};
// if (clone.cluster != null)
// clone.cluster.nuclei.Add(clone);
foreach (Synapse synapse in this.synapses) {
Synapse clonedSynapse = clone.AddSynapse(synapse.nucleus);
clonedSynapse.weight = synapse.weight;
}
foreach (INucleus receiver in this.receivers) {
clone.AddReceiver(receiver);
}
return clone;
}
protected virtual void CloneFields(Neuron clone) {
clone.clusterPrefab = this.clusterPrefab;
clone.bias = this.bias;
clone.combinator = this.combinator;
clone.curve = this.curve;
clone.curvePreset = this.curvePreset;
clone.curveMax = this.curveMax;
public virtual void AddReceiver(INucleus receivingNucleus, float weight = 1) {
this._receivers.Add(receivingNucleus);
receivingNucleus.AddSynapse(this, weight);
}
public static void Delete(Nucleus nucleus) {
public void RemoveReceiver(INucleus receiverNucleus) {
this._receivers.RemoveAll(receiver => receiver == receiverNucleus);
receiverNucleus.synapses.RemoveAll(synapse => synapse.nucleus == this);
}
public static void Delete(INucleus nucleus) {
foreach (Synapse synapse in nucleus.synapses) {
if (synapse.nucleus is Neuron synapse_nucleus) {
if (synapse_nucleus.receivers.Count > 1) {
if (synapse_nucleus._receivers.Count > 1) {
// there is another nucleus feeding into this input nucleus
synapse_nucleus.receivers.RemoveAll(r => r == nucleus);
synapse_nucleus._receivers.RemoveAll(r => r == nucleus);
}
else {
// No other links, delete it.
@ -159,162 +264,124 @@ public class Neuron : Nucleus {
}
}
}
if (nucleus is Neuron neuron) {
foreach (Nucleus receiver in neuron.receivers) {
if (receiver != null && receiver.synapses != null)
receiver.synapses.RemoveAll(s => s.nucleus == nucleus);
}
} else if (nucleus is Cluster cluster) {
// remove all receivers for this cluster
foreach (Neuron output in cluster.outputs) {
foreach (Nucleus receiver in output.receivers) {
receiver.synapses.RemoveAll(s => s.nucleus == output);
}
}
foreach (INucleus receiver in nucleus.receivers) {
if (receiver != null && receiver.synapses != null)
receiver.synapses.RemoveAll(s => s.nucleus == nucleus);
}
if (nucleus.clusterPrefab != null) {
nucleus.clusterPrefab.nuclei.RemoveAll(n => n == nucleus);
nucleus.clusterPrefab.RefreshOutputs();
nucleus.clusterPrefab.GarbageCollection();
if (nucleus.cluster != null) {
nucleus.cluster.nuclei.RemoveAll(n => n == nucleus);
nucleus.cluster.GarbageCollection();
}
}
public override void UpdateStateIsolated() {
float3 result = Combinator();
this.outputValue = Activator(result);
public Synapse AddSynapse(IReceptor sendingNucleus, float weight = 1.0f) {
Synapse synapse = new(sendingNucleus, weight);
this.synapses.Add(synapse);
return synapse;
}
#region Combinator
protected Func<float3> Combinator => combinator switch {
CombinatorType.Sum => CombinatorSum,
CombinatorType.Product => CombinatorProduct,
CombinatorType.Max => CombinatorMax,
_ => CombinatorSum
};
public float3 CombinatorSum() {
float3 sum = this.bias;
foreach (Synapse synapse in this.synapses)
sum += synapse.weight * synapse.nucleus.outputValue;
return sum;
public virtual void UpdateState() {
//UpdateState(new float3(0, 0, 0));
this.parent?.UpdateState();
}
public float3 CombinatorProduct() {
float3 product = this.bias;
foreach (Synapse synapse in this.synapses)
product *= synapse.weight * synapse.nucleus.outputValue;
return product;
}
public virtual void UpdateState(float3 inputValue) {
float3 sum = inputValue;
int n = 0;
public float3 CombinatorMax() {
float3 max = this.bias;
float maxSqrLength = lengthsq(max);
//Applying the weight factors
//Applying the weight factgors
foreach (Synapse synapse in this.synapses) {
float3 input = synapse.weight * synapse.nucleus.outputValue;
sum += synapse.weight * synapse.nucleus.outputValue;
float inputSqrlength = lengthsq(input);
if (inputSqrlength > maxSqrLength) {
max = input;
maxSqrLength = inputSqrlength;
}
// Perhaps synapses should be removed when the output value goes to 0....
if (lengthsq(synapse.nucleus.outputValue) != 0)
n++;
}
return max;
if (this.average && n > 0)
sum /= n;
// Activation function
Vector3 result;
switch (this.curvePreset) {
case CurvePresets.Linear:
result = sum;
break;
case CurvePresets.Sqrt:
result = normalize(sum) * System.MathF.Sqrt(length(sum));
break;
case CurvePresets.Power:
result = normalize(sum) * System.MathF.Pow(length(sum), 2);
break;
case CurvePresets.Reciprocal:
result = normalize(sum) * (1 / length(sum));
break;
default:
float activatedValue = this.curve.Evaluate(length(sum));
result = normalize(sum) * activatedValue;
break;
}
UpdateResult(result);
}
#endregion Combinator
#region Activator
protected Func<float3, float3> Activator => this.curvePreset switch {
CurvePresets.Linear => ActivatorLinear,
CurvePresets.Sqrt => ActivatorSqrt,
CurvePresets.Power => ActivatorPower,
CurvePresets.Reciprocal => ActivatorReciprocal,
_ => ActivatorCustom
};
protected float3 ActivatorLinear(float3 input) {
return input;
public virtual void UpdateStateIsolated() {
UpdateStateIsolated(new float3(0, 0, 0));
}
public virtual void UpdateStateIsolated(float3 bias) {
float3 sum = bias;
int n = 0;
protected float3 ActivatorSqrt(float3 input) {
float3 result = normalize(input) * System.MathF.Sqrt(length(input));
return result;
}
//Applying the weight factgors
foreach (Synapse synapse in this.synapses) {
sum += synapse.weight * synapse.nucleus.outputValue;
protected float3 ActivatorPower(float3 input) {
float3 result = normalize(input) * System.MathF.Pow(length(input), 2);
return result;
}
// Perhaps synapses should be removed when the output value goes to 0....
if (lengthsq(synapse.nucleus.outputValue) != 0)
n++;
}
if (this.average && n > 0)
sum /= n;
protected float3 ActivatorReciprocal(float3 input) {
float magnitude = length(input);
if (magnitude == 0)
return new float3(0, 0, 0);
float3 result = normalize(input) * (1 / magnitude);
return result;
}
protected float3 ActivatorCustom(float3 input) {
float activatedValue = this.curve.Evaluate(length(input));
float3 result = normalize(input) * activatedValue;
return result;
}
#endregion Activator
#region Receivers
[SerializeReference]
private List<Nucleus> _receivers = new();
public virtual List<Nucleus> receivers {
get { return _receivers; }
set { _receivers = value; }
}
public virtual void AddReceiver(Nucleus receiverToAdd, float weight = 1) {
// if (this is IReceptor receptor) {
// foreach (Nucleus element in receptor.array.nuclei) {
// if (element is Neuron neuron) {
// neuron._receivers.Add(receiverToAdd);
// receiverToAdd.AddSynapse(element, weight);
// }
// }
// }
// else {
this._receivers.Add(receiverToAdd);
receiverToAdd.AddSynapse(this, weight);
// }
}
public virtual void RemoveReceiver(Nucleus receiverToRemove) {
if (this is IReceptor receptor) {
foreach (Nucleus element in receptor.nucleiArray) {
if (element is Neuron neuron) {
neuron._receivers.RemoveAll(receiver => receiver == receiverToRemove);
receiverToRemove.synapses.RemoveAll(synapse => synapse.nucleus == neuron);
// Activation function
float3 result = Vector3.zero;
switch (this.curvePreset) {
case CurvePresets.Linear:
result = sum;
break;
case CurvePresets.Sqrt:
result = normalize(sum) * System.MathF.Sqrt(length(sum));
break;
case CurvePresets.Power:
result = normalize(sum) * System.MathF.Pow(length(sum), 2);
break;
case CurvePresets.Reciprocal: {
float magnitude = length(sum);
if (magnitude > 0)
result = normalize(sum) * (1 / magnitude);
break;
}
}
}
else {
this._receivers.RemoveAll(receiver => receiver == receiverToRemove);
receiverToRemove.synapses.RemoveAll(synapse => synapse.nucleus == this);
default:
float activatedValue = this.curve.Evaluate(length(sum));
result = normalize(sum) * activatedValue;
break;
}
this.outputValue = result;
}
public virtual void UpdateResult(Vector3 result) {
// float d = Vector3.Distance(result, this.outputValue);
// if (d < 0.5f) {
// //Debug.Log($"insignificant update: {d}");
// return;
// }
#endregion Receivers
this.outputValue = result;
if (lengthsq(outputValue) != 0) {
Debug.Log($"{this.parent.name}.{this.name}: {this.outputValue}");
}
foreach (INucleus receiver in this.receivers)
receiver.UpdateState();
public virtual void ProcessStimulus(Vector3 inputValue, string thingName = null) {
this.stale = 0;
this.bias = inputValue;
this.parent.UpdateFromNucleus(this);
}
}

View File

@ -1,93 +0,0 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using Unity.Mathematics;
using static Unity.Mathematics.math;
[Serializable]
public abstract class Nucleus {
public string name;
[SerializeReference]
public ClusterPrefab clusterPrefab;
[SerializeReference]
public Cluster parent;
protected float3 _outputValue;
public virtual float3 outputValue {
get { return _outputValue; }
set {
_outputValue = value;
if (this.isFiring)
WhenFiring?.Invoke();
}
}
public bool isFiring => length(_outputValue) > 0.5f;
public Action WhenFiring;
public virtual bool isSleeping => lengthsq(this.outputValue) == 0;
[NonSerialized]
public int stale = 1000;
public readonly int staleValueForSleep = 20;
public bool trace = false;
public abstract Nucleus ShallowCloneTo(Cluster parent);
public abstract Nucleus Clone(ClusterPrefab prefab);
public enum Type {
None,
Neuron,
MemoryCell,
Selector,
Cluster,
Pulsar,
Receptor,
ReceptorArray,
ClusterReceptor,
}
#region Synapses
public Vector3 bias = Vector3.zero;
[SerializeField]
private List<Synapse> _synapses = new();
public List<Synapse> synapses => _synapses;
public Synapse AddSynapse(Nucleus sendingNucleus, float weight = 1.0f) {
Synapse synapse = new(sendingNucleus, weight);
this.synapses.Add(synapse);
return synapse;
}
public Synapse GetSynapse(Nucleus sender) {
foreach (Synapse synapse in this.synapses)
if (synapse.nucleus == sender)
return synapse;
return null;
}
public void RemoveSynapse(Nucleus sendingNucleus) {
this.synapses.RemoveAll(synapse => synapse.nucleus == sendingNucleus);
}
#endregion Synapses
#region Update
public abstract void UpdateStateIsolated();
public virtual void UpdateNuclei() {
}
public virtual void SetBias(Vector3 inputValue) {
this.stale = 0;
this.bias = inputValue;
this.parent.UpdateFromNucleus(this);
}
#endregion Update
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 4310eea6ab77628b085387a226c1c386

View File

@ -1,48 +1,49 @@
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using Unity.Mathematics;
using static Unity.Mathematics.math;
[System.Serializable]
public class NucleusArray {
[SerializeReference]
private Nucleus[] _nuclei;
public Nucleus[] nuclei {
private INucleus[] _nuclei;
private ClusterPrefab[] _clusters;
public IEnumerable<INucleus> nuclei {
get {
return _nuclei;
}
set {
_nuclei = value;
// if (_nuclei == null)
// return _clusters;
// else if (_clusters == null)
return _nuclei;
// else
// return _nuclei.Concat(_clusters);
}
}
public string name;
public NucleusArray(Nucleus nucleus) {
this._nuclei = new Nucleus[1];
public NucleusArray(INucleus nucleus) {
this.name = nucleus.name;
this._nuclei = new INucleus[1];
this._nuclei[0] = nucleus;
this._clusters = new ClusterPrefab[0];
}
public NucleusArray(ClusterPrefab cluster) {
this._nuclei = new Nucleus[0];
}
public NucleusArray(int size, string name) {
this._nuclei = new Nucleus[size];
this.name = cluster.name;
this._nuclei = new INucleus[0];
this._clusters = new ClusterPrefab[1];
this._clusters[0] = cluster;
}
public void AddNucleus(ClusterPrefab prefab) {
public void AddNucleus() {
if (this._nuclei.Length == 0) {
Debug.LogError("Empty perceptoid array, cannot add");
return;
}
int newLength = this._nuclei.Length + 1;
Nucleus[] newArray = new Nucleus[newLength];
INucleus[] newArray = new INucleus[newLength];
for (int i = 0; i < this._nuclei.Length; i++)
newArray[i] = this._nuclei[i];
if (this._nuclei[0] is Nucleus nucleus) {
newArray[newLength - 1] = nucleus.Clone(prefab);
newArray[newLength - 1].name += $": {newLength - 1}";
}
if (this._nuclei[0] is INucleus nucleus)
newArray[newLength - 1] = (INucleus) nucleus.Clone();
this._nuclei = newArray;
}
@ -53,100 +54,14 @@ public class NucleusArray {
Debug.LogWarning("Perceptoid array cannot be empty");
return;
}
Nucleus[] newPerceptei = new Nucleus[newLength];
INucleus[] newPerceptei = new INucleus[newLength];
for (int i = 0; i < newLength; i++)
newPerceptei[i] = this._nuclei[i];
// Delete the last perception
if (this._nuclei[newLength] is Nucleus nucleus)
Neuron.Delete(nucleus); //this._nuclei[newLength]);
Neuron.Delete(this._nuclei[newLength]);
this._nuclei = newPerceptei;
}
public Dictionary<int, Nucleus> thingReceivers = new();
private Nucleus FindReceiver(int thingId, float3 inputValue) {
// No existing nucleus for this thing
float inputMagnitude = length(inputValue);
Nucleus selectedReceiver = null;
float selectedMagnitude = 0;
foreach (Nucleus receiver in this._nuclei) {
if (thingReceivers.ContainsValue(receiver) == false) {
// We found an unusued receiver
thingReceivers.Add(thingId, receiver);
return receiver;
}
else if (receiver.isSleeping) {
// A sleeping receiver is not active and can therefore always be used
thingReceivers.Add(thingId, receiver);
return receiver;
}
else if (selectedReceiver == null) {
// If we haven't found a receiver yet, just start by taking the first
selectedReceiver = receiver;
selectedMagnitude = length(selectedReceiver.outputValue);
}
// Look for the receiver with the lowest magnitude
else {
float magnitude = length(receiver.outputValue);
if (magnitude < inputMagnitude && length(receiver.outputValue) < selectedMagnitude) {
selectedReceiver = receiver;
selectedMagnitude = length(selectedReceiver.outputValue);
}
}
}
if (selectedReceiver != null) {
// Replace the receiver
// Find the thingId current associated with the receiver
int keyToRemove = thingReceivers.FirstOrDefault(r => r.Value.Equals(selectedReceiver)).Key;
if (keyToRemove != 0 || thingReceivers.ContainsKey(keyToRemove))
thingReceivers.Remove(keyToRemove);
// And add the new association
thingReceivers.Add(thingId, selectedReceiver);
}
return selectedReceiver;
}
public virtual void ProcessStimulus(int thingId, Vector3 inputValue, string thingName = null) {
CleanupReceivers();
if (!thingReceivers.TryGetValue(thingId, out Nucleus selectedReceiver)) {
// No existing nucleus for this thing
selectedReceiver = FindReceiver(thingId, inputValue);
}
if (selectedReceiver == null)
return;
if (thingName != null) {
string baseName = selectedReceiver.name;
int colonPos = selectedReceiver.name.IndexOf(":");
if (colonPos > 0)
baseName = selectedReceiver.name[..colonPos];
selectedReceiver.name = baseName + ": " + thingName;
}
if (selectedReceiver is Neuron selectedNucleus)
selectedNucleus.ProcessStimulus(inputValue);
//selectedReceiver.parent.UpdateFromNucleus(selectedReceiver);
}
private void CleanupReceivers() {
// Remove a thing-receiver connection when the nucleus is inactive
List<int> receiversToRemove = new();
foreach (KeyValuePair<int, Nucleus> item in thingReceivers) {
if (item.Value != null && item.Value.isSleeping)
receiversToRemove.Add(item.Key);
}
foreach (int thingId in receiversToRemove) {
Nucleus selectedReceiver = thingReceivers[thingId];
thingReceivers.Remove(thingId);
int colonPos = selectedReceiver.name.IndexOf(":");
if (colonPos > 0)
selectedReceiver.name = selectedReceiver.name[..colonPos];
}
}
}

View File

@ -1,54 +0,0 @@
using System;
using Unity.Mathematics;
/// <summary>
/// The Pulsar represents a type of neuron that operates based on
/// the product of its weighted inputs rather than the traditional summation.
/// Drawing inspiration from the concept of pulsars in astrophysics
/// —highly magnetized rotating neutron stars that emit beams of radiation—
/// the Pulsar could symbolize dynamic, focused output based on the interaction of multiple factors.
/// </summary>
/// Multiplicative Functionality:
/// Instead of summing inputs, the Pulsar takes the weighted product of its inputs.
/// This means that all inputs must be active (non-zero) for the neuron to "pulse" or activate.
/// Output Behavior:
/// The output could amplify or diminish depending on the magnitude of the inputs.
/// The product would be sensitive to small values,
/// which means that even a small input could significantly lower the overall output if multiplied.
/// Activation Mechanism:
/// The activation function can further refine the output from the product result.
/// For instance, a certain threshold could be used to determine if a pulse occurs.
/// Modeling Complex Interactions:
/// The Pulsar could be particularly beneficial for modeling situations where interactions multiply rather than add.
/// This is useful in fields such as economics (e.g., compound growth models),
/// biology (e.g., interaction of hormones), and machine learning where multiplicative relationships exist.
[Serializable]
public class Pulsar : Neuron {
public Pulsar(Cluster parent, string name) : base(parent, name) {
// To prevent mistakes, bias one (instead of zero for standard neurons)
this.bias = new float3(1, 1, 1);
}
public Pulsar(ClusterPrefab parent, string name) : base(parent, name) {
// To prevent mistakes, bias one (instead of zero for standard neurons)
this.bias = new float3(1, 1, 1);
}
public override Nucleus ShallowCloneTo(Cluster newParent) {
Pulsar clone = new(newParent, this.name);
CloneFields(clone);
return clone;
}
public override void UpdateStateIsolated() {
float3 product = this.bias;
//Applying the weight factors
foreach (Synapse synapse in this.synapses) {
float3 input = synapse.weight * synapse.nucleus.outputValue;
product *= input;
}
// Activation function
this.outputValue = Activator(product);
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 46bd155173053a01585411c3e07f85d4

View File

@ -1,83 +1,191 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using Unity.Mathematics;
using static Unity.Mathematics.math;
[System.Serializable]
public class Receptor : Neuron, IReceptor {
public Receptor(Cluster parent, string name) : base(parent, name) {
this.array = new NucleusArray(this);
if (this.name.IndexOf(":") < 0)
this.name += ": 0";
}
public Receptor(ClusterPrefab prefab, string name) : base(prefab, name) {
this.array = new NucleusArray(this);
public class Receptor : IReceptor {
private ClusterPrefab cluster;
private Cluster parent;
[SerializeField]
protected string _name;
public virtual string name {
get => _name;
set => _name = value;
}
public string GetName() {
return this.name;
public Receptor(Cluster parent) {
this.parent = parent;
if (parent != null)
parent.nuclei.Add(this);
}
public Receptor(ClusterPrefab cluster) {
this.cluster = cluster;
if (cluster != null)
cluster.nuclei.Add(this);
}
public override Nucleus ShallowCloneTo(Cluster parent) {
Receptor clone = new(parent, name) {
public Receptor(ClusterPrefab cluster, INucleus nucleus) {
this.cluster = cluster;
if (cluster != null)
cluster.nuclei.Add(this);
this.AddReceiver(nucleus);
}
};
CloneFields(clone);
public static Receptor CreateReceptor(Cluster cluster, string nucleusName) {
if (cluster == null)
return null;
Receptor receptor = new(cluster);
foreach (INucleus nucleus in cluster.inputs) {
if (nucleus != null && nucleus.name == nucleusName) {
receptor.AddReceiver(nucleus);
}
}
if (receptor._receivers.Count == 0)
return null;
else
return receptor;
}
public virtual IReceptor ShallowCloneTo(Cluster parent) {
Receptor clone = new(parent);
return clone;
}
public override Nucleus Clone(ClusterPrefab prefab) {
Receptor clone = new(prefab, name) {
array = this._array
};
CloneFields(clone);
// Adding receivers will also add synapses to the receivers
foreach (Nucleus receiver in this.receivers.ToArray())
public virtual IReceptor ShallowCloneTo(ClusterPrefab parent) {
Receptor clone = new(parent);
return clone;
}
public virtual IReceptor CloneTo(ClusterPrefab parent) {
Receptor clone = new(parent);
foreach (INucleus receiver in this.receivers) {
clone.AddReceiver(receiver);
}
return clone;
}
public virtual IReceptor Clone() {
Receptor clone = new(this.cluster);
foreach (INucleus receiver in this.receivers) {
clone.AddReceiver(receiver);
}
return clone;
}
class Receiver {
public INucleus nucleus;
public int thingId;
public string thingName;
public Receiver(INucleus nucleus, int thingId, string thingName) {
this.nucleus = nucleus;
this.thingId = thingId;
this.thingName = thingName;
}
}
[SerializeReference]
private NucleusArray _array;
public NucleusArray array {
set { _array = value; }
private List<INucleus> _receivers = new();
public List<INucleus> receivers {
get { return _receivers; }
set { _receivers = value; }
}
public Nucleus[] nucleiArray {
get { return _array.nuclei; }
set { _array.nuclei = value; }
protected int[] thingIds; // every receiver can handle a thing with this id
public virtual void AddReceiver(INucleus receivingNucleus, float weight = 1) {
this._receivers.Add(receivingNucleus);
receivingNucleus.AddSynapse(this, weight);
}
public void AddReceptorElement(ClusterPrefab prefab) {
this.nucleiArray = IReceptorHelpers.AddReceptorElement(this.nucleiArray, prefab);
public void RemoveReceiver(INucleus receiverNucleus) {
this._receivers.RemoveAll(receiver => receiver == receiverNucleus);
receiverNucleus.synapses.RemoveAll(synapse => synapse.nucleus == this);
}
public void RemoveReceptorElement() {
this.nucleiArray = IReceptorHelpers.RemoveReceptorElement(this.nucleiArray);
private int stale = 1000;
private bool _isSleeping = false;
public bool isSleeping => _isSleeping;
public Vector3 localPosition {
set {
this.stale = 0;
this._isSleeping = false;
this._outputValue = value;
}
}
public float distanceResolution = 0.1f;
public float directionResolution = 5;
private float3 _outputValue;
public float3 outputValue {
get { return this._outputValue; }
set {
this.stale = 0;
this._isSleeping = false;
this._outputValue = value;
}
}
public virtual void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1) {
foreach (Nucleus element in this._array.nuclei) {
if (element is Neuron neuron) {
neuron.AddReceiver(receiverToAdd, weight);
public virtual void ProcessStimulus(int thingId, Vector3 newLocalPositionVector, string thingName = null) {
this.localPosition = newLocalPositionVector;
if (this._receivers == null)
return;
thingIds ??= new int[this._receivers.Count];
int receiverIx = 0;
INucleus selectedReceiver = null;
int selectedReceiverIx = 0;
foreach (INucleus receiver in this.receivers) {
if (thingIds[receiverIx] == thingId) {
// We found an existing receiver for this thing
selectedReceiver = receiver;
selectedReceiverIx = receiverIx;
// Do not look any further
break;
}
else if (receiver.isSleeping) {
// A sleeping receiver is not active and can therefore always be used
selectedReceiver = receiver;
selectedReceiverIx = receiverIx;
// Look further because we may find an existing receiver for this thing
}
else if (selectedReceiver == null) {
// If we haven't found a receiver yet, just start by taking the first
selectedReceiver = receiver;
selectedReceiverIx = receiverIx;
}
else if (selectedReceiver.isSleeping == false) {
// If no existing or sleeping receiver is found, we look for
// the receiver with the furthest/least interesting stimulus
if (length(receiver.outputValue) < length(selectedReceiver.outputValue)) {
// Debug.Log($"{selectedReceiver.name}[{selectedReceiverIx}] {length(selectedReceiver.outputValue)}" +
// $" {receiver.name}[{receiverIx}] {length(receiver.outputValue)} ");
selectedReceiver = receiver;
selectedReceiverIx = receiverIx;
}
}
receiverIx++;
}
// Debug.Log($"Receiver {selectedReceiver.name}[{selectedReceiverIx}] for thing {thingId}");
thingIds[selectedReceiverIx] = thingId;
// if (thingName != null)
// selectedReceiver.nucleus.name = selectedReceiver.nucleus.baseName + " " + thingName;
selectedReceiver.UpdateState();
}
public override void UpdateStateIsolated() {
this.outputValue = this.bias;
//Debug.Log($"Receptor {this.name} outputvalue = {this.outputValue}");
}
public override void UpdateNuclei() {
public void UpdateNuclei() {
this.stale++;
if (this.stale > staleValueForSleep && lengthsq(this.bias) > 0) {
this.bias = new float3(0, 0, 0);
this.parent.UpdateFromNucleus(this);
}
}
public virtual void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) {
this._array ??= new NucleusArray(this.parent);
this._array.ProcessStimulus(thingId, inputValue, thingName);
this._isSleeping = this.stale > 2;
if (isSleeping)
this._outputValue = Vector3.zero;
}
}

View File

@ -1,261 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Unity.Mathematics;
using static Unity.Mathematics.math;
[Serializable]
public class ReceptorInstance : Nucleus {
public ReceptorInstance(Cluster parent, string name) {
this.parent = parent;
this.name = name;
// We explicitly do not add this to the parent, as it is serialized in the ReceptorArray
}
public ReceptorInstance(ClusterPrefab prefab, string name) {
this.clusterPrefab = prefab;
this.name = name;
// We explicitly do not add this to the prefab, as it is serialized in the ReceptorArray
}
public override Nucleus ShallowCloneTo(Cluster parent) {
ReceptorInstance clone = new(parent, name + " +1") {
receptor = this.receptor
};
return clone;
}
public override Nucleus Clone(ClusterPrefab prefab) {
ReceptorInstance clone = new(prefab, name) {
receptor = this.receptor
};
return clone;
}
[SerializeReference]
public ReceptorArray receptor;
public override void UpdateStateIsolated() {
}
}
[Serializable]
public class ReceptorArray : Nucleus {
public ReceptorArray(Cluster parent, string name) {
this.parent = parent;
this.name = name;
this._instances = new ReceptorInstance[1];
this._instances[0] = new ReceptorInstance(parent, this.name + "[0]") {
receptor = this
};
this.parent?.clusterNuclei.Add(this);
}
public ReceptorArray(ClusterPrefab prefab, string name) {
this.clusterPrefab = prefab;
this.name = name;
this._instances = new ReceptorInstance[1];
this._instances[0] = new ReceptorInstance(prefab, this.name + "[0]") {
receptor = this
};
if (this.clusterPrefab != null)
this.clusterPrefab.nuclei.Add(this);
else
Debug.LogError("No prefab when adding receptor to prefab");
}
public override Nucleus ShallowCloneTo(Cluster parent) {
ReceptorArray clone = new(parent, name) {
_instances = new ReceptorInstance[this.instances.Length]
};
for (int ix = 0; ix < this.instances.Length; ix++) {
clone._instances[ix] = new ReceptorInstance(parent, $"{this.name} [{ix}]") {
receptor = clone
};
}
return clone;
}
public override Nucleus Clone(ClusterPrefab prefab) {
ReceptorArray clone = new(prefab, this.name) {
_instances = new ReceptorInstance[this.instances.Length]
};
for (int ix = 0; ix < this.instances.Length; ix++) {
clone._instances[ix] = new ReceptorInstance(prefab, this.name) {
receptor = this
};
}
foreach (Synapse synapse in this.synapses) {
Synapse clonedSynapse = clone.AddSynapse(synapse.nucleus);
clonedSynapse.weight = synapse.weight;
}
// foreach (Nucleus receiver in this.receivers) {
// clone.AddReceiver(receiver);
// }
return clone;
}
[SerializeReference]
private ReceptorInstance[] _instances = new ReceptorInstance[0];
public ReceptorInstance[] instances {
get {
return _instances;
}
}
public void AddReceptor(ClusterPrefab prefab) {
if (this._instances.Length == 0) {
Debug.LogError("Empty receptor array, cannot add");
return;
}
int newLength = this._instances.Length + 1;
ReceptorInstance[] newArray = new ReceptorInstance[newLength];
for (int i = 0; i < this._instances.Length; i++)
newArray[i] = this._instances[i];
ReceptorInstance newReceptor = (ReceptorInstance)this._instances[0].Clone(prefab);
newReceptor.name = $"{this.name} [{this._instances.Length}]";
newArray[newLength - 1] = newReceptor;
this._instances = newArray;
}
public void RemoveReceptor() {
int newLength = this._instances.Length - 1;
if (newLength == 0) {
Debug.LogWarning("Receptor array cannot be empty");
return;
}
ReceptorInstance[] newPerceptei = new ReceptorInstance[newLength];
for (int i = 0; i < newLength; i++)
newPerceptei[i] = this._instances[i];
// Delete the last perception
if (this._instances[newLength] is Nucleus nucleus)
Neuron.Delete(nucleus); //this._nuclei[newLength]);
this._instances = newPerceptei;
}
private Dictionary<int, Nucleus> thingReceivers = new();
// public override void ProcessStimulus(int thingId, Vector3 inputValue, string thingName = null) {
// ProcessStimulus(inputValue, thingId, thingName);
// }
public virtual void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) {
CleanupReceivers();
if (!thingReceivers.TryGetValue(thingId, out Nucleus selectedReceiver)) {
//Debug.Log($" no receiver found for {thingId}");
// No existing nucleus for this thing
selectedReceiver = SelectReceptor(thingId, inputValue);
}
if (selectedReceiver == null)
return;
if (thingName != null) {
string baseName = selectedReceiver.name;
int colonPos = selectedReceiver.name.IndexOf(":");
if (colonPos > 0)
baseName = selectedReceiver.name[..colonPos];
selectedReceiver.name = baseName + ": " + thingName;
}
//if (selectedReceiver is Neuron selectedNucleus) {
selectedReceiver.stale = 0;
selectedReceiver.outputValue = inputValue;
this.parent.UpdateFromNucleus(this);
//selectedNucleus.ProcessStimulus(inputValue);
//}
}
private void CleanupReceivers() {
// Remove a thing-receiver connection when the nucleus is inactive
List<int> receiversToRemove = new();
thingReceivers ??= new();
foreach (KeyValuePair<int, Nucleus> item in thingReceivers) {
if (item.Value.isSleeping)
receiversToRemove.Add(item.Key);
}
foreach (int thingId in receiversToRemove) {
Nucleus selectedReceiver = thingReceivers[thingId];
// Debug.Log($" removed receiver for {thingId}");
thingReceivers.Remove(thingId);
int colonPos = selectedReceiver.name.IndexOf(":");
if (colonPos > 0)
selectedReceiver.name = selectedReceiver.name[..colonPos];
}
}
private Nucleus SelectReceptor(int thingId, float3 inputValue) {
// No existing nucleus for this thing
float inputMagnitude = length(inputValue);
Nucleus selectedReceiver = null;
float selectedMagnitude = 0;
this._instances ??= new ReceptorInstance[0];
foreach (Nucleus receiver in this._instances) {
if (thingReceivers.ContainsValue(receiver) == false) {
// We found an unusued receiver
// Debug.Log($"{thingId} -> [{receiver.name}]");
thingReceivers.Add(thingId, receiver);
return receiver;
}
else if (receiver.isSleeping) {
// A sleeping receiver is not active and can therefore always be used
thingReceivers.Add(thingId, receiver);
Debug.Log($"{thingId} -> [{selectedReceiver.name}]");
return receiver;
}
else if (selectedReceiver == null) {
// If we haven't found a receiver yet, just start by taking the first
selectedReceiver = receiver;
selectedMagnitude = length(selectedReceiver.outputValue);
}
// Look for the receiver with the lowest magnitude
else {
float magnitude = length(receiver.outputValue);
if (magnitude < inputMagnitude && length(receiver.outputValue) < selectedMagnitude) {
selectedReceiver = receiver;
selectedMagnitude = length(selectedReceiver.outputValue);
}
}
}
if (selectedReceiver != null) {
// Replace the receiver
// Find the thingId current associated with the receiver
int keyToRemove = thingReceivers.FirstOrDefault(r => r.Value.Equals(selectedReceiver)).Key;
if (keyToRemove != 0 || thingReceivers.ContainsKey(keyToRemove))
thingReceivers.Remove(keyToRemove);
// And add the new association
thingReceivers.Add(thingId, selectedReceiver);
}
return selectedReceiver;
}
public override void UpdateStateIsolated() {
float3 sum = this.bias;
// Receptors do not have inputs, so we ignore the synapses
foreach (Nucleus nucleus in this._instances)
sum += nucleus.outputValue;
this.outputValue = sum / _instances.Length;
this.stale = 0;
UpdateNuclei();
}
public override void UpdateNuclei() {
foreach (Nucleus nucleus in this.instances) {
nucleus.stale++;
if (nucleus.stale > staleValueForSleep && lengthsq(nucleus.outputValue) > 0) {
nucleus.outputValue = Vector3.zero;
//this.UpdateStateIsolated();
this.parent.UpdateFromNucleus(this);
}
}
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 9e915c8563642f23891b20522b3589cf

View File

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 4f343147e37db9eeda3e98058c553c92
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,365 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 10
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 0
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 13
m_BakeOnSceneLoad: 0
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 2
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 1
m_PVRDenoiserTypeIndirect: 1
m_PVRDenoiserTypeAO: 1
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 1
m_PVRFilteringGaussRadiusAO: 1
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 3
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &388118692
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 388118694}
- component: {fileID: 388118693}
m_Layer: 0
m_Name: GameObject
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &388118693
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 388118692}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9051408e82b511584998506096af4bf0, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::SelectorBrain
defaultBrain: {fileID: 11400000, guid: d5b3a22d9bb7d13aeb3174077125967b, type: 2}
input1: {x: 0, y: 0, z: 1}
input2: {x: 0, y: -2, z: 0}
output: {x: 0, y: 0, z: 0}
--- !u!4 &388118694
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 388118692}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: -2.01476, y: -0, z: 0.65362}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &968074744
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 968074747}
- component: {fileID: 968074746}
- component: {fileID: 968074745}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!81 &968074745
AudioListener:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 968074744}
m_Enabled: 1
--- !u!20 &968074746
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 968074744}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 1
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 0
orthographic size: 5
m_Depth: -1
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 3
m_HDR: 1
m_AllowMSAA: 1
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &968074747
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 968074744}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 1, z: -10}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &2011285159
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2011285161}
- component: {fileID: 2011285160}
m_Layer: 0
m_Name: Directional Light
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!108 &2011285160
Light:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2011285159}
m_Enabled: 1
serializedVersion: 12
m_Type: 1
m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1}
m_Intensity: 1
m_Range: 10
m_SpotAngle: 30
m_InnerSpotAngle: 21.80208
m_CookieSize2D: {x: 0.5, y: 0.5}
m_Shadows:
m_Type: 2
m_Resolution: -1
m_CustomResolution: -1
m_Strength: 1
m_Bias: 0.05
m_NormalBias: 0.4
m_NearPlane: 0.2
m_CullingMatrixOverride:
e00: 1
e01: 0
e02: 0
e03: 0
e10: 0
e11: 1
e12: 0
e13: 0
e20: 0
e21: 0
e22: 1
e23: 0
e30: 0
e31: 0
e32: 0
e33: 1
m_UseCullingMatrixOverride: 0
m_Cookie: {fileID: 0}
m_DrawHalo: 0
m_Flare: {fileID: 0}
m_RenderMode: 0
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingLayerMask: 1
m_Lightmapping: 4
m_LightShadowCasterMode: 0
m_AreaSize: {x: 1, y: 1}
m_BounceIntensity: 1
m_ColorTemperature: 6570
m_UseColorTemperature: 0
m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
m_UseBoundingSphereOverride: 0
m_UseViewFrustumForShadowCasterCull: 1
m_ForceVisible: 0
m_ShadowRadius: 0
m_ShadowAngle: 0
m_LightUnit: 1
m_LuxAtDistance: 1
m_EnableSpotReflector: 1
--- !u!4 &2011285161
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2011285159}
serializedVersion: 2
m_LocalRotation: {x: 0.40821788, y: -0.23456968, z: 0.10938163, w: 0.8754261}
m_LocalPosition: {x: 0, y: 3, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 968074747}
- {fileID: 2011285161}
- {fileID: 388118694}

View File

@ -1,62 +0,0 @@
using System;
using Unity.Mathematics;
using static Unity.Mathematics.math;
[Serializable]
public class Selector : Neuron {
public Selector(Cluster parent, string name) : base(parent, name) { }
public Selector(ClusterPrefab parent, string name) : base(parent, name) {}
public override Nucleus ShallowCloneTo(Cluster newParent) {
Selector clone = new(newParent, this.name) {
// array = this.array,
curve = this.curve,
curvePreset = this.curvePreset,
curveMax = this.curveMax,
};
return clone;
}
public override void UpdateStateIsolated() { //float3 bias) {
float3 max = this.bias;
float maxSqrLength = lengthsq(max);
//Applying the weight factors
foreach (Synapse synapse in this.synapses) {
float3 input = synapse.weight * synapse.nucleus.outputValue;
float inputSqrlength = lengthsq(input);
if (inputSqrlength > maxSqrLength) {
max = input;
maxSqrLength = inputSqrlength;
}
}
// Activation function
float3 result;
switch (this.curvePreset) {
case CurvePresets.Linear:
result = max;
break;
case CurvePresets.Sqrt:
result = normalize(max) * System.MathF.Sqrt(length(max));
break;
case CurvePresets.Power:
result = normalize(max) * System.MathF.Pow(length(max), 2);
break;
case CurvePresets.Reciprocal: {
float magnitude = length(max);
if (magnitude > 0)
result = normalize(max) * (1 / magnitude);
else
result = float3(0, 0, 0);
break;
}
default:
float activatedValue = this.curve.Evaluate(length(max));
result = normalize(max) * activatedValue;
break;
}
this.outputValue = result;
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 4218c2f3f15af944db0eadd6e1500d17

View File

@ -3,12 +3,19 @@ using UnityEngine;
[Serializable]
public class Synapse {
// Support access to cluster of basic nucleus
//public IReceptor nucleus => basicNucleus;
//[SerializeReference]
//public Cluster cluster;
[SerializeReference]
public Nucleus nucleus;
public IReceptor nucleus;
public float weight;
public Synapse(Nucleus nucleus, float weight = 1.0f) {
public Synapse(IReceptor nucleus, float weight = 1.0f) {
this.nucleus = nucleus;
this.weight = weight;
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 363b69b84de0e4b729794c10e7c40ab5
guid: 62a58c801eda0c9eab7a49fb1d0840cb
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 2c1e3956a0b70ae6b8d09fb467b73621
guid: e47ea55fc051fcdcb8ae6197d1105cc0
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@ -0,0 +1,728 @@
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using Unity.Mathematics;
using static Unity.Mathematics.math;
[CustomEditor(typeof(ClusterPrefab))]
public class ClusterInspector : Editor {
protected static VisualElement mainContainer;
protected static VisualElement inspectorContainer;
protected bool breakOnWake = false;
#region Start
public override VisualElement CreateInspectorGUI() {
ClusterPrefab cluster = target as ClusterPrefab;
serializedObject.Update();
VisualElement root = new();
//root.style.flexDirection = FlexDirection.Row; // side-by-side layout
//root.style.flexGrow = 1;
//root.style.minHeight = 600;
root.style.paddingLeft = 0;
root.style.paddingRight = 0;
root.style.paddingTop = 0;
root.style.paddingBottom = 0;
root.styleSheets.Add(Resources.Load<StyleSheet>("GraphStyles"));
mainContainer = new() {
// name = "main",
style = {
// flexDirection = FlexDirection.Row,
// flexGrow = 1,
height = 450,
}
};
GraphView graph = new();
graph.style.flexGrow = 1;
inspectorContainer = new VisualElement {
// name = "inspector"
};
mainContainer.Add(graph);
mainContainer.Add(inspectorContainer);
root.Add(mainContainer);
// Run once for initial state (use resolved style width if available)
float initialWidth = root.layout.width > 0 ? root.layout.width : root.contentRect.width;
UpdateLayout(initialWidth);
// React to size changes of root (or parent if appropriate)
root.RegisterCallback<GeometryChangedEvent>(evt => {
UpdateLayout(evt.newRect.width);
});
if (cluster != null) {
cluster.EnsureInitialization();
graph.SetGraph(null, cluster, cluster.output, inspectorContainer);
}
else
Debug.LogWarning(" No brain!");
serializedObject.ApplyModifiedProperties();
return root;
}
public class GraphView : VisualElement {
ClusterPrefab cluster;
SerializedObject serializedBrain;
INucleus currentNucleus;
GameObject gameObject;
private List<NeuroidLayer> layers = new();
private readonly Dictionary<IReceptor, Vector2Int> neuroidPositions = new();
private bool expandArray = false;
//Vector2 pan = Vector2.zero;
//float zoom = 1f;
//bool draggingCanvas = false;
//Vector2 lastMouse;
ClusterWrapper currentWrapper;
public GraphView() {
name = "content";
style.flexGrow = 1;
IMGUIContainer imguiContainer = new(OnIMGUI);
imguiContainer.style.position = Position.Absolute;
imguiContainer.style.left = 0; imguiContainer.style.top = 0;
imguiContainer.style.right = 0; imguiContainer.style.bottom = 0;
imguiContainer.pickingMode = PickingMode.Position;
imguiContainer.focusable = true;
Add(imguiContainer);
//RegisterCallback<WheelEvent>(OnWheel);
// RegisterCallback<MouseDownEvent>(OnMouseDown);
// RegisterCallback<MouseMoveEvent>(OnMouseMove);
// RegisterCallback<MouseUpEvent>(OnMouseUp);
// Subscribe when added to panel (editor UI ready)
RegisterCallback<AttachToPanelEvent>(evt => Subscribe());
RegisterCallback<DetachFromPanelEvent>(evt => Unsubscribe());
}
bool subscribed = false;
void Subscribe() {
if (subscribed) return;
SceneView.duringSceneGui += OnSceneGUI;
subscribed = true;
SceneView.RepaintAll();
}
void Unsubscribe() {
if (!subscribed) return;
SceneView.duringSceneGui -= OnSceneGUI;
subscribed = false;
}
public void SetGraph(GameObject gameObject, ClusterPrefab brain, INucleus nucleus, VisualElement inspectorContainer) {
this.gameObject = gameObject;
this.cluster = brain;
if (Application.isPlaying == false)
this.serializedBrain = new SerializedObject(brain);
this.currentNucleus = nucleus;
Rebuild(inspectorContainer);
}
void Rebuild(VisualElement inspectorContainer) {
BuildLayers();
if (this.currentNucleus == null) {
inspectorContainer.Clear();
return;
}
if (currentWrapper != null)
DestroyImmediate(currentWrapper);
currentWrapper = CreateInstance<ClusterWrapper>().Init(this.currentNucleus, cluster);
DrawInspector(inspectorContainer);
}
private void BuildLayers() {
// A temporary list to track what's been added to layers
this.layers = new();
int layerIx = 0;
INucleus selectedNucleus = this.currentNucleus;
if (selectedNucleus == null)
return;
NeuroidLayer currentLayer = new() { ix = layerIx };
if (selectedNucleus.receivers != null) {
foreach (INucleus receiver in selectedNucleus.receivers) {
INucleus outputNeuroid = receiver;
if (outputNeuroid != null) {
AddToLayer(currentLayer, outputNeuroid);
// Debug.Log($"layer {layerIx} nucleus {outputNeuroid.name}");
}
}
}
if (currentLayer.neuroids.Count > 0) {
this.layers.Add(currentLayer);
layerIx++;
currentLayer = new() { ix = layerIx };
}
AddToLayer(currentLayer, selectedNucleus);
this.layers.Add(currentLayer);
// Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}");
layerIx++;
currentLayer = new() { ix = layerIx };
if (selectedNucleus.synapses != null) {
foreach (Synapse synapse in selectedNucleus.synapses) {
IReceptor input = synapse.nucleus;
AddToLayer(currentLayer, input);
// Debug.Log($"layer {layerIx} nucleus {input.name}");
}
}
if (currentLayer.neuroids.Count > 0) {
this.layers.Add(currentLayer);
}
}
private void AddToLayer(NeuroidLayer layer, IReceptor nucleus) {
if (nucleus == null)
return;
layer.neuroids.Add(nucleus);
//nucleus.layerIx = layer.ix;
// Store its position
Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1);
neuroidPositions[nucleus] = neuroidPosition;
}
public void OnIMGUI() {
if (currentNucleus == null)
return;
if (Application.isPlaying == false)
serializedBrain.Update();
Handles.BeginGUI();
DrawGraph();
Handles.EndGUI();
}
private void DrawGraph() {
float size = 20;
Vector3 position = new(150, 210, 0);
DrawReceivers(this.currentNucleus, position, size);
DrawSynapses(this.currentNucleus, position, size);
// Draw selected Nucleus
if (expandArray) {
float maxValue = 0;
foreach (INucleus nucleus in this.currentNucleus.array.nuclei) {
float value = length(nucleus.outputValue);
if (value > maxValue)
maxValue = value;
}
float spacing = 400f / this.currentNucleus.array.nuclei.Count();
float margin = 10 + spacing / 2;
float xMin = 150 - size;
float xMax = 150 + size;
float yMin = 10 + margin - size / 2;
float yMax = 400 - margin + size;
Vector3[] verts = new Vector3[4] {
new(xMin, yMin, 0),
new(xMax, yMin, 0),
new(xMax, yMax, 0),
new(xMin, yMax, 0)
};
Handles.color = Color.black;
Handles.DrawAAConvexPolygon(verts);
int row = 0;
foreach (INucleus nucleus in this.currentNucleus.array.nuclei) {
Vector3 pos = new(150, margin + row * spacing, 0.0f);
Handles.color = Color.white;
//Handles.DrawLine(parentPos, pos);
Handles.color = Color.white;
Handles.DrawSolidDisc(pos, Vector3.forward, size + 2);
DrawNucleus(nucleus, pos, maxValue, size);
row++;
}
GUIStyle style = new(EditorStyles.label) {
alignment = TextAnchor.UpperCenter,
normal = { textColor = Color.white },
fontStyle = FontStyle.Bold,
};
Vector3 labelPos = new Vector3(150, yMax, 0) - Vector3.down * (size + 10); // below disc along up axis
Handles.Label(labelPos, this.currentNucleus.name, style);
}
else {
Handles.color = Color.white;
Handles.DrawSolidDisc(position, Vector3.forward, size + 2);
DrawNucleus(this.currentNucleus, position, length(this.currentNucleus.outputValue), 20);
}
}
private void DrawReceivers(INucleus nucleus, Vector3 parentPos, float size) {
int nodeCount = nucleus.receivers.Count();
// Determine the maximum value in this layer
// This is used to 'scale' the output value colors of the nuclei
float maxValue = 0;
foreach (INucleus receiver in nucleus.receivers) {
if (receiver is Neuron neuroid) {
float value = length(neuroid.outputValue);
if (value > maxValue)
maxValue = value;
}
}
// Determine the spacing of the nuclei in the layer
float spacing = 400f / nodeCount;
float margin = 10 + spacing / 2;
int row = 0;
foreach (INucleus receiver in nucleus.receivers) {
INucleus receiverNucleus = receiver;
if (receiverNucleus == null)
continue;
Vector3 pos = new(50, margin + row * spacing, 0.0f);
Handles.color = Color.white;
Handles.DrawLine(parentPos, pos);
DrawNucleus(receiverNucleus, pos, maxValue, size);
row++;
}
}
private void DrawSynapses(INucleus nucleus, Vector3 parentPos, float size) {
int nodeCount = nucleus.synapses.Count;
// Determine the maximum value in this layer
// This is used to 'scale' the output value colors of the nuclei
float maxValue = 0;
int neuronCount = 0;
List<NucleusArray> drawnArrays = new();
foreach (Synapse synapse in nucleus.synapses) {
if (synapse.nucleus is Neuron neuroid) {
if (drawnArrays.Contains(neuroid.array))
continue;
drawnArrays.Add(neuroid.array);
}
float value = length(synapse.nucleus.outputValue) * synapse.weight;
// Debug.Log($"{synapse.nucleus.name}: {value} {length(synapse.nucleus.outputValue)} {synapse.weight}");
if (value > maxValue)
maxValue = value;
neuronCount++;
}
// Determine the spacing of the nuclei in the layer
float spacing = 400f / neuronCount;
float margin = 10 + spacing / 2;
int row = 0;
drawnArrays = new();
foreach (Synapse synapse in nucleus.synapses) {
if (synapse.nucleus is Neuron neuron) {
if (drawnArrays.Contains(neuron.array))
continue;
drawnArrays.Add(neuron.array);
}
Vector3 pos = new(250, margin + row * spacing, 0.0f);
Handles.color = Color.white;
Handles.DrawLine(parentPos, pos);
if (synapse.nucleus != null) {
Color color = Color.black;
if (synapse.nucleus.isSleeping)
color = Color.darkRed;
else if (Application.isPlaying) {
float brightness = length(synapse.nucleus.outputValue) * synapse.weight / maxValue;
color = new Color(brightness, brightness, brightness, 1f);
}
DrawNucleus(synapse.nucleus, pos, maxValue, size, color);
}
row++;
}
}
private void DrawNucleus(IReceptor nucleus, Vector3 position, float maxValue, float size) {
Color color;
if (nucleus.isSleeping)
color = Color.darkRed;
else {
if (Application.isPlaying) {
float brightness = length(nucleus.outputValue) / maxValue;
color = new Color(brightness, brightness, brightness, 1f);
}
else
color = Color.black;
}
DrawNucleus(nucleus, position, maxValue, size, color);
}
private void DrawNucleus(IReceptor nucleus, Vector3 position, float maxValue, float size, Color color) {
if (nucleus is MemoryCell memory) {
Handles.color = Color.white;
Handles.DrawWireDisc(position + Vector3.right * 10, Vector3.forward, size);
}
Handles.color = color;
Handles.DrawSolidDisc(position, Vector3.forward, size);
Handles.color = Color.white;
// Position the label in front of the disc
Vector3 labelPosition = position + (Vector3.forward * 0.1f);
GUIStyle style = new(EditorStyles.label) {
alignment = TextAnchor.MiddleCenter,
normal = { textColor = Color.white },
fontStyle = FontStyle.Bold,
};
if (nucleus is INucleus neuron) {
if (neuron.array == null || neuron.array.nuclei == null || neuron.array.nuclei.Count() == 0)
neuron.array = new NucleusArray(neuron);
if ((!expandArray || neuron.array.nuclei.First() != this.currentNucleus) && neuron.array.nuclei.Count() > 1) {
Handles.Label(labelPosition, neuron.array.nuclei.Count().ToString(), style);
}
if (expandArray && neuron.array.nuclei.First() == this.currentNucleus) {
int arrayIx = 0;
foreach (INucleus n in neuron.array.nuclei) {
if (n == neuron)
break;
arrayIx++;
}
Handles.Label(labelPosition, $"[{arrayIx}]", style);
}
else {
style.alignment = TextAnchor.UpperCenter;
Vector3 labelPos = position - Vector3.down * (size + 10f); // below disc along up axis
Handles.Label(labelPos, nucleus.name, style);
}
if (nucleus is Cluster cluster) {
Handles.color = Color.white;
Handles.DrawWireDisc(position, Vector3.forward, size + 10);
}
}
else {
style.alignment = TextAnchor.UpperCenter;
Vector3 labelPos = position - Vector3.down * (size + 10); // below disc along up axis
Handles.Label(labelPos, nucleus.name, style);
}
Rect neuronRect = new(position.x - size, position.y - size, size * 2, size * 2);
int id = GUIUtility.GetControlID(FocusType.Passive);
Event e = Event.current;
EventType et = e.GetTypeForControl(id);
if (e != null && neuronRect.Contains(e.mousePosition)) {
// Process Hover
HandleMouseHover(nucleus, neuronRect);
// Process click
if (e.type == EventType.MouseDown && e.button == 0) {
// Consume the event so the scene doesn't also handle it
e.Use();
HandleClicked(nucleus);
}
}
}
private void HandleMouseHover(IReceptor nucleus, Rect rect) {
GUIContent tooltip;
if (nucleus is INucleus n) {
tooltip = new(
$"{nucleus.name}" +
$"\nsynapse count {n.synapses.Count}" +
$"\nValue: {nucleus.outputValue}");
}
else {
tooltip = new(
$"{nucleus.name}" +
$"\nValue: {nucleus.outputValue}");
}
Vector2 mousePosition = Event.current.mousePosition;
// Display tooltip with some offset
Vector2 tooltipSize = GUI.skin.box.CalcSize(tooltip);
Rect tooltipRect = new Rect(mousePosition.x + 10, mousePosition.y + 10, tooltipSize.x, tooltipSize.y);
GUI.Box(tooltipRect, tooltip);
}
private void HandleClicked(IReceptor nucleus) {
if (nucleus == this.currentNucleus) {
if (nucleus is INucleus n) {
expandArray = !expandArray;
return;
}
}
else if (nucleus is INucleus n) {
this.currentNucleus = n;
BuildLayers();
}
}
void DrawInspector(VisualElement inspectorContainer) {
if (inspectorContainer == null)
return;
inspectorContainer.Clear();
if (this.currentNucleus == null)
return;
// create a SerializedObject wrapper so Unity inspector controls work (and Undo)
SerializedObject so = new(currentWrapper);
IMGUIContainer container = new(() => {
if (so.targetObject == null)
return;
so.Update();
if (this.currentNucleus == null)
return;
this.currentNucleus.name = EditorGUILayout.TextField(this.currentNucleus.name);
if (this.currentNucleus is Neuron neuroid) {
if (this.currentNucleus is MemoryCell memory) {
}
else {
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Activation Curve", GUILayout.Width(150));
if (neuroid.curveMax > 0)
EditorGUILayout.CurveField(neuroid.curve, Color.cyan, new Rect(0, 0, 1, neuroid.curveMax));
else
EditorGUILayout.CurveField(neuroid.curve, Color.cyan, new Rect(0, neuroid.curveMax, 1, -neuroid.curveMax));
neuroid.curvePreset = (Neuron.CurvePresets)EditorGUILayout.EnumPopup(neuroid.curvePreset, GUILayout.Width(100));
EditorGUILayout.EndHorizontal();
}
if (neuroid.array == null || neuroid.array.nuclei == null || neuroid.array.nuclei.Count() == 0)
neuroid.array = new NucleusArray(neuroid);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.IntField("Array size", neuroid.array.nuclei.Count());
if (GUILayout.Button("Add"))
neuroid.array.AddNucleus();
if (GUILayout.Button("Del"))
neuroid.array.RemoveNucleus();
EditorGUILayout.EndHorizontal();
}
if (Application.isPlaying)
EditorGUILayout.FloatField("Output", length(this.currentNucleus.outputValue));
else
EditorGUILayout.LabelField(" ");
if (this.currentNucleus.synapses.Count > 0) {
EditorGUILayout.LabelField("Synapses");
Synapse[] synapses = this.currentNucleus.synapses.ToArray();
foreach (Synapse synapse in synapses) {
if (synapse.nucleus != null) {
EditorGUILayout.Space();
//EditorGUI.BeginDisabledGroup(synapse.nucleus.isSleeping);
if (Application.isPlaying)
EditorGUILayout.FloatField(synapse.nucleus.name, length(synapse.nucleus.outputValue) * synapse.weight);
else {
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(synapse.nucleus.name);
if (GUILayout.Button("Disconnect"))
synapse.nucleus.RemoveReceiver(this.currentNucleus);
EditorGUILayout.EndHorizontal();
}
EditorGUI.indentLevel++;
synapse.weight = EditorGUILayout.FloatField("Weight", synapse.weight);
EditorGUI.indentLevel--;
//EditorGUI.EndDisabledGroup();
}
}
}
EditorGUILayout.Space();
ConnectNucleus(this.cluster, this.currentNucleus);
if (GUILayout.Button("Add Input Neuron"))
AddInputNeuron(this.currentNucleus);
if (GUILayout.Button("Add Input MemoryCell"))
AddInputMemoryCell(this.currentNucleus);
if (GUILayout.Button("Add Input Cluster"))
AddCluster(this.currentNucleus);
EditorGUILayout.Space();
if (GUILayout.Button("Delete this neuron"))
DeleteNeuron(this.currentNucleus);
if (this.currentNucleus is Cluster subCluster) {
if (GUILayout.Button("Edit Cluster"))
EditCluster(subCluster);
}
// if (this.gameObject != null) {
// Vector3 worldVector = this.gameObject.transform.TransformVector(this.currentNucleus.outputValue);
// //Debug.DrawRay(this.gameObject.transform.position, worldVector, Color.yellow);
// Handles.color = Color.yellow;
// Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector);
// }
});
inspectorContainer.Add(container);
}
void OnSceneGUI(SceneView sceneView) {
if (this.gameObject != null) {
Vector3 worldVector = this.gameObject.transform.TransformVector(this.currentNucleus.outputValue);
Handles.color = Color.yellow;
Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector);
}
}
protected virtual void AddInputNeuron(INucleus nucleus) {
Neuron newNeuroid = new(this.cluster, "New neuron");
newNeuroid.AddReceiver(nucleus);
this.currentNucleus = newNeuroid;
BuildLayers();
}
protected virtual void DeleteNeuron(INucleus nucleus) {
if (nucleus == null)
return;
if (nucleus.cluster != null)
this.currentNucleus = nucleus.cluster.output;
foreach (INucleus receiver in nucleus.receivers) {
if (receiver != null) {
this.currentNucleus = receiver;
break;
}
}
Neuron.Delete(nucleus);
BuildLayers();
}
protected virtual void AddInputMemoryCell(INucleus nucleus) {
MemoryCell newMemory = new(this.cluster, "New memory cell");
newMemory.AddReceiver(nucleus);
this.currentNucleus = newMemory;
BuildLayers();
}
protected virtual void AddCluster(INucleus nucleus) {
ClusterPickerWindow.ShowPicker(brain => OnClusterPicked(nucleus, brain), "Select Cluster");
}
private void OnClusterPicked(INucleus nucleus, ClusterPrefab prefab) {
Cluster subclusterInstance = new(prefab, this.cluster);
subclusterInstance.AddReceiver(nucleus);
// This does not work somehow
// this.currentNucleus = subclusterInstance;
// BuildLayers();
}
private void EditCluster(Cluster subCluster) {
// May be used with storedPrefab...
Selection.activeObject = subCluster.prefab;
EditorGUIUtility.PingObject(subCluster.prefab);
var editor = Editor.CreateEditor(subCluster.prefab);
}
// Connect to another nucleus in the same cluster
protected virtual void ConnectNucleus(ClusterPrefab cluster, INucleus nucleus) {
if (cluster == null)
return;
IEnumerable<string> synapseNuclei = this.currentNucleus.synapses.Select(synapse => synapse.nucleus != null ? synapse.nucleus.name : "");
//IEnumerable<string> perceptei = this.currentNucleus.brain.perceptei.Select(i => i.name).Except(synapseNuclei);
IEnumerable<string> nuclei = cluster.nuclei.Select(i => i.name).Except(synapseNuclei);
//string[] names = perceptei.Concat(nuclei).ToArray();
string[] names = nuclei.ToArray();
int selectedIndex = -1;
selectedIndex = EditorGUILayout.Popup("Connect to", selectedIndex, names);
if (selectedIndex >= 0) {
// if (selectedIndex < perceptei.Count()) {
// Nucleus n = this.currentNucleus.brain.perceptei[selectedIndex];
// n.AddReceiver(this.currentNucleus);
// }
// else {
// Nucleus n = this.currentNucleus.brain.nuclei[selectedIndex - perceptei.Count()];
// n.AddReceiver(this.currentNucleus);
// }
IReceptor receptor = cluster.nuclei[selectedIndex];
receptor.AddReceiver(this.currentNucleus);
}
}
protected virtual void DisconnectNucleus(Neuron nucleus) {
if (this.currentNucleus.cluster == null)
return;
string[] names = this.currentNucleus.synapses.Select(synapse => synapse.nucleus.name).ToArray();
int selectedIndex = -1;
selectedIndex = EditorGUILayout.Popup("Disconnect from", selectedIndex, names);
//if (selectedIndex >= 0 && selectedIndex < this.currentNucleus.brain.perceptei.Count) {
if (selectedIndex >= 0 && selectedIndex < this.currentNucleus.cluster.nuclei.Count) {
Synapse synapse = this.currentNucleus.synapses[selectedIndex];
synapse.nucleus.RemoveReceiver(this.currentNucleus);
}
}
}
#endregion Start
#region Update
private void UpdateLayout(float containerWidth) {
if (containerWidth > 600f) {
mainContainer.style.flexDirection = FlexDirection.Row;
inspectorContainer.style.width = 300; // fixed sidebar width
inspectorContainer.style.flexGrow = 0;
}
else {
mainContainer.style.flexDirection = FlexDirection.Column;
inspectorContainer.style.width = Length.Percent(100); // full width below
inspectorContainer.style.flexDirection = FlexDirection.Column;
inspectorContainer.style.flexGrow = 1; // can set 0 or keep as needed
}
}
#endregion Update
}
public class NeuroidLayer {
public int ix = 0;
public List<IReceptor> neuroids = new();
}
public class ClusterWrapper : ScriptableObject {
// expose fields that map to GraphNode
//public string title;
public Vector2 position;
INucleus node;
ClusterPrefab graph; // needed to write back and mark dirty
public ClusterWrapper Init(INucleus node, ClusterPrefab graphAsset) {
this.node = node;
this.graph = graphAsset;
//this.title = " A " + node.name;
//position = node.position;
return this;
}
void OnValidate() {
if (node != null) {
//node.name = title;
//node.position = position;
#if UNITY_EDITOR
if (graph != null)
UnityEditor.EditorUtility.SetDirty(graph);
#endif
}
}
}

View File

@ -0,0 +1,129 @@
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
[CustomEditor(typeof(NanoBrainComponent))]
public class NanoBrainComponent_Editor : Editor {
protected static VisualElement mainContainer;
protected static VisualElement inspectorContainer;
protected NanoBrainComponent component;
private SerializedProperty brainProp;
ClusterInspector.GraphView board;
public void OnEnable() {
component = target as NanoBrainComponent;
if (Application.isPlaying == false)
brainProp = serializedObject.FindProperty(nameof(NanoBrainComponent.defaultBrain));
}
public override VisualElement CreateInspectorGUI() {
//ClusterPrefab brain = Application.isPlaying ? component.brain.prefab : component.defaultBrain;
Cluster brain = component.brain;
if (Application.isPlaying == false)
serializedObject.Update();
VisualElement root = new();
root.style.flexDirection = FlexDirection.Column; // side-by-side layout
root.style.flexGrow = 1;
root.style.minHeight = 600;
root.style.paddingLeft = 0;
root.style.paddingRight = 0;
root.style.paddingTop = 0;
root.style.paddingBottom = 0;
root.styleSheets.Add(Resources.Load<StyleSheet>("GraphStyles"));
if (Application.isPlaying == false) {
PropertyField brainField = new(brainProp) {
label = "Nano Brain"
};
root.Add(brainField);
}
mainContainer = new() {
name = "main",
style = {
flexDirection = FlexDirection.Row,
flexGrow = 1,
minHeight = 500,
}
};
board = new ClusterInspector.GraphView();
board.style.flexGrow = 1;
mainContainer.Add(board);
inspectorContainer = new VisualElement {
name = "inspector",
style = {
width = 400,
}
};
mainContainer.Add(inspectorContainer);
root.Add(mainContainer);
// Run once for initial state (use resolved style width if available)
float initialWidth = root.layout.width > 0 ? root.layout.width : root.contentRect.width;
UpdateLayout(initialWidth);
// React to size changes of root (or parent if appropriate)
root.RegisterCallback<GeometryChangedEvent>(evt => {
UpdateLayout(evt.newRect.width);
});
if (brain != null && board != null)
board.SetGraph(component.gameObject, brain.prefab, brain.output, inspectorContainer);
// else
// Debug.LogWarning(" No brain!");
if (Application.isPlaying == false)
serializedObject.ApplyModifiedProperties();
return root;
}
// void OnSceneGUI() {
// if (Application.isPlaying && board != null)
// board.OnIMGUI();
// }
void OnSceneGui(SceneView sv) {
if (Application.isPlaying == false)
return;
// May need some throttling here...
if (board != null) {
Debug.Log(".");
board.OnIMGUI();
}
// EditorApplication.delayCall = UpdateInspectorUI;
}
void UpdateInspectorUI() {
if (board != null) {
Debug.Log(".");
board.OnIMGUI();
}
}
private void UpdateLayout(float containerWidth) {
if (containerWidth > 800f) {
mainContainer.style.flexDirection = FlexDirection.Row;
inspectorContainer.style.width = 400; // fixed sidebar width
inspectorContainer.style.flexGrow = 0;
}
else {
mainContainer.style.flexDirection = FlexDirection.Column;
inspectorContainer.style.width = Length.Percent(100); // full width below
inspectorContainer.style.flexDirection = FlexDirection.Column;
inspectorContainer.style.flexGrow = 1; // can set 0 or keep as needed
}
}
}

View File

@ -0,0 +1,511 @@
/*
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.Callbacks;
using System.Linq;
using System.Collections.Generic;
public class NucleusLayer {
public int ix = 0;
public List<Nucleus> neuroids = new();
}
public class NanoBrainEditor : EditorWindow {
public NanoBrain brain;
public static VisualElement inspectorContainer;
[MenuItem("Window/NanoBrain Editor")]
public static void ShowWindow() {
GetWindow<NanoBrainEditor>("NanoBrain Editor");
}
public static void Open(NanoBrain asset) {
NanoBrainEditor editor = GetWindow<NanoBrainEditor>("NanoBrain Editor");
editor.brain = asset;
editor.Show();
}
GraphBoardView board;
private void OnEnable() {
OnFocus();
}
private void OnFocus() {
if (brain == null) {
// brain = CreateInstance<NanoBrainObj>();
// EditorUtility.SetDirty(brain);
return;
}
VisualElement root = rootVisualElement;
root.Clear();
root.styleSheets.Add(Resources.Load<StyleSheet>("GraphStyles"));
VisualElement main = new() {
name = "main",
style = {
flexDirection = FlexDirection.Row,
flexGrow = 1
}
};
board = new GraphBoardView();
board.style.flexGrow = 1;
inspectorContainer = new VisualElement {
name = "inspector",
style = {
width = 400
}
};
main.Add(board);
main.Add(inspectorContainer);
root.Add(main);
board.SetGraph(brain, brain.root);
}
}
public class GraphBoardView : VisualElement {
NanoBrain brain;
SerializedObject serializedBrain;
Nucleus currentNucleus;
private List<NeuroidLayer> layers = new();
private Dictionary<Nucleus, Vector2Int> neuroidPositions = new();
Vector2 pan = Vector2.zero;
//float zoom = 1f;
bool draggingCanvas = false;
Vector2 lastMouse;
GraphNodeWrapper currentWrapper;
public GraphBoardView() {
name = "content";
style.flexGrow = 1;
IMGUIContainer imguiContainer = new(OnIMGUI);
imguiContainer.style.position = Position.Absolute;
imguiContainer.style.left = 0; imguiContainer.style.top = 0;
imguiContainer.style.right = 0; imguiContainer.style.bottom = 0;
imguiContainer.pickingMode = PickingMode.Position;
imguiContainer.focusable = true;
Add(imguiContainer);
//RegisterCallback<WheelEvent>(OnWheel);
RegisterCallback<MouseDownEvent>(OnMouseDown);
RegisterCallback<MouseMoveEvent>(OnMouseMove);
RegisterCallback<MouseUpEvent>(OnMouseUp);
}
public void SetGraph(NanoBrain brain, Nucleus nucleus) {
this.brain = brain;
this.serializedBrain = new SerializedObject(brain);
this.currentNucleus = nucleus;
Rebuild();
}
void Rebuild() {
BuildLayers();
if (currentNucleus == null) {
NanoBrainEditor.inspectorContainer.Clear();
return;
}
if (currentWrapper != null)
Object.DestroyImmediate(currentWrapper);
currentWrapper = ScriptableObject.CreateInstance<GraphNodeWrapper>().Init(currentNucleus, brain);
DrawInspector();
}
private void BuildLayers() {
// A temporary list to track what's been added to layers
this.layers = new();
int layerIx = 0;
Nucleus selectedNucleus = this.currentNucleus;
if (selectedNucleus == null)
return;
NeuroidLayer currentLayer = new() { ix = layerIx };
//foreach (Nucleus outputNeuroid in selectedNucleus.receivers) {
foreach (Receiver receiver in selectedNucleus.receivers) {
Nucleus outputNeuroid = receiver.nucleus;
if (outputNeuroid != null) {
AddToLayer(currentLayer, outputNeuroid);
// Debug.Log($"layer {layerIx} nucleus {outputNeuroid.name}");
}
}
if (currentLayer.neuroids.Count > 0) {
this.layers.Add(currentLayer);
layerIx++;
currentLayer = new() { ix = layerIx };
}
AddToLayer(currentLayer, selectedNucleus);
this.layers.Add(currentLayer);
// Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}");
layerIx++;
currentLayer = new() { ix = layerIx };
//foreach (Nucleus input in selectedNucleus.synapses.Keys) {
foreach (Synapse synapse in selectedNucleus.synapses) {
Nucleus input = synapse.nucleus;
AddToLayer(currentLayer, input);
// Debug.Log($"layer {layerIx} nucleus {input.name}");
}
if (currentLayer.neuroids.Count > 0) {
this.layers.Add(currentLayer);
}
}
private void AddToLayer(NeuroidLayer layer, Nucleus nucleus) {
if (nucleus == null)
return;
layer.neuroids.Add(nucleus);
nucleus.layerIx = layer.ix;
// Store its position
Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1);
neuroidPositions[nucleus] = neuroidPosition;
}
// basic pan/zoom handling
// void OnWheel(WheelEvent e) {
// if (e.ctrlKey) {
// float delta = -e.delta.y * 0.001f;
// zoom = Mathf.Clamp(zoom + delta, 0.25f, 2f);
// content.transform.rotation = Quaternion.identity; // keep transform accessible
// content.transform.scale = new Vector3(zoom, zoom, 1);
// e.StopPropagation();
// }
// else {
// pan += e.delta;
// content.style.left = pan.x;
// content.style.top = pan.y;
// }
// }
void OnMouseDown(MouseDownEvent e) {
if (e.button == 2) { draggingCanvas = true; lastMouse = e.mousePosition; e.StopPropagation(); }
}
void OnMouseMove(MouseMoveEvent e) {
if (draggingCanvas) {
var delta = e.mousePosition - lastMouse;
pan += delta;
//content.style.left = pan.x;
//content.style.top = pan.y;
lastMouse = e.mousePosition;
}
}
void OnMouseUp(MouseUpEvent e) { if (e.button == 2) draggingCanvas = false; }
void OnIMGUI() {
if (currentNucleus == null)
return;
serializedBrain.Update();
Handles.BeginGUI();
foreach (NeuroidLayer layer in layers)
DrawLayer(layer);
Handles.EndGUI();
}
private void DrawLayer(NeuroidLayer layer) {
int nodeCount = layer.neuroids.Count;
float maxValue = 0;
foreach (Nucleus nucleus in layer.neuroids) {
if (nucleus is Neuroid neuroid) {
float value = neuroid.outputValue.magnitude;
if (value > maxValue)
maxValue = value;
}
}
float spacing = 400f / nodeCount;
float margin = 10 + spacing / 2;
foreach (Nucleus layerNucleus in layer.neuroids) {
Vector2Int layerNeuroidPos = this.neuroidPositions[layerNucleus];
Vector3 parentPos = new(100 + layerNeuroidPos.x * 100, margin + layerNeuroidPos.y * spacing, 0.1f);
//int i = 0;
float inputSpacing = 400f / layerNucleus.synapses.Count;
float inputMargin = 10 + inputSpacing / 2;
// int minStale = 10000;
//foreach ((Nucleus nucleus, float weight) in layerNucleus.synapses) {
foreach (Synapse synapse in layerNucleus.synapses) {
Nucleus nucleus = synapse.nucleus;
if (nucleus != null) {
float weight = synapse.weight;
if (this.neuroidPositions.ContainsKey(nucleus)) {
Vector2Int inputNeuroidPos = this.neuroidPositions[nucleus];
if (inputNeuroidPos.x == layerNeuroidPos.x + 1) {
Vector3 pos = new(100 + inputNeuroidPos.x * 100, inputMargin + inputNeuroidPos.y * inputSpacing, 0.0f);
float brightness = weight / 10.0f;
Handles.color = new Color(brightness, brightness, brightness);
Handles.DrawLine(parentPos, pos);
}
}
// if (nucleus is Neuroid neuroid && neuroid.stale < minStale)
// minStale = neuroid.stale;
}
}
// if (layerNucleus.synapses.Count > 0 && minStale > 2 && layerNucleus.stale < 3)
// Debug.LogWarning($"Strange {minStale} is big duing update");
float size = 20;
if (layerNucleus.isSleeping)
Handles.color = Color.darkRed;
else {
float brightness = layerNucleus.outputValue.magnitude / maxValue;
Handles.color = new Color(brightness, brightness, brightness);
}
Handles.DrawSolidDisc(parentPos, Vector3.forward, size);
Vector3 labelPos = parentPos - Vector3.down * (size + 0.2f); // below disc along up axis
GUIStyle style = new GUIStyle(EditorStyles.label) {
alignment = TextAnchor.UpperCenter,
normal = { textColor = Color.white },
fontStyle = FontStyle.Bold
};
Handles.Label(labelPos, layerNucleus.name, style);
Rect neuronRect = new(parentPos.x - size, parentPos.y - size, size * 2, size * 2);
int id = GUIUtility.GetControlID(FocusType.Passive);
Event e = Event.current;
EventType et = e.GetTypeForControl(id);
if (e != null && neuronRect.Contains(e.mousePosition)) {
// Process Hover
HandleMouseHover(layerNucleus, neuronRect);
// Process click
// Debug.Log($"{et} {e.type}");
if (e.type == EventType.MouseDown && e.button == 0) {
// Consume the event so the scene doesn't also handle it
e.Use();
HandleDiscClicked(layerNucleus);
}
}
}
}
private void HandleMouseHover(Nucleus neuroid, Rect rect) {
GUIContent tooltip;
// if (neuroid is SensoryNeuroid sensoryNeuroid) {
// tooltip = new(
// $"{sensoryNeuroid.name}" +
// $"\nThing {sensoryNeuroid.receptor.thingType}" +
// $"\nValue: {neuroid.outputValue}");
// }
// else {
tooltip = new(
$"{neuroid.name}" +
$"\nsynapse count {neuroid.synapses.Count}" +
$"\nValue: {neuroid.outputValue}");
// }
Vector2 mousePosition = Event.current.mousePosition;
// Display tooltip with some offset
Vector2 tooltipSize = GUI.skin.box.CalcSize(tooltip);
Rect tooltipRect = new Rect(mousePosition.x + 10, mousePosition.y + 10, tooltipSize.x, tooltipSize.y);
GUI.Box(tooltipRect, tooltip);
}
private void HandleDiscClicked(Nucleus nucleus) {
this.currentNucleus = nucleus;
BuildLayers();
}
void DrawInspector() {
if (NanoBrainEditor.inspectorContainer == null)
return;
NanoBrainEditor.inspectorContainer.Clear();
if (this.currentNucleus == null)
return;
// create a SerializedObject wrapper so Unity inspector controls work (and Undo)
SerializedObject so = new SerializedObject(currentWrapper);
IMGUIContainer container = new IMGUIContainer(() => {
so.Update();
currentNucleus.name = EditorGUILayout.TextField(currentNucleus.name);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Output Value", GUILayout.Width(100));
EditorGUILayout.Vector3Field(GUIContent.none, currentNucleus.outputValue);
EditorGUILayout.EndHorizontal();
if (currentNucleus.synapses.Count > 0) {
EditorGUILayout.LabelField("Synapses");
EditorGUI.indentLevel++;
//List<Nucleus> nuclei = currentNucleus.synapses.Keys.ToList();
// foreach (Nucleus nucleus in nuclei) {
foreach (Synapse synapse in currentNucleus.synapses) {
EditorGUI.BeginDisabledGroup(synapse.nucleus.isSleeping);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(synapse.nucleus.name, GUILayout.Width(120));
EditorGUI.indentLevel--;
EditorGUILayout.LabelField("Weight", GUILayout.Width(45));
// float weight = currentNucleus.synapses[nucleus];
// currentNucleus.synapses[nucleus] = EditorGUILayout.FloatField(weight, GUILayout.Width(40));
synapse.weight = EditorGUILayout.FloatField(synapse.weight, GUILayout.Width(40));
EditorGUI.indentLevel++;
EditorGUILayout.Vector3Field(GUIContent.none, synapse.nucleus.outputValue, GUILayout.Width(180));
EditorGUILayout.EndHorizontal();
EditorGUI.EndDisabledGroup();
}
EditorGUI.indentLevel--;
}
if (GUILayout.Button("Add Neuron"))
AddInputNeuron(currentNucleus);
});
NanoBrainEditor.inspectorContainer.Add(container);
}
protected virtual void AddInputNeuron(Nucleus receiver) {
Neuroid newNeuroid = new(brain, "New neuron");
newNeuroid.AddReceiver(receiver);
Rebuild();
}
private Vector3 NodePosition(Nucleus nucleus, int layerNodeCount = 1) {
if (this.neuroidPositions.ContainsKey(nucleus)) {
Vector2Int nucleusPos = this.neuroidPositions[nucleus];
return NodePosition(nucleusPos, layerNodeCount);
}
else {
return Vector3.zero;
}
}
private Vector3 NodePosition(Vector2Int location, int layerNodeCount = 1) {
float spacing = 400f / layerNodeCount;
float margin = 10 + spacing / 2;
float size = 20;
Vector3 parentPos = new(100 + location.x * 100 - size, margin + location.y * spacing - size, 0.1f);
return parentPos;
}
// public void CreateEdge(string fromId, string toId) {
// if (fromId == toId) return;
// Undo.RecordObject(graph, "Create Edge");
// graph.edges.Add(new GraphEdge { fromNodeId = fromId, toNodeId = toId });
// EditorUtility.SetDirty(graph);
// Rebuild();
// }
}
public class NodeView : VisualElement {
Nucleus data;
GraphBoardView board;
Label titleLabel;
//bool dragging = false;
Vector2 localDragStart;
public NodeView(Nucleus node, GraphBoardView boardView) {
data = node;
board = boardView;
name = "node";
style.width = 20; //node.size.x;
style.height = 20; //node.size.y;
titleLabel = new Label(node.name) { name = "title" };
Add(titleLabel);
// ports
// var outPort = new Button(() => StartEdgeDrag(true)) { text = "◀", name = "out" };
// var inPort = new Button(() => StartEdgeDrag(false)) { text = "▶", name = "in" };
// Add(outPort);
// Add(inPort);
RegisterCallback<MouseDownEvent>(OnMouseDown);
// RegisterCallback<MouseMoveEvent>(OnMouseMove);
RegisterCallback<MouseUpEvent>(OnMouseUp);
//RegisterCallback<MouseLeaveEvent>(e => dragging = false);
}
// void StartEdgeDrag(bool isOutput) {
// // simplified: on first click store source; on second click on target port call board.CreateEdge
// if (EdgeDragState.active == null) EdgeDragState.active = new EdgeDragState { fromNode = data, fromIsOutput = isOutput };
// else {
// var src = EdgeDragState.active.fromNode;
// if (src != null && src.id != data.id) board.CreateEdge(src.id, data.id);
// EdgeDragState.active = null;
// }
// }
void OnMouseDown(MouseDownEvent e) {
if (e.button == 0 && e.target == this) {
//dragging = true;
localDragStart = e.mousePosition;
e.StopPropagation();
}
}
// void OnMouseMove(MouseMoveEvent e) {
// if (!dragging) return;
// var delta = e.mousePosition - localDragStart;
// var worldPos = new Vector2(layout.x + delta.x, layout.y + delta.y);
// style.left = worldPos.x;
// style.top = worldPos.y;
// // commit on every move
// board.UpdateNodePosition(data, worldPos);
// }
void OnMouseUp(MouseUpEvent e) {
//dragging = false;
}
}
public class GraphNodeWrapper : ScriptableObject {
// expose fields that map to GraphNode
public string title;
public Vector2 position;
Nucleus node;
NanoBrain graph; // needed to write back and mark dirty
public GraphNodeWrapper Init(Nucleus node, NanoBrain graphAsset) {
this.node = node;
this.graph = graphAsset;
this.title = " A " + node.name;
//position = node.position;
return this;
}
void OnValidate() {
if (node != null) {
node.name = title;
//node.position = position;
#if UNITY_EDITOR
if (graph != null)
UnityEditor.EditorUtility.SetDirty(graph);
#endif
}
}
}
//static class EdgeDragState { public static EdgeDragState active; public GraphNode fromNode; public bool fromIsOutput; }
public static class OpenAssetHandler {
// Called when an asset is double-clicked or opened.
[OnOpenAsset]
public static bool OpenMyScriptableObject(int instanceID, int line) {
NanoBrain obj = EditorUtility.EntityIdToObject(instanceID) as NanoBrain;
if (obj != null) {
NanoBrainEditor.Open(obj);
return true; // handled
}
return false; // let Unity open normally
}
}
*/

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c57f78e25f0e55b96a50fd5592b26317

View File

@ -0,0 +1,645 @@
/*
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
[CustomEditor(typeof(NanoBrain))]
public class NanoBrainInspector : Editor {
protected static VisualElement mainContainer;
protected static VisualElement inspectorContainer;
protected bool breakOnWake = false;
#region Start
public override VisualElement CreateInspectorGUI() {
NanoBrain brain = target as NanoBrain;
serializedObject.Update();
VisualElement root = new();
//root.style.flexDirection = FlexDirection.Row; // side-by-side layout
//root.style.flexGrow = 1;
//root.style.minHeight = 600;
root.style.paddingLeft = 0;
root.style.paddingRight = 0;
root.style.paddingTop = 0;
root.style.paddingBottom = 0;
root.styleSheets.Add(Resources.Load<StyleSheet>("GraphStyles"));
mainContainer = new() {
// name = "main",
style = {
// flexDirection = FlexDirection.Row,
// flexGrow = 1,
height = 450,
}
};
GraphView graph = new();
graph.style.flexGrow = 1;
inspectorContainer = new VisualElement {
// name = "inspector"
};
mainContainer.Add(graph);
mainContainer.Add(inspectorContainer);
root.Add(mainContainer);
// Run once for initial state (use resolved style width if available)
float initialWidth = root.layout.width > 0 ? root.layout.width : root.contentRect.width;
UpdateLayout(initialWidth);
// React to size changes of root (or parent if appropriate)
root.RegisterCallback<GeometryChangedEvent>(evt => {
UpdateLayout(evt.newRect.width);
});
if (brain != null)
graph.SetGraph(null, brain, brain.output, inspectorContainer);
else
Debug.LogWarning(" No brain!");
serializedObject.ApplyModifiedProperties();
return root;
}
public class GraphView : VisualElement {
NanoBrain brain;
SerializedObject serializedBrain;
INucleus currentNucleus;
GameObject gameObject;
private List<NeuroidLayer> layers = new();
private readonly Dictionary<IReceptor, Vector2Int> neuroidPositions = new();
Vector2 pan = Vector2.zero;
//float zoom = 1f;
bool draggingCanvas = false;
Vector2 lastMouse;
GraphNodeWrapper currentWrapper;
public GraphView() {
name = "content";
style.flexGrow = 1;
IMGUIContainer imguiContainer = new(OnIMGUI);
imguiContainer.style.position = Position.Absolute;
imguiContainer.style.left = 0; imguiContainer.style.top = 0;
imguiContainer.style.right = 0; imguiContainer.style.bottom = 0;
imguiContainer.pickingMode = PickingMode.Position;
imguiContainer.focusable = true;
Add(imguiContainer);
//RegisterCallback<WheelEvent>(OnWheel);
RegisterCallback<MouseDownEvent>(OnMouseDown);
RegisterCallback<MouseMoveEvent>(OnMouseMove);
RegisterCallback<MouseUpEvent>(OnMouseUp);
}
public void SetGraph(GameObject gameObject, NanoBrain brain, Nucleus nucleus, VisualElement inspectorContainer) {
this.gameObject = gameObject;
this.brain = brain;
if (Application.isPlaying == false)
this.serializedBrain = new SerializedObject(brain);
this.currentNucleus = nucleus;
Rebuild(inspectorContainer);
}
void Rebuild(VisualElement inspectorContainer) {
BuildLayers();
if (this.currentNucleus == null) {
inspectorContainer.Clear();
return;
}
if (currentWrapper != null)
DestroyImmediate(currentWrapper);
currentWrapper = CreateInstance<GraphNodeWrapper>().Init(this.currentNucleus, brain);
DrawInspector(inspectorContainer);
}
private void BuildLayers() {
// A temporary list to track what's been added to layers
this.layers = new();
int layerIx = 0;
INucleus selectedNucleus = this.currentNucleus;
if (selectedNucleus == null)
return;
NeuroidLayer currentLayer = new() { ix = layerIx };
if (selectedNucleus.receivers != null) {
foreach (INucleus receiver in selectedNucleus.receivers) {
INucleus outputNeuroid = receiver;
if (outputNeuroid != null) {
AddToLayer(currentLayer, outputNeuroid);
// Debug.Log($"layer {layerIx} nucleus {outputNeuroid.name}");
}
}
}
if (currentLayer.neuroids.Count > 0) {
this.layers.Add(currentLayer);
layerIx++;
currentLayer = new() { ix = layerIx };
}
AddToLayer(currentLayer, selectedNucleus);
this.layers.Add(currentLayer);
// Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}");
layerIx++;
currentLayer = new() { ix = layerIx };
if (selectedNucleus.synapses != null) {
foreach (Synapse synapse in selectedNucleus.synapses) {
IReceptor input = synapse.nucleus;
AddToLayer(currentLayer, input);
// Debug.Log($"layer {layerIx} nucleus {input.name}");
}
}
if (currentLayer.neuroids.Count > 0) {
this.layers.Add(currentLayer);
}
}
private void AddToLayer(NeuroidLayer layer, IReceptor nucleus) {
if (nucleus == null)
return;
layer.neuroids.Add(nucleus);
//nucleus.layerIx = layer.ix;
// Store its position
Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1);
neuroidPositions[nucleus] = neuroidPosition;
}
void OnMouseDown(MouseDownEvent e) {
if (e.button == 2) { draggingCanvas = true; lastMouse = e.mousePosition; e.StopPropagation(); }
}
void OnMouseMove(MouseMoveEvent e) {
if (draggingCanvas) {
var delta = e.mousePosition - lastMouse;
pan += delta;
//content.style.left = pan.x;
//content.style.top = pan.y;
lastMouse = e.mousePosition;
}
}
void OnMouseUp(MouseUpEvent e) { if (e.button == 2) draggingCanvas = false; }
void OnIMGUI() {
if (currentNucleus == null)
return;
if (Application.isPlaying == false)
serializedBrain.Update();
Handles.BeginGUI();
DrawGraph();
Handles.EndGUI();
}
private void DrawGraph() {
float size = 20;
Vector3 position = new(150, 210, 0);
DrawReceivers(this.currentNucleus, position, size);
DrawSynapses(this.currentNucleus, position, size);
// Draw selected Nucleus
Handles.color = Color.white;
Handles.DrawSolidDisc(position, Vector3.forward, size + 2);
DrawNucleus(this.currentNucleus, position, this.currentNucleus.outputValue.magnitude, 20);
}
private void DrawReceivers(INucleus nucleus, Vector3 parentPos, float size) {
int nodeCount = nucleus.receivers.Count;
// Determine the maximum value in this layer
// This is used to 'scale' the output value colors of the nuclei
float maxValue = 0;
foreach (INucleus receiver in nucleus.receivers) {
if (receiver is Neuroid neuroid) {
float value = neuroid.outputValue.magnitude;
if (value > maxValue)
maxValue = value;
}
}
// Determine the spacing of the nuclei in the layer
float spacing = 400f / nodeCount;
float margin = 10 + spacing / 2;
int row = 0;
foreach (INucleus receiver in nucleus.receivers) {
INucleus receiverNucleus = receiver;
if (receiverNucleus == null)
continue;
Vector3 pos = new(50, margin + row * spacing, 0.0f);
Handles.color = Color.white;
Handles.DrawLine(parentPos, pos);
DrawNucleus(receiverNucleus, pos, maxValue, size);
row++;
}
}
private void DrawSynapses(INucleus nucleus, Vector3 parentPos, float size) {
int nodeCount = nucleus.synapses.Count;
// Determine the maximum value in this layer
// This is used to 'scale' the output value colors of the nuclei
float maxValue = 0;
foreach (Synapse receiver in nucleus.synapses) {
if (receiver.nucleus is Neuroid neuroid) {
float value = neuroid.outputValue.magnitude;
if (value > maxValue)
maxValue = value;
}
}
// Determine the spacing of the nuclei in the layer
float spacing = 400f / nodeCount;
float margin = 10 + spacing / 2;
int row = 0;
List<PercepteiArray> drawnArrays = new();
foreach (Synapse synapse in nucleus.synapses) {
Vector3 pos = new(250, margin + row * spacing, 0.0f);
Handles.color = Color.white;
Handles.DrawLine(parentPos, pos);
// if (synapse.nucleus is Perceptoid perceptoid && perceptoid.array != null) {
// // if (drawnArrays.Contains(perceptoid.array))
// // // We already drawn this array
// // continue;
// drawnArrays.Add(perceptoid.array);
// DrawArray(perceptoid.array, pos, size);
// }
// else {
DrawNucleus(synapse.nucleus, pos, maxValue, size);
row++;
// }
}
}
private void DrawNucleus(IReceptor nucleus, Vector3 position, float maxValue, float size) {
if (nucleus.isSleeping)
Handles.color = Color.darkRed;
else {
if (Application.isPlaying) {
float brightness = nucleus.outputValue.magnitude / maxValue;
Handles.color = new Color(brightness, brightness, brightness, 1f);
}
else
Handles.color = Color.black;
}
Handles.DrawSolidDisc(position, Vector3.forward, size);
Handles.color = Color.white;
// Position the label in front of the disc
Vector3 labelPosition = position + (Vector3.forward * 0.1f);
GUIStyle style = new(EditorStyles.label) {
alignment = TextAnchor.MiddleCenter,
normal = { textColor = Color.white },
fontStyle = FontStyle.Bold,
};
if (nucleus is Perceptoid perceptoid) {
if (perceptoid.array == null || perceptoid.array.perceptei == null || perceptoid.array.perceptei.Length == 0)
perceptoid.array = new PercepteiArray(perceptoid);
if (perceptoid.array.perceptei.Length > 1) {
Handles.Label(labelPosition, perceptoid.array.perceptei.Length.ToString(), style);
}
}
style.alignment = TextAnchor.UpperCenter;
Vector3 labelPos = position - Vector3.down * (size + 0.2f); // below disc along up axis
Handles.Label(labelPos, nucleus.name, style);
Rect neuronRect = new(position.x - size, position.y - size, size * 2, size * 2);
int id = GUIUtility.GetControlID(FocusType.Passive);
Event e = Event.current;
EventType et = e.GetTypeForControl(id);
if (e != null && neuronRect.Contains(e.mousePosition)) {
// Process Hover
HandleMouseHover(nucleus, neuronRect);
// Process click
if (e.type == EventType.MouseDown && e.button == 0) {
// Consume the event so the scene doesn't also handle it
e.Use();
HandleClicked(nucleus);
}
}
}
private void DrawArray(PercepteiArray array, Vector3 position, float size) {
Vector3 offset = new(size / 4, size / 4, 0);
Handles.color = Color.black;
Handles.DrawSolidDisc(position, Vector3.forward, size);
GUIStyle style = new(EditorStyles.label) {
alignment = TextAnchor.UpperCenter,
normal = { textColor = Color.white },
fontStyle = FontStyle.Bold
};
Handles.Label(position, array.perceptei.Length.ToString(), style);
Vector3 labelPos = position - Vector3.down * (size + 0.2f); // below disc along up axis
Handles.Label(labelPos, array.name, style);
// To do: add HandleClick (see above) to expand the array
}
private void HandleMouseHover(IReceptor nucleus, Rect rect) {
GUIContent tooltip;
if (nucleus is Perceptoid perceptoid) {
if (perceptoid.receptor != null) {
tooltip = new(
$"{perceptoid.name}" +
// $"\nType {perceptoid.receptor.thingType}" +
$" Thing {perceptoid.thingId}" +
$"\nValue: {nucleus.outputValue}");
}
else {
tooltip = new(
$"{perceptoid.name}" +
$"\nThing {perceptoid.thingId}" +
$"\nValue: {nucleus.outputValue}");
}
}
else if (nucleus is INucleus n) {
tooltip = new(
$"{nucleus.name}" +
$"\nsynapse count {n.synapses.Count}" +
$"\nValue: {nucleus.outputValue}");
}
else {
tooltip = new(
$"{nucleus.name}" +
$"\nValue: {nucleus.outputValue}");
}
Vector2 mousePosition = Event.current.mousePosition;
// Display tooltip with some offset
Vector2 tooltipSize = GUI.skin.box.CalcSize(tooltip);
Rect tooltipRect = new Rect(mousePosition.x + 10, mousePosition.y + 10, tooltipSize.x, tooltipSize.y);
GUI.Box(tooltipRect, tooltip);
}
private void HandleClicked(IReceptor nucleus) {
if (nucleus is INucleus n) {
this.currentNucleus = n;
BuildLayers();
}
}
void DrawInspector(VisualElement inspectorContainer) {
if (inspectorContainer == null)
return;
inspectorContainer.Clear();
if (this.currentNucleus == null)
return;
// create a SerializedObject wrapper so Unity inspector controls work (and Undo)
SerializedObject so = new(currentWrapper);
IMGUIContainer container = new(() => {
if (so.targetObject == null)
return;
so.Update();
if (this.currentNucleus == null)
return;
this.currentNucleus.name = EditorGUILayout.TextField(this.currentNucleus.name);
if (this.currentNucleus is Perceptoid perceptoid) {
// perceptoid.receptor.thingType = EditorGUILayout.IntField("Thing Type", perceptoid.receptor.thingType);
if (perceptoid.array == null || perceptoid.array.perceptei == null || perceptoid.array.perceptei.Length == 0)
perceptoid.array = new PercepteiArray(perceptoid);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.IntField("Array size", perceptoid.array.perceptei.Length);
if (GUILayout.Button("Add"))
perceptoid.array.AddPerceptoid();
if (GUILayout.Button("Del"))
perceptoid.array.RemovePerceptoid();
EditorGUILayout.EndHorizontal();
}
else if (this.currentNucleus is Neuroid neuroid) {
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Activation Curve", GUILayout.Width(150));
if (neuroid.curveMax > 0)
EditorGUILayout.CurveField(neuroid.curve, Color.cyan, new Rect(0, 0, 1, neuroid.curveMax));
else
EditorGUILayout.CurveField(neuroid.curve, Color.cyan, new Rect(0, neuroid.curveMax, 1, -neuroid.curveMax));
neuroid.curvePreset = (Neuroid.CurvePresets)EditorGUILayout.EnumPopup(neuroid.curvePreset, GUILayout.Width(100));
EditorGUILayout.EndHorizontal();
}
if (Application.isPlaying)
EditorGUILayout.FloatField("Output", this.currentNucleus.outputValue.magnitude);
else
EditorGUILayout.LabelField(" ");
if (this.currentNucleus.synapses.Count > 0) {
Synapse[] synapses = this.currentNucleus.synapses.ToArray();
foreach (Synapse synapse in synapses) {
if (synapse.nucleus != null) {
EditorGUILayout.Space();
EditorGUI.BeginDisabledGroup(synapse.nucleus.isSleeping);
if (Application.isPlaying)
EditorGUILayout.FloatField(synapse.nucleus.name, synapse.nucleus.outputValue.magnitude * synapse.weight);
else {
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField(synapse.nucleus.name);
// if (synapse.nucleus is Perceptoid perceptoid) {
// if (perceptoid.array == null || perceptoid.array.perceptei == null || perceptoid.array.perceptei.Length == 0) {
// perceptoid.array = new PercepteiArray(perceptoid);
// }
// EditorGUILayout.IntField(perceptoid.array.perceptei.Length);
// if (GUILayout.Button("Add"))
// perceptoid.array.AddPerceptoid();
// }
if (GUILayout.Button("Disconnect"))
synapse.nucleus.RemoveReceiver(this.currentNucleus);
EditorGUILayout.EndHorizontal();
}
EditorGUI.indentLevel++;
synapse.weight = EditorGUILayout.FloatField("Weight", synapse.weight);
EditorGUI.indentLevel--;
EditorGUI.EndDisabledGroup();
}
}
}
EditorGUILayout.Space();
ConnectNucleus(this.currentNucleus);
if (GUILayout.Button("Add Input Neuron"))
AddInputNeuron(this.currentNucleus);
if (GUILayout.Button("Add Input Perceptoid"))
AddPerceptoid(this.currentNucleus);
if (GUILayout.Button("Add Input Cluster"))
AddCluster(this.currentNucleus);
EditorGUILayout.Space();
if (GUILayout.Button("Delete this neuron"))
DeleteNeuron(this.currentNucleus);
//DisconnectNucleus(this.currentNucleus);
if (this.gameObject != null) {
Vector3 worldVector = this.gameObject.transform.TransformVector(this.currentNucleus.outputValue);
Debug.DrawRay(this.gameObject.transform.position, worldVector, Color.yellow);
}
});
inspectorContainer.Add(container);
}
protected virtual void AddInputNeuron(INucleus nucleus) {
Neuroid newNeuroid = new(this.brain.cluster, "New neuron");
newNeuroid.AddReceiver(nucleus);
this.currentNucleus = newNeuroid;
BuildLayers();
}
protected virtual void DeleteNeuron(INucleus nucleus) {
if (nucleus == null)
return;
if (nucleus.cluster != null)
this.currentNucleus = nucleus.cluster.output;
foreach (INucleus receiver in nucleus.receivers) {
if (receiver != null) {
this.currentNucleus = receiver;
break;
}
}
Nucleus.Delete(nucleus);
BuildLayers();
}
protected virtual void AddPerceptoid(INucleus nucleus) {
Perceptoid newPerceptoid = new(this.brain, 0, "New Perceptoid");
newPerceptoid.AddReceiver(nucleus);
this.currentNucleus = newPerceptoid;
BuildLayers();
}
protected virtual void AddCluster(INucleus nucleus) {
BrainPickerWindow.ShowPicker(brain => OnClusterPicked(nucleus, brain), "Select Cluster");
}
private void OnClusterPicked(INucleus nucleus, NanoBrain brain) {
NanoBrain brainInstance = Instantiate(brain);
brainInstance.AddReceiver(nucleus);
}
protected virtual void ConnectNucleus(INucleus nucleus) {
if (this.currentNucleus.cluster == null)
return;
IEnumerable<string> synapseNuclei = this.currentNucleus.synapses.Select(synapse => synapse.nucleus.name);
//IEnumerable<string> perceptei = this.currentNucleus.brain.perceptei.Select(i => i.name).Except(synapseNuclei);
IEnumerable<string> nuclei = this.currentNucleus.cluster.nuclei.Select(i => i.name).Except(synapseNuclei);
//string[] names = perceptei.Concat(nuclei).ToArray();
string[] names = nuclei.ToArray();
int selectedIndex = -1;
selectedIndex = EditorGUILayout.Popup("Connect to", selectedIndex, names);
if (selectedIndex >= 0) {
// if (selectedIndex < perceptei.Count()) {
// Nucleus n = this.currentNucleus.brain.perceptei[selectedIndex];
// n.AddReceiver(this.currentNucleus);
// }
// else {
// Nucleus n = this.currentNucleus.brain.nuclei[selectedIndex - perceptei.Count()];
// n.AddReceiver(this.currentNucleus);
// }
INucleus n = this.currentNucleus.cluster.nuclei[selectedIndex];
n.AddReceiver(this.currentNucleus);
}
}
protected virtual void DisconnectNucleus(Nucleus nucleus) {
if (this.currentNucleus.cluster == null)
return;
string[] names = this.currentNucleus.synapses.Select(synapse => synapse.nucleus.name).ToArray();
int selectedIndex = -1;
selectedIndex = EditorGUILayout.Popup("Disconnect from", selectedIndex, names);
//if (selectedIndex >= 0 && selectedIndex < this.currentNucleus.brain.perceptei.Count) {
if (selectedIndex >= 0 && selectedIndex < this.currentNucleus.cluster.nuclei.Count) {
Synapse synapse = this.currentNucleus.synapses[selectedIndex];
synapse.nucleus.RemoveReceiver(this.currentNucleus);
}
}
}
#endregion Start
#region Update
private void UpdateLayout(float containerWidth) {
if (containerWidth > 600f) {
mainContainer.style.flexDirection = FlexDirection.Row;
inspectorContainer.style.width = 300; // fixed sidebar width
inspectorContainer.style.flexGrow = 0;
}
else {
mainContainer.style.flexDirection = FlexDirection.Column;
inspectorContainer.style.width = Length.Percent(100); // full width below
inspectorContainer.style.flexDirection = FlexDirection.Column;
inspectorContainer.style.flexGrow = 1; // can set 0 or keep as needed
}
}
#endregion Update
}
/*
public class NeuroidLayer {
public int ix = 0;
public List<INucleus> neuroids = new();
}
*/
/*
public class GraphNodeWrapper : ScriptableObject {
// expose fields that map to GraphNode
//public string title;
public Vector2 position;
INucleus node;
NanoBrain graph; // needed to write back and mark dirty
public GraphNodeWrapper Init(INucleus node, NanoBrain graphAsset) {
this.node = node;
this.graph = graphAsset;
//this.title = " A " + node.name;
//position = node.position;
return this;
}
void OnValidate() {
if (node != null) {
//node.name = title;
//node.position = position;
#if UNITY_EDITOR
if (graph != null)
UnityEditor.EditorUtility.SetDirty(graph);
#endif
}
}
}
*/

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c96ad47c3d4498640b52630789e38573

102
VisualEditor/NanoBrain.cs Normal file
View File

@ -0,0 +1,102 @@
/*
using System;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(menuName = "Passer/NanoBrain")]
public class NanoBrain : ScriptableObject, ISerializationCallbackReceiver {
public List<Neuroid> nuclei = new();
public List<Perceptoid> perceptei = new();
public List<Receptor> receptors = new();
// This is probably always the first element in the nuclei list...
[System.NonSerialized]
public Nucleus output;
public int rootId;
public NanoBrain() {
// this.cluster = new();
// this.output = new Neuroid(this.cluster, "Root");
}
public Cluster cluster;
public void AddReceiver(INucleus receiver) {
output.AddReceiver(receiver);
}
public Neuroid AddNeuron(string name) {
Neuroid neuroid = new(this.cluster, name);
return neuroid;
}
public void UpdateNuclei() {
foreach (Nucleus nucleus in nuclei)
nucleus.IncreaseAge();
foreach (Perceptoid perception in perceptei)
perception.IncreaseAge();
}
public void OnBeforeSerialize() {
if (output != null) {
this.rootId = output.id;
}
}
public void OnAfterDeserialize() {
try {
foreach (Nucleus nucleus in this.nuclei.ToArray()) {
if (this.rootId == nucleus.id)
this.output = nucleus;
nucleus.Rebuild(this);
}
foreach (Perceptoid perceptoid in this.perceptei.ToArray())
perceptoid.Rebuild(this);
}
catch (System.Exception) { }
if (this.cluster != null)
this.cluster.GarbageCollection();
}
public void GarbageCollection() {
HashSet<INucleus> visitedNuclei = new();
MarkNuclei(visitedNuclei, this.output);
//Debug.Log($"Garbage collection found {visitedNuclei.Count} Nuclei");
this.nuclei.RemoveAll(nucleus => visitedNuclei.Contains(nucleus) == false);
this.perceptei.RemoveAll(perceptoid => visitedNuclei.Contains(perceptoid) == false);
}
public void MarkNuclei(HashSet<INucleus> visitedNuclei, INucleus nucleus) {
if (nucleus is null)
return;
if (nucleus.brain == null)
nucleus.brain = this;
visitedNuclei.Add(nucleus);
if (nucleus.synapses != null) {
HashSet<Synapse> visitedSynapses = new();
foreach (Synapse synapse in nucleus.synapses) {
if (synapse != null && synapse.nucleus != null) {
visitedSynapses.Add(synapse);
MarkNuclei(visitedNuclei, synapse.nucleus);
}
}
nucleus.synapses.RemoveAll(synapse => visitedSynapses.Contains(synapse) == false);
}
if (nucleus.receivers != null) {
HashSet<Receiver> visitedReceivers = new();
foreach (Receiver receiver in nucleus.receivers) {
if (receiver != null && receiver.nucleus != null) {
visitedReceivers.Add(receiver);
visitedNuclei.Add(receiver.nucleus);
}
}
nucleus.receivers.RemoveAll(receiver => visitedReceivers.Contains(receiver) == false);
}
}
}
*/

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 36081359186edfec998d891a1feeb17b

View File

@ -1,7 +1,7 @@
using System;
using UnityEngine;
public class NanoBrain : MonoBehaviour {
public class NanoBrainComponent : MonoBehaviour {
public ClusterPrefab defaultBrain;
[NonSerialized]
@ -13,12 +13,19 @@ public class NanoBrain : MonoBehaviour {
name = defaultBrain.name + " (Instance)"
};
}
SwarmControl sc = FindFirstObjectByType<SwarmControl>();
if (sc != null) {
UpdateWeight(brainInstance, "Containment", sc.containmentForce);
UpdateWeight(brainInstance, "Cohesion", sc.cohesionForce);
UpdateWeight(brainInstance, "Separation", sc.separationForce);
UpdateWeight(brainInstance, "Alignment", sc.alignmentForce);
}
return brainInstance;
}
}
public static void UpdateWeight(Cluster brain, string name, float weight) {
Nucleus root = brain.defaultOutput;
INucleus root = brain.output;
foreach (Synapse synapse in root.synapses) {
if (synapse.nucleus.name == name) {
if (synapse.weight != weight) {
@ -28,4 +35,5 @@ public class NanoBrain : MonoBehaviour {
}
}
}
}