Compare commits
61 Commits
37261bdce6
...
133804a154
| Author | SHA1 | Date | |
|---|---|---|---|
| 133804a154 | |||
| f8a6f579ea | |||
| 923a5fafe3 | |||
| 94df545222 | |||
| 62bacfb1c0 | |||
| ffa1581f54 | |||
| 5584123c20 | |||
| 57967e3409 | |||
| 92bfb29ed0 | |||
| a87118fc23 | |||
| 76023718cb | |||
| 1918faf280 | |||
| 40f8da92ef | |||
| e23389d072 | |||
| fa8cd1c728 | |||
| 35a8c07e8a | |||
| 26ef665634 | |||
| 4363079d43 | |||
| 6c30009564 | |||
| 09a75aa22e | |||
| b1121eed87 | |||
| 67beb5e486 | |||
| 7fb23e9b89 | |||
| 10d99bce87 | |||
| fdae20b6b4 | |||
| 3f0ed444bb | |||
| d2ef26b6f8 | |||
| 79376c30ea | |||
| ec2a6b7ae9 | |||
| dd326823a8 | |||
| 36c414ae02 | |||
| 9eda1cdf06 | |||
| 4cb6286f20 | |||
| 4ae9a15fc6 | |||
| 983b1e1c40 | |||
| ae035d4672 | |||
| 7c1061ade5 | |||
| b71115d823 | |||
| 38391181af | |||
| 742fc3f323 | |||
| 0a66372c3f | |||
| 08a3a7da56 | |||
| 0e31a37068 | |||
| f2884327cf | |||
| 7ced91909a | |||
| f0bb2a9fa4 | |||
| dce7835dfc | |||
| 6fab0a35cf | |||
| 05fd588f9b | |||
| f65dcf803b | |||
| c5808c5fb9 | |||
| 3f8716794a | |||
| c9ad9f22a2 | |||
| 334a4418c0 | |||
| 8e441f86ff | |||
| 611055cdcd | |||
| 3b842e7751 | |||
| 9fcbaa5bf8 | |||
| 3c6f2c1f66 | |||
| 8e87e4ea77 | |||
| 628f20b103 |
@ -1,75 +0,0 @@
|
||||
/*
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace NanoBrain.Unity {
|
||||
|
||||
[CustomEditor(typeof(Brain))]
|
||||
public class Brain_Editor : Editor {
|
||||
protected static VisualElement mainContainer;
|
||||
protected static VisualElement inspectorContainer;
|
||||
|
||||
public Brain component;
|
||||
private SerializedProperty brainProp;
|
||||
|
||||
public void OnEnable() {
|
||||
component = target as Brain;
|
||||
|
||||
if (Application.isPlaying == false && serializedObject != null) {
|
||||
string propertyName = nameof(Brain.brainPrefab);
|
||||
brainProp = serializedObject.FindProperty(propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
public override VisualElement CreateInspectorGUI() {
|
||||
|
||||
if (Application.isPlaying == false)
|
||||
serializedObject.Update();
|
||||
|
||||
|
||||
VisualElement root = new() {
|
||||
style = {
|
||||
paddingLeft = 0,
|
||||
paddingRight = 0,
|
||||
paddingTop = 0,
|
||||
paddingBottom = 0
|
||||
}
|
||||
};
|
||||
root.styleSheets.Add(Resources.Load<StyleSheet>("GraphStyles"));
|
||||
|
||||
PropertyField brainField = new(brainProp) {
|
||||
label = "Cluster Prefab"
|
||||
};
|
||||
root.Add(brainField);
|
||||
|
||||
CreateViewer(root, component.brain, component.gameObject);
|
||||
|
||||
if (Application.isPlaying == false)
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
return root;
|
||||
}
|
||||
|
||||
public ClusterViewer.GraphView CreateViewer(VisualElement root, Cluster cluster, GameObject gameObject) {
|
||||
VisualElement mainContainer = new() {
|
||||
style = {
|
||||
flexDirection = FlexDirection.Row,
|
||||
minHeight = 450
|
||||
}
|
||||
};
|
||||
ClusterViewer.GraphView graph = new(cluster);
|
||||
graph.style.flexGrow = 1;
|
||||
|
||||
mainContainer.Add(graph);
|
||||
root.Add(mainContainer);
|
||||
|
||||
graph.SetGraph(gameObject);
|
||||
|
||||
return graph;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
*/
|
||||
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f05072314d39990639a2dbf99f322664
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -76,8 +76,15 @@ namespace NanoBrain.Unity {
|
||||
margin = new RectOffset(10, 0, 4, 4)
|
||||
};
|
||||
// Nucleus type
|
||||
string nucleusType = this.view.currentNucleus.GetType().Name;
|
||||
GUILayout.Label(nucleusType, headerStyle);
|
||||
System.Type nucleusType = this.view.currentNucleus.GetType();
|
||||
if (nucleusType == typeof(Cluster)) {
|
||||
Cluster cluster = this.view.currentNucleus as Cluster;
|
||||
GUILayout.Label($"{cluster.prefab.name} {nucleusType.Name}", headerStyle);
|
||||
}
|
||||
else
|
||||
GUILayout.Label(nucleusType.Name, headerStyle);
|
||||
// string nucleusType = this.view.currentNucleus.GetType().Name;
|
||||
// GUILayout.Label(nucleusType, headerStyle);
|
||||
|
||||
// Nucleus name
|
||||
string newName = EditorGUILayout.TextField(this.view.currentNucleus.name, boldTextFieldStyle);
|
||||
@ -304,10 +311,10 @@ namespace NanoBrain.Unity {
|
||||
if (this.view.currentNucleus is not MemoryCell) {
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField("Activation Curve", GUILayout.MinWidth(60));
|
||||
if (neuron.curveMax > 0)
|
||||
EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, 0, 1, neuron.curveMax), GUILayout.Width(40));
|
||||
else
|
||||
EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, neuron.curveMax, 1, -neuron.curveMax), GUILayout.Width(40));
|
||||
// if (neuron.curveMax > 0)
|
||||
// EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, 0, 1, neuron.curveMax), GUILayout.Width(40));
|
||||
// else
|
||||
// EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, neuron.curveMax, 1, -neuron.curveMax), GUILayout.Width(40));
|
||||
Neuron.ActivationType newPreset = (Neuron.ActivationType)EditorGUILayout.EnumPopup(neuron.activator, GUILayout.MinWidth(50));
|
||||
anythingChanged |= newPreset != neuron.activator;
|
||||
neuron.activator = newPreset;
|
||||
|
||||
@ -1,129 +0,0 @@
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using UnityEditor;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
namespace NanoBrain.Unity {
|
||||
|
||||
[CustomPropertyDrawer(typeof(ClusterPrefab))]
|
||||
class ClusterPrefab_Drawer : PropertyDrawer {
|
||||
public static void Insepctor(SerializedObject serializedObject, string propertyName) {
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty(propertyName));
|
||||
}
|
||||
|
||||
const float padding = 4f;
|
||||
const float elementHeight = 64f; // height reserved for the VisualElement
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
|
||||
float height = EditorGUIUtility.singleLineHeight + padding;
|
||||
string key = property.propertyPath + "_" + property.serializedObject.targetObject.GetEntityId();
|
||||
s_foldouts.TryGetValue(key, out bool isOpen);
|
||||
if (property.objectReferenceValue != null && isOpen) {
|
||||
height += padding + elementHeight;
|
||||
height = 500;
|
||||
}
|
||||
else
|
||||
height = 36;
|
||||
return height;
|
||||
}
|
||||
|
||||
static readonly Dictionary<string, bool> s_foldouts = new();
|
||||
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
|
||||
label = EditorGUI.BeginProperty(position, label, property);
|
||||
|
||||
// Begin indent block
|
||||
int indent = EditorGUI.indentLevel;
|
||||
EditorGUI.indentLevel = 0;
|
||||
|
||||
// Draw the object field on the top line
|
||||
Rect fieldRect = new(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
|
||||
EditorGUI.PropertyField(fieldRect, property, label);
|
||||
|
||||
if (property.objectReferenceValue is ClusterPrefab prefab) {
|
||||
// key per field instance
|
||||
string key = property.propertyPath + "_" + property.serializedObject.targetObject.GetEntityId();
|
||||
if (!s_foldouts.TryGetValue(key, out bool isOpen))
|
||||
isOpen = true;
|
||||
|
||||
// foldout header rect
|
||||
Rect headerRect = new(fieldRect.x, fieldRect.yMax + 4f, fieldRect.width, EditorGUIUtility.singleLineHeight);
|
||||
isOpen = EditorGUI.Foldout(headerRect, isOpen, "Graph", true);
|
||||
s_foldouts[key] = isOpen;
|
||||
|
||||
if (isOpen) {
|
||||
// content rect below header
|
||||
Rect drawRect = new(fieldRect.x, headerRect.yMax + 2f, fieldRect.width, 450f);
|
||||
|
||||
ClusterView.Render(drawRect, prefab.cluster, property);
|
||||
//Debug.Log(prefab.cluster.defaultOutput.outputMagnitude);
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel = indent;
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
[InitializeOnLoad]
|
||||
static class ClusterPrefabInspectorRepaints {
|
||||
static ClusterPrefabInspectorRepaints() {
|
||||
EditorApplication.update += OnEditorUpdate;
|
||||
}
|
||||
|
||||
static double lastRepaint = 0;
|
||||
const double repaintInterval = 1.0 / 15.0; // up to 15 FPS in inspector
|
||||
|
||||
static void OnEditorUpdate() {
|
||||
if (!Application.isPlaying) return;
|
||||
|
||||
// throttle repaint frequency
|
||||
if (EditorApplication.timeSinceStartup - lastRepaint < repaintInterval) return;
|
||||
lastRepaint = EditorApplication.timeSinceStartup;
|
||||
|
||||
// Find all open inspectors (Editors) that target objects containing ClusterPrefab fields
|
||||
var editors = Resources.FindObjectsOfTypeAll<Editor>();
|
||||
foreach (var ed in editors) {
|
||||
var targets = ed.targets;
|
||||
if (targets == null)
|
||||
continue;
|
||||
bool shouldRepaint = targets.Any(t => ObjectHasClusterPrefabField(t));
|
||||
if (shouldRepaint) {
|
||||
try {
|
||||
ed.Repaint();
|
||||
}
|
||||
catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool ObjectHasClusterPrefabField(UnityEngine.Object obj) {
|
||||
if (obj == null)
|
||||
return false;
|
||||
Type type = obj.GetType();
|
||||
// search fields (instance, non-public/public)
|
||||
FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
foreach (FieldInfo f in fields) {
|
||||
if (f.FieldType == typeof(ClusterPrefab))
|
||||
return true;
|
||||
// also handle arrays/lists of ClusterPrefab or serializable wrappers:
|
||||
if (f.FieldType.IsArray && f.FieldType.GetElementType() == typeof(ClusterPrefab))
|
||||
return true;
|
||||
if (f.FieldType.IsGenericType) {
|
||||
Type[] gen = f.FieldType.GetGenericArguments();
|
||||
if (gen.Length == 1 && gen[0] == typeof(ClusterPrefab))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5f43b401b7d59dec7ac7d493cbc4927d
|
||||
@ -7,6 +7,13 @@ namespace NanoBrain.Unity {
|
||||
|
||||
public class ClusterView {
|
||||
|
||||
public ClusterView(string key) {
|
||||
this.key = key;
|
||||
ClusterView.clusterViews[this.key] = this;
|
||||
}
|
||||
|
||||
public static ClusterPrefab previousPrefab;
|
||||
|
||||
private static readonly float discRadius = 20;
|
||||
private float viewWidth;
|
||||
private float contentWidth = 1000;
|
||||
@ -17,17 +24,17 @@ namespace NanoBrain.Unity {
|
||||
}
|
||||
public Mode mode = Mode.Focus;
|
||||
|
||||
static readonly Dictionary<string, ClusterView> clusterViews = new();
|
||||
public static readonly Dictionary<string, ClusterView> clusterViews = new();
|
||||
public static ClusterView GetClusterView(SerializedProperty property) {
|
||||
string key = property.propertyPath + "_" + property.serializedObject.targetObject.GetEntityId();
|
||||
string key = property.propertyPath + "_" + property.serializedObject.targetObject.GetInstanceID();//GetEntityId();
|
||||
if (!clusterViews.TryGetValue(key, out ClusterView clusterView))
|
||||
clusterView = new() { key = key };
|
||||
clusterView = new(key);// { key = key };
|
||||
return clusterView;
|
||||
}
|
||||
public static ClusterView GetClusterView(SerializedObject serializedObject) {
|
||||
string key = serializedObject.targetObject.GetEntityId().ToString();
|
||||
string key = serializedObject.targetObject.GetInstanceID().ToString(); //GetEntityId().ToString();
|
||||
if (!clusterViews.TryGetValue(key, out ClusterView clusterView))
|
||||
clusterView = new() { key = key };
|
||||
clusterView = new(key); // { key = key };
|
||||
return clusterView;
|
||||
}
|
||||
|
||||
@ -37,7 +44,7 @@ namespace NanoBrain.Unity {
|
||||
|
||||
public static void Render(Rect drawRect, Cluster cluster, SerializedProperty property) {
|
||||
ClusterView clusterView = GetClusterView(property);
|
||||
if (clusterView.currentCluster == null) {
|
||||
if (clusterView.currentCluster == null || clusterView.currentCluster != cluster) {
|
||||
clusterView.currentCluster = cluster;
|
||||
clusterView.currentNucleus = cluster.defaultOutput;
|
||||
clusterView.selectedOutput = clusterView.currentNucleus;
|
||||
@ -46,7 +53,7 @@ namespace NanoBrain.Unity {
|
||||
}
|
||||
public static void Render(Rect drawRect, Cluster cluster, SerializedObject obj) {
|
||||
ClusterView clusterView = GetClusterView(obj);
|
||||
if (clusterView.currentCluster == null) {
|
||||
if (clusterView.currentCluster == null || clusterView.currentCluster != cluster) {
|
||||
clusterView.currentCluster = cluster;
|
||||
clusterView.currentNucleus = cluster.defaultOutput;
|
||||
clusterView.selectedOutput = clusterView.currentNucleus;
|
||||
@ -101,6 +108,8 @@ namespace NanoBrain.Unity {
|
||||
public Nucleus currentNucleus = null;
|
||||
public Nucleus selectedSynapseNeuron = null;
|
||||
public Nucleus selectedOutput;
|
||||
public bool isOpen = true;
|
||||
public bool initialized = false;
|
||||
|
||||
#region Focus Graph
|
||||
|
||||
@ -199,7 +208,7 @@ namespace NanoBrain.Unity {
|
||||
DrawOutputs(position);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Dag dag = GenerateGraph(this.selectedOutput);
|
||||
Dag.ComputeLayout(dag);
|
||||
|
||||
@ -267,6 +276,9 @@ namespace NanoBrain.Unity {
|
||||
|
||||
private void DescendGraph(Dag.Node receiver, ref int ix, Dag dag) {
|
||||
Neuron receiverNeuron = receiver.nucleus as Neuron;
|
||||
if (receiverNeuron == null)
|
||||
return;
|
||||
|
||||
foreach (Synapse synapse in receiverNeuron.synapses) {
|
||||
Nucleus nucleus = synapse.neuron;
|
||||
if (nucleus.parent != null && nucleus.parent != currentNucleus.parent) {
|
||||
@ -307,7 +319,7 @@ namespace NanoBrain.Unity {
|
||||
if (nucleus == this.selectedOutput) {
|
||||
// Add link to 'Outpus'
|
||||
nodeCount++;
|
||||
if (ClusterViewer.previousPrefab != null)
|
||||
if (ClusterView.previousPrefab != null && ClusterView.previousPrefab != nucleus.parent.prefab)
|
||||
// Add link to previous editor
|
||||
nodeCount++;
|
||||
}
|
||||
@ -341,9 +353,9 @@ namespace NanoBrain.Unity {
|
||||
}
|
||||
if (nucleus == this.selectedOutput) {
|
||||
Vector3 pos = new(50, margin + row * spacing, 0);
|
||||
if (ClusterViewer.previousPrefab != null) {
|
||||
if (ClusterView.previousPrefab != null && ClusterView.previousPrefab != nucleus.parent.prefab) {
|
||||
DrawEdge(parentPos, pos);
|
||||
DrawClusterPrefab(ClusterViewer.previousPrefab, pos);
|
||||
DrawClusterPrefab(ClusterView.previousPrefab, pos);
|
||||
row++;
|
||||
}
|
||||
pos = new(50, margin + row * spacing, 0);
|
||||
@ -433,7 +445,7 @@ namespace NanoBrain.Unity {
|
||||
|
||||
float maxValue = 0;
|
||||
foreach (Cluster sibling in nucleus.parent.instances) {
|
||||
Neuron siblingNeuron = sibling.GetNucleus(nucleus.name) as Neuron;
|
||||
Neuron siblingNeuron = sibling.GetNeuron(nucleus.name);
|
||||
float value = siblingNeuron.outputMagnitude; // no need to add weight as they are all the same
|
||||
if (value > maxValue)
|
||||
maxValue = value;
|
||||
@ -444,9 +456,10 @@ namespace NanoBrain.Unity {
|
||||
float margin = 10 + spacing / 2;
|
||||
|
||||
int row = 0;
|
||||
Vector3 position = Vector3.zero;
|
||||
foreach (Cluster sibling in nucleus.parent.instances) {
|
||||
Neuron siblingNeuron = sibling.GetNucleus(nucleus.name) as Neuron;
|
||||
Vector3 position = new(250, margin + row * spacing, 0.0f);
|
||||
Neuron siblingNeuron = sibling.GetNeuron(nucleus.name);
|
||||
position = new(250, margin + row * spacing, 0.0f);
|
||||
DrawEdge(parentPos, position);
|
||||
Color color = Color.black;
|
||||
if (Application.isPlaying) {
|
||||
@ -456,16 +469,16 @@ namespace NanoBrain.Unity {
|
||||
color = new Color(brightness, brightness, brightness, 1f);
|
||||
}
|
||||
DrawNucleus(siblingNeuron, position, color);
|
||||
row++;
|
||||
}
|
||||
Vector3 labelPos = position - Vector3.down * (discRadius + 5); // below neuron
|
||||
string name = $"{nucleus.parent.instances[0].baseName}\n{nucleus.name}";
|
||||
GUIStyle style = new(EditorStyles.label) {
|
||||
alignment = TextAnchor.UpperCenter,
|
||||
normal = { textColor = Color.white },
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
Vector3 labelPos = position - Vector3.down * (discRadius + 5); // below neuron
|
||||
string name = $"{sibling.baseName}\n{nucleus.name}";
|
||||
Handles.Label(labelPos, name, style);
|
||||
row++;
|
||||
}
|
||||
Handles.Label(labelPos, name, style);
|
||||
expandArray = false;
|
||||
}
|
||||
|
||||
@ -523,7 +536,6 @@ namespace NanoBrain.Unity {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue) {
|
||||
maxValue = 1;
|
||||
Color color;
|
||||
@ -538,7 +550,6 @@ namespace NanoBrain.Unity {
|
||||
DrawNucleus(nucleus, position, color);
|
||||
}
|
||||
|
||||
|
||||
protected void DrawNucleus(Nucleus nucleus, Vector2 position, Color color) {
|
||||
if (nucleus == null)
|
||||
return;
|
||||
@ -696,7 +707,7 @@ namespace NanoBrain.Unity {
|
||||
e.Use();
|
||||
Selection.activeObject = prefab;
|
||||
EditorGUIUtility.PingObject(prefab);
|
||||
ClusterViewer.previousPrefab = null;
|
||||
ClusterView.previousPrefab = null;
|
||||
Editor.CreateEditor(prefab);
|
||||
}
|
||||
}
|
||||
@ -762,16 +773,16 @@ namespace NanoBrain.Unity {
|
||||
protected void OnNeuronClick(Nucleus nucleus) {
|
||||
if (nucleus == this.currentNucleus) {
|
||||
this.selectedSynapseNeuron = null;
|
||||
// if (Application.isPlaying) {
|
||||
// if (nucleus is Cluster)
|
||||
// expandArray = !expandArray;
|
||||
// else
|
||||
// expandArray = false;
|
||||
// }
|
||||
// else {
|
||||
if (nucleus is Cluster cluster)
|
||||
OnClusterClick(cluster);
|
||||
// }
|
||||
if (Application.isPlaying) {
|
||||
if (nucleus is Cluster)
|
||||
expandArray = !expandArray;
|
||||
else
|
||||
expandArray = false;
|
||||
}
|
||||
else {
|
||||
if (nucleus is Cluster cluster)
|
||||
OnClusterClick(cluster);
|
||||
}
|
||||
}
|
||||
else if (nucleus.parent != null && this.currentNucleus != null && nucleus.parent != this.currentNucleus.parent) {
|
||||
// We go to a different cluster
|
||||
@ -792,6 +803,7 @@ namespace NanoBrain.Unity {
|
||||
else {
|
||||
// select the cluster, not the neuron in the cluster
|
||||
this.currentNucleus = nucleus.parent;
|
||||
this.selectedSynapseNeuron = null;
|
||||
this.expandArray = false;
|
||||
}
|
||||
}
|
||||
@ -799,6 +811,7 @@ namespace NanoBrain.Unity {
|
||||
this.currentNucleus = nucleus;
|
||||
if (this.currentNucleus is Neuron neuron && neuron.receivers.Count == 0)
|
||||
this.selectedOutput = this.currentNucleus;
|
||||
this.selectedSynapseNeuron = null;
|
||||
this.expandArray = false;
|
||||
}
|
||||
}
|
||||
@ -807,7 +820,7 @@ namespace NanoBrain.Unity {
|
||||
// May be used with storedPrefab...
|
||||
Selection.activeObject = subCluster.prefab;
|
||||
EditorGUIUtility.PingObject(subCluster.prefab);
|
||||
ClusterViewer.previousPrefab = this.currentCluster.prefab;
|
||||
ClusterView.previousPrefab = this.currentCluster.prefab;
|
||||
Editor.CreateEditor(subCluster.prefab);
|
||||
}
|
||||
|
||||
|
||||
@ -1,2 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8a7663ccd347fd78dbdba393c03ed7c7
|
||||
guid: 8a7663ccd347fd78dbdba393c03ed7c7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@ -1,902 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace NanoBrain.Unity {
|
||||
|
||||
public class ClusterViewer : Editor {
|
||||
|
||||
public static ClusterPrefab previousPrefab;
|
||||
|
||||
public class GraphView : VisualElement {
|
||||
protected Cluster currentCluster;
|
||||
protected SerializedObject serializedBrain;
|
||||
protected Nucleus _currentNucleus;
|
||||
protected virtual Nucleus currentNucleus {
|
||||
get => _currentNucleus;
|
||||
set => _currentNucleus = value;
|
||||
}
|
||||
//protected Nucleus currentNucleus;
|
||||
protected Nucleus selectedOutput;
|
||||
// Only used when selecting a synapse to a multi-cluster
|
||||
protected Nucleus selectedSynapseNeuron;
|
||||
|
||||
protected GameObject gameObject;
|
||||
private bool expandArray = false;
|
||||
|
||||
protected ClusterPrefab prefabAsset;
|
||||
protected VisualElement topMenuContainer;
|
||||
protected ScrollView scrollView;
|
||||
protected IMGUIContainer graphContainer;
|
||||
//protected readonly PopupField<string> outputsPopup;
|
||||
|
||||
public enum Mode {
|
||||
Focus,
|
||||
Full
|
||||
}
|
||||
public Mode mode = Mode.Focus;
|
||||
|
||||
public GraphView(Cluster cluster) {
|
||||
this.currentCluster = cluster;
|
||||
|
||||
name = "content";
|
||||
style.flexGrow = 1;
|
||||
|
||||
topMenuContainer = new() {
|
||||
style = {
|
||||
flexDirection = FlexDirection.Row,
|
||||
alignItems = Align.Center,
|
||||
}
|
||||
};
|
||||
|
||||
EnumField modePopup = new(mode);
|
||||
modePopup.style.width = 80;
|
||||
modePopup.RegisterValueChangedCallback(OnModeChange);
|
||||
topMenuContainer.Add(modePopup);
|
||||
|
||||
scrollView = new(ScrollViewMode.Horizontal);
|
||||
scrollView.style.position = Position.Absolute;
|
||||
scrollView.style.left = 0; scrollView.style.top = 0;
|
||||
scrollView.style.right = 0; scrollView.style.bottom = 0;
|
||||
scrollView.horizontalScrollerVisibility = ScrollerVisibility.Auto; // Auto shows when needed
|
||||
scrollView.verticalScrollerVisibility = ScrollerVisibility.Hidden;
|
||||
|
||||
graphContainer = new(OnIMGUI) {
|
||||
pickingMode = PickingMode.Position,
|
||||
focusable = true
|
||||
};
|
||||
|
||||
scrollView.contentContainer.Add(graphContainer);
|
||||
Add(scrollView);
|
||||
Add(topMenuContainer);
|
||||
|
||||
// Subscribe when added to panel (editor UI ready)
|
||||
RegisterCallback<AttachToPanelEvent>(evt => Subscribe());
|
||||
RegisterCallback<DetachFromPanelEvent>(evt => Unsubscribe());
|
||||
}
|
||||
|
||||
protected virtual void OnModeChange(ChangeEvent<System.Enum> changeEvent) {
|
||||
this.mode = (Mode)changeEvent.newValue;
|
||||
}
|
||||
|
||||
private 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) {
|
||||
this.gameObject = gameObject;
|
||||
if (this.currentCluster == null)
|
||||
return;
|
||||
|
||||
if (Application.isPlaying == false)
|
||||
this.serializedBrain = new SerializedObject(this.currentCluster.prefab);
|
||||
this.selectedOutput = this.currentCluster.defaultOutput;
|
||||
this.currentNucleus = this.selectedOutput;
|
||||
Rebuild();
|
||||
}
|
||||
|
||||
void Rebuild() {
|
||||
if (this.currentNucleus == null)
|
||||
return;
|
||||
|
||||
string path = AssetDatabase.GetAssetPath(this.currentCluster.prefab); // or known path
|
||||
this.prefabAsset = AssetDatabase.LoadAssetAtPath<ClusterPrefab>(path);
|
||||
if (this.prefabAsset == null) {
|
||||
// create in memory save if it doesn't exist
|
||||
this.prefabAsset = CreateInstance<ClusterPrefab>();
|
||||
//Debug.LogError("Cluster Prefab is not found on disk");
|
||||
}
|
||||
}
|
||||
|
||||
public void OnIMGUI() {
|
||||
var rect = graphContainer.layout; // container local size
|
||||
int id = GUIUtility.GetControlID(123456, FocusType.Passive, new Rect(0, 0, rect.width, rect.height));
|
||||
|
||||
//int id = GUIUtility.GetControlID(FocusType.Passive);
|
||||
|
||||
if (Application.isPlaying == false && serializedBrain != null)
|
||||
serializedBrain.Update();
|
||||
|
||||
Rect r = new Rect(0, 0, rect.width, rect.height);
|
||||
ClusterView.Render(r, currentCluster, serializedBrain);
|
||||
// ClusterView clusterView = ClusterView.GetClusterView(serializedBrain);
|
||||
// clusterView.currentCluster ??= currentCluster;
|
||||
// clusterView.DrawGraph(id);
|
||||
|
||||
// Handles.BeginGUI();
|
||||
// DrawGraph();
|
||||
// Handles.EndGUI();
|
||||
}
|
||||
|
||||
#region Graph
|
||||
|
||||
// public virtual void DrawGraph() {
|
||||
// if (mode == Mode.Focus)
|
||||
// DrawFocusGraph();
|
||||
// // else
|
||||
// // DrawFullGraph();
|
||||
// }
|
||||
|
||||
#region Full Graph
|
||||
/*
|
||||
protected void DrawFullGraph() {
|
||||
//Dag dag = GenerateGraph(this.prefab);
|
||||
Dag dag = GenerateGraph(this.selectedOutput);
|
||||
Dag.ComputeLayout(dag);
|
||||
// Draw edges
|
||||
foreach (Dag.Edge e in dag.edges) {
|
||||
Dag.Node from = dag.nodes.FirstOrDefault(x => x.id == e.fromId);
|
||||
Dag.Node to = dag.nodes.FirstOrDefault(x => x.id == e.toId);
|
||||
if (from == null || to == null)
|
||||
continue;
|
||||
|
||||
Vector2 fromPosition = from.position;
|
||||
Vector2 toPosition = to.position;
|
||||
DrawEdge(fromPosition, toPosition);
|
||||
}
|
||||
|
||||
// Draw nodes
|
||||
foreach (Dag.Node n in dag.nodes)
|
||||
DrawNucleus(n.nucleus, n.position, 1, n.radius);
|
||||
|
||||
// Determine graph width
|
||||
float width = 0;
|
||||
float currentNucleusPosition = 0;
|
||||
foreach (Dag.Node node in dag.nodes) {
|
||||
if (node.position.x > width)
|
||||
width = node.position.x;
|
||||
if (node.nucleus == currentNucleus)
|
||||
currentNucleusPosition = node.position.x;
|
||||
}
|
||||
|
||||
// Resize the graph container to the full graph width
|
||||
float margin = 50f;
|
||||
graphContainer.style.width = width + 2 * margin;
|
||||
|
||||
// Scroll to the current nucleus
|
||||
float viewportWidth = scrollView.layout.width;
|
||||
// center currentNucleus in viewport
|
||||
float desiredScrollX = currentNucleusPosition - viewportWidth * 0.5f;
|
||||
// clamp between 0 and maximum scrollable range
|
||||
float maxScrollX = Mathf.Max(0f, graphContainer.resolvedStyle.width - viewportWidth);
|
||||
desiredScrollX = Mathf.Clamp(desiredScrollX, 0f, maxScrollX);
|
||||
|
||||
Vector2 current = scrollView.scrollOffset;
|
||||
scrollView.scrollOffset = new Vector2(desiredScrollX, current.y);
|
||||
}
|
||||
|
||||
public Dag GenerateGraph(Nucleus rootNucleus) {
|
||||
Dag dag = new();
|
||||
if (rootNucleus == null)
|
||||
return dag;
|
||||
|
||||
int ix = 0;
|
||||
Dag.Node receiver = new() {
|
||||
id = ix,
|
||||
//title = nucleus.name,
|
||||
nucleus = rootNucleus
|
||||
};
|
||||
dag.nodes.Add(receiver);
|
||||
ix++;
|
||||
DescendGraph(receiver, ref ix, dag);
|
||||
return dag;
|
||||
}
|
||||
|
||||
private void DescendGraph(Dag.Node receiver, ref int ix, Dag dag) {
|
||||
Neuron receiverNeuron = receiver.nucleus as Neuron;
|
||||
foreach (Synapse synapse in receiverNeuron.synapses) {
|
||||
Nucleus nucleus = synapse.neuron;
|
||||
if (nucleus.parent != null && nucleus.parent != currentNucleus.parent) {
|
||||
nucleus = nucleus.parent;
|
||||
}
|
||||
string nucleusName = nucleus.name;
|
||||
Dag.Node synapseNode = dag.FindNode(nucleusName);
|
||||
if (synapseNode == null) {
|
||||
synapseNode = new() {
|
||||
id = ix,
|
||||
nucleus = nucleus
|
||||
};
|
||||
dag.nodes.Add(synapseNode);
|
||||
}
|
||||
Dag.Edge edge = new() {
|
||||
fromId = synapseNode.id,
|
||||
toId = receiver.id
|
||||
};
|
||||
dag.edges.Add(edge);
|
||||
ix++;
|
||||
DescendGraph(synapseNode, ref ix, dag);
|
||||
}
|
||||
}
|
||||
*/
|
||||
#endregion Full Graph
|
||||
|
||||
#region Focus Graph
|
||||
/*
|
||||
protected void DrawFocusGraph() {
|
||||
float size = 20;
|
||||
Vector3 position = new(150, 210, 0);
|
||||
|
||||
if (this.currentNucleus != null) {
|
||||
DrawReceivers(this.currentNucleus, position, size);
|
||||
DrawSynapses(this.currentNucleus, position, size);
|
||||
|
||||
// Draw selected Nucleus
|
||||
if (expandArray) {
|
||||
float maxValue = 1;
|
||||
|
||||
if (this.currentNucleus is Cluster cluster) {
|
||||
float spacing = 400f / cluster.instanceCount;
|
||||
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;
|
||||
if (cluster.instances == null) {
|
||||
Vector3 pos = new(150, margin + row * spacing, 0.0f);
|
||||
Handles.color = Color.white;
|
||||
// The selected sibling highlight ring
|
||||
Handles.DrawSolidDisc(pos, Vector3.forward, size + 2);
|
||||
DrawNucleus(cluster, pos, maxValue, size);
|
||||
row++;
|
||||
}
|
||||
else {
|
||||
foreach (Cluster sibling in cluster.instances) {
|
||||
Vector3 pos = new(150, margin + row * spacing, 0.0f);
|
||||
Handles.color = Color.white;
|
||||
// The selected sibling highlight ring
|
||||
Handles.DrawSolidDisc(pos, Vector3.forward, size + 2);
|
||||
DrawNucleus(sibling, pos, maxValue, size);
|
||||
row++;
|
||||
}
|
||||
}
|
||||
GUIStyle style = new(EditorStyles.label) {
|
||||
alignment = TextAnchor.UpperCenter,
|
||||
normal = { textColor = Color.white },
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
Vector3 labelPos = new(150, yMax + size + 5, 0);
|
||||
string clusterName = cluster.name;
|
||||
int colonPos = clusterName.IndexOf(":");
|
||||
if (colonPos > 0) {
|
||||
string baseName = clusterName[..colonPos];
|
||||
Handles.Label(labelPos, baseName, style);
|
||||
}
|
||||
else
|
||||
Handles.Label(labelPos, clusterName, style);
|
||||
}
|
||||
else {
|
||||
if (this.currentNucleus is Neuron neuron)
|
||||
maxValue = neuron.outputMagnitude;
|
||||
|
||||
DrawNucleus(this.currentNucleus, position, maxValue, 20);
|
||||
|
||||
}
|
||||
}
|
||||
else {
|
||||
float maxValue = 1;
|
||||
if (this.currentNucleus is Neuron neuron)
|
||||
maxValue = neuron.outputMagnitude;
|
||||
else if (this.currentNucleus is Cluster cluster)
|
||||
maxValue = cluster.defaultOutput.outputMagnitude;
|
||||
DrawNucleus(this.currentNucleus, position, maxValue, 20);
|
||||
}
|
||||
}
|
||||
else {
|
||||
DrawAllOutputs(position, size);
|
||||
DrawOutputs(position, size);
|
||||
}
|
||||
graphContainer.style.width = 300;
|
||||
}
|
||||
|
||||
protected void DrawReceivers(Nucleus nucleus, Vector3 parentPos, float size) {
|
||||
List<Nucleus> receivers;
|
||||
if (nucleus is Neuron neuron)
|
||||
receivers = neuron.receivers;
|
||||
else if (nucleus is Cluster cluster)
|
||||
receivers = cluster.CollectReceivers(true);
|
||||
else
|
||||
return;
|
||||
|
||||
// For top-level nodes, add link to previous editor and/or 'Outputs'
|
||||
int nodeCount = receivers.Count();
|
||||
if (nucleus == this.selectedOutput) {
|
||||
// Add link to 'Outpus'
|
||||
nodeCount++;
|
||||
if (ClusterViewer.previousPrefab != null)
|
||||
// Add link to previous editor
|
||||
nodeCount++;
|
||||
}
|
||||
|
||||
// Determine the maximum value in this layer
|
||||
// This is used to 'scale' the output value colors of the nuclei
|
||||
float maxValue = 0;
|
||||
foreach (Nucleus receiver in receivers) {
|
||||
if (receiver is Neuron neuroid) {
|
||||
float value = neuroid.outputMagnitude;
|
||||
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 (Nucleus receiver in receivers) {
|
||||
Nucleus receiverNucleus = receiver;
|
||||
if (receiverNucleus == null)
|
||||
continue;
|
||||
|
||||
Vector3 pos = new(50, margin + row * spacing, 0.0f);
|
||||
DrawEdge(parentPos, pos);
|
||||
|
||||
DrawNucleus(receiverNucleus, pos, maxValue, size);
|
||||
row++;
|
||||
}
|
||||
if (nucleus == this.selectedOutput) {
|
||||
Vector3 pos = new(50, margin + row * spacing, 0);
|
||||
if (ClusterViewer.previousPrefab != null) {
|
||||
DrawEdge(parentPos, pos);
|
||||
DrawClusterPrefab(ClusterViewer.previousPrefab, pos, size);
|
||||
row++;
|
||||
}
|
||||
pos = new(50, margin + row * spacing, 0);
|
||||
DrawEdge(parentPos, pos);
|
||||
DrawAllOutputs(pos, size);
|
||||
}
|
||||
}
|
||||
|
||||
protected void DrawSynapses(Nucleus nucleus, Vector3 parentPos, float size) {
|
||||
if (nucleus is not Neuron neuron)
|
||||
return;
|
||||
|
||||
if (this.selectedSynapseNeuron != null) {
|
||||
DrawClusterSynapses(this.selectedSynapseNeuron, parentPos, size);
|
||||
return;
|
||||
}
|
||||
if (nucleus == null)
|
||||
return;
|
||||
|
||||
// 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<string> drawnNeuronNames = new();
|
||||
foreach (Synapse synapse in neuron.synapses) {
|
||||
if (synapse.neuron == null)
|
||||
continue;
|
||||
|
||||
// Count multiple synapses to the same neuron only once
|
||||
string neuronName = synapse.neuron.name;
|
||||
if (synapse.neuron.parent != null)
|
||||
neuronName = synapse.neuron.parent.baseName + "." + neuronName;
|
||||
|
||||
if (drawnNeuronNames.Contains(neuronName))
|
||||
continue;
|
||||
drawnNeuronNames.Add(neuronName);
|
||||
|
||||
float value = synapse.neuron.outputMagnitude * 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;
|
||||
//List<Neuron> drawnNeurons = new();
|
||||
drawnNeuronNames = new();
|
||||
foreach (Synapse synapse in neuron.synapses) {
|
||||
if (synapse.neuron is null)
|
||||
continue;
|
||||
|
||||
// Draw multiple synapses to the same neuron only once
|
||||
string neuronName = synapse.neuron.name;
|
||||
if (synapse.neuron.parent != null)
|
||||
neuronName = synapse.neuron.parent.baseName + "." + neuronName;
|
||||
|
||||
if (drawnNeuronNames.Contains(neuronName))
|
||||
continue;
|
||||
drawnNeuronNames.Add(neuronName);
|
||||
|
||||
Vector3 pos = new(250, margin + row * spacing, 0.0f);
|
||||
DrawEdge(parentPos, pos);
|
||||
// Handles.color = Color.white;
|
||||
// Handles.DrawLine(parentPos, pos);
|
||||
Color color = Color.black;
|
||||
if (Application.isPlaying) {
|
||||
if (maxValue == 0 || !float.IsFinite(maxValue))
|
||||
maxValue = 1;
|
||||
float brightness = synapse.neuron.outputMagnitude * synapse.weight / maxValue;
|
||||
color = new Color(brightness, brightness, brightness, 1f);
|
||||
}
|
||||
DrawNucleus(synapse.neuron, pos, size, color);
|
||||
row++;
|
||||
}
|
||||
}
|
||||
|
||||
protected void DrawClusterSynapses(Nucleus nucleus, Vector3 parentPos, float size) {
|
||||
if (nucleus == null || nucleus.parent == null || nucleus.parent.instances == null)
|
||||
return;
|
||||
|
||||
// Hack to disable showing labels
|
||||
expandArray = true;
|
||||
|
||||
float maxValue = 0;
|
||||
foreach (Cluster sibling in nucleus.parent.instances) {
|
||||
Neuron siblingNeuron = sibling.GetNucleus(nucleus.name) as Neuron;
|
||||
float value = siblingNeuron.outputMagnitude; // no need to add weight as they are all the same
|
||||
if (value > maxValue)
|
||||
maxValue = value;
|
||||
}
|
||||
|
||||
// Determine the spacing of the nuclei in the layer
|
||||
float spacing = 400f / nucleus.parent.instanceCount;
|
||||
float margin = 10 + spacing / 2;
|
||||
|
||||
int row = 0;
|
||||
foreach (Cluster sibling in nucleus.parent.instances) {
|
||||
Neuron siblingNeuron = sibling.GetNucleus(nucleus.name) as Neuron;
|
||||
Vector3 position = new(250, margin + row * spacing, 0.0f);
|
||||
DrawEdge(parentPos, position);
|
||||
Color color = Color.black;
|
||||
if (Application.isPlaying) {
|
||||
if (maxValue == 0 || !float.IsFinite(maxValue))
|
||||
maxValue = 1;
|
||||
float brightness = siblingNeuron.outputMagnitude / maxValue;
|
||||
color = new Color(brightness, brightness, brightness, 1f);
|
||||
}
|
||||
DrawNucleus(siblingNeuron, position, size, color);
|
||||
GUIStyle style = new(EditorStyles.label) {
|
||||
alignment = TextAnchor.UpperCenter,
|
||||
normal = { textColor = Color.white },
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron
|
||||
string name = $"{sibling.baseName}\n{nucleus.name}";
|
||||
Handles.Label(labelPos, name, style);
|
||||
row++;
|
||||
}
|
||||
expandArray = false;
|
||||
}
|
||||
|
||||
protected void DrawOutputs(Vector2 parentPos, float size) {
|
||||
if (this.currentCluster == null)
|
||||
return;
|
||||
|
||||
// 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<Nucleus> drawnNuclei = new();
|
||||
foreach (Nucleus nucleus in this.currentCluster.outputs) {
|
||||
if (nucleus is not Neuron neuron)
|
||||
continue;
|
||||
|
||||
// Draw multiple synapses to the same neuron only once
|
||||
if (drawnNuclei.Contains(nucleus))
|
||||
continue;
|
||||
drawnNuclei.Add(nucleus);
|
||||
|
||||
float value = neuron.outputMagnitude;
|
||||
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;
|
||||
drawnNuclei = new();
|
||||
foreach (Nucleus nucleus in this.currentCluster.outputs) {
|
||||
if (nucleus is not Neuron neuron)
|
||||
continue;
|
||||
|
||||
// Draw multiple synapses to the same neuron only once
|
||||
if (drawnNuclei.Contains(nucleus))
|
||||
continue;
|
||||
drawnNuclei.Add(nucleus);
|
||||
|
||||
Vector3 pos = new(250, margin + row * spacing, 0.0f);
|
||||
DrawEdge(parentPos, pos);
|
||||
Color color = Color.black;
|
||||
if (Application.isPlaying) {
|
||||
if (maxValue == 0 || !float.IsFinite(maxValue))
|
||||
maxValue = 1;
|
||||
float brightness = neuron.outputMagnitude / maxValue;
|
||||
color = new Color(brightness, brightness, brightness, 1f);
|
||||
}
|
||||
DrawNucleus(nucleus, pos, size, color);
|
||||
row++;
|
||||
}
|
||||
}
|
||||
*/
|
||||
#endregion Focus Graph
|
||||
/*
|
||||
protected void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size) {
|
||||
Color color;
|
||||
if (Application.isPlaying) {
|
||||
float brightness = 0;
|
||||
if (nucleus is Neuron neuron)
|
||||
brightness = neuron.outputMagnitude / maxValue;
|
||||
color = new Color(brightness, brightness, brightness, 1f);
|
||||
}
|
||||
else
|
||||
color = Color.black;
|
||||
DrawNucleus(nucleus, position, size, color);
|
||||
}
|
||||
|
||||
protected void DrawNucleus(Nucleus nucleus, Vector3 position, float size, Color color) {
|
||||
if (nucleus == null)
|
||||
return;
|
||||
|
||||
if (nucleus == this.currentNucleus) {
|
||||
// The selected nucleus highlight ring
|
||||
Handles.color = Color.white;
|
||||
Handles.DrawSolidDisc(position, Vector3.forward, size + 2);
|
||||
}
|
||||
|
||||
if (nucleus is MemoryCell) {
|
||||
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.parent is Cluster parentCluster && currentNucleus != null && parentCluster != currentNucleus.parent)
|
||||
DrawCluster(parentCluster, position, color, size);
|
||||
else if (nucleus is Cluster cluster)
|
||||
DrawCluster(cluster, position, color, size);
|
||||
|
||||
if (expandArray == false) {// || nucleus != currentNucleus) {
|
||||
// put name below nucleus
|
||||
Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron
|
||||
style.alignment = TextAnchor.UpperCenter;
|
||||
|
||||
if (nucleus.parent != null && currentNucleus != null && nucleus.parent != currentNucleus.parent && nucleus.parent is Cluster parentCluster1) {
|
||||
// This neuron is part of another cluster
|
||||
parentCluster1.name ??= "";
|
||||
int colonPos = parentCluster1.name.IndexOf(":");
|
||||
string baseName;
|
||||
if (colonPos > 0 && colonPos < parentCluster1.name.Length - 2)
|
||||
baseName = parentCluster1.name[..colonPos] + "\n";
|
||||
else
|
||||
baseName = parentCluster1.name + "\n";
|
||||
Handles.Label(labelPos, baseName + nucleus.name, style);
|
||||
}
|
||||
else {
|
||||
nucleus.name ??= "";
|
||||
int colonPos = nucleus.name.IndexOf(":");
|
||||
if (colonPos > 0 && colonPos < nucleus.name.Length - 2) {
|
||||
// if it is an array, we should not show the :0 of the first element
|
||||
string baseName = nucleus.name[..colonPos];
|
||||
Handles.Label(labelPos, baseName, style);
|
||||
}
|
||||
else
|
||||
Handles.Label(labelPos, nucleus.name, style);
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltip
|
||||
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();
|
||||
if (nucleus is Cluster parentCluster2)
|
||||
OnNeuronClick(parentCluster2);
|
||||
else
|
||||
OnNeuronClick(nucleus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void DrawCluster(Cluster cluster, Vector3 position, Color color, float size) {
|
||||
GUIStyle labelTextStyle = new(EditorStyles.label) {
|
||||
normal = { textColor = Color.white },
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
|
||||
if (expandArray) {
|
||||
// Put array indices above the discs
|
||||
labelTextStyle.alignment = TextAnchor.LowerCenter;
|
||||
Vector3 labelPosition = position + Vector3.down * (size + 5); // below disc
|
||||
|
||||
// Strip the instance number in the name
|
||||
int colonPos1 = cluster.name.IndexOf(":");
|
||||
if (colonPos1 > 0) {
|
||||
string extName = cluster.name[(colonPos1 + 2)..];
|
||||
Handles.Label(labelPosition, extName, labelTextStyle);
|
||||
}
|
||||
else
|
||||
Handles.Label(labelPosition, "0", labelTextStyle);
|
||||
}
|
||||
else {
|
||||
// Put instance count inside the disc
|
||||
labelTextStyle.alignment = TextAnchor.MiddleCenter;
|
||||
Vector3 labelPosition = position + (Vector3.forward * 0.1f);
|
||||
|
||||
// Adjust text color based on disc color
|
||||
if (color.grayscale > 0.5f)
|
||||
labelTextStyle.normal.textColor = Color.black;
|
||||
else
|
||||
labelTextStyle.normal.textColor = Color.white;
|
||||
|
||||
if (cluster.instanceCount > 1) {
|
||||
Handles.Label(labelPosition, cluster.instanceCount.ToString(), labelTextStyle);
|
||||
labelTextStyle.normal.textColor = Color.white;
|
||||
}
|
||||
else if (cluster.instances != null && cluster.instances.Length > 1) {
|
||||
Handles.Label(labelPosition, cluster.instances.Length.ToString(), labelTextStyle);
|
||||
labelTextStyle.normal.textColor = Color.white;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw a circle around the disc to indicate this is a Cluster
|
||||
Handles.color = Color.white;
|
||||
Handles.DrawWireDisc(position, Vector3.forward, size + 5);
|
||||
}
|
||||
|
||||
protected void DrawClusterPrefab(ClusterPrefab prefab, Vector2 position, float size) {
|
||||
Handles.color = Color.black;
|
||||
Handles.DrawSolidDisc(position, Vector3.forward, size);
|
||||
// Draw a circle around the disc to indicate this is a Cluster
|
||||
Handles.color = Color.white;
|
||||
Handles.DrawWireDisc(position, Vector3.forward, size + 5);
|
||||
|
||||
// put name below nucleus
|
||||
GUIStyle style = new(EditorStyles.label) {
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
normal = { textColor = Color.white },
|
||||
fontStyle = FontStyle.Bold,
|
||||
};
|
||||
Vector2 labelPos = position - Vector2.down * (size + 5); // below neuron
|
||||
style.alignment = TextAnchor.UpperCenter;
|
||||
Handles.Label(labelPos, prefab.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 click
|
||||
if (e.type == EventType.MouseDown && e.button == 0) {
|
||||
// Consume the event so the scene doesn't also handle it
|
||||
e.Use();
|
||||
Selection.activeObject = prefab;
|
||||
EditorGUIUtility.PingObject(prefab);
|
||||
ClusterViewer.previousPrefab = null;
|
||||
CreateEditor(prefab);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void DrawAllOutputs(Vector2 position, float size) {
|
||||
GUIStyle labelTextStyle = new(EditorStyles.label) {
|
||||
normal = { textColor = Color.white },
|
||||
fontStyle = FontStyle.Bold,
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
};
|
||||
Handles.Label(position, "Outputs", labelTextStyle);
|
||||
|
||||
Rect neuronRect = new(position.x - size, position.y - size, size * 2, size * 2);
|
||||
Event e = Event.current;
|
||||
if (e != null && neuronRect.Contains(e.mousePosition)) {
|
||||
// Process click
|
||||
if (e.type == EventType.MouseDown && e.button == 0) {
|
||||
// Consume the event so the scene doesn't also handle it
|
||||
e.Use();
|
||||
OnAllOutputsClick();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected void DrawEdge(Vector2 from, Vector2 to, float radius = 20) {
|
||||
Handles.color = Color.white;
|
||||
// Handles.DrawLine(from, to);
|
||||
|
||||
Vector2 dir = to - from;
|
||||
float len = dir.magnitude;
|
||||
if (len <= 2f * radius || len <= Mathf.Epsilon)
|
||||
// line too short
|
||||
return;
|
||||
|
||||
Vector2 n = dir / len; // normalized
|
||||
Vector2 a = from + n * radius;
|
||||
Vector2 b = to - n * radius;
|
||||
Handles.DrawLine(a, b);
|
||||
}
|
||||
|
||||
protected void HandleMouseHover(Nucleus nucleus, Rect rect) {
|
||||
GUIContent tooltip;
|
||||
if (nucleus is Neuron neuron) {
|
||||
tooltip = new(
|
||||
$"{nucleus.name}" +
|
||||
$"\nValue: {neuron.outputMagnitude}");
|
||||
}
|
||||
else
|
||||
tooltip = new($"{nucleus.name}");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
protected void OnNeuronClick(Nucleus nucleus) {
|
||||
if (nucleus == this.currentNucleus) {
|
||||
this.selectedSynapseNeuron = null;
|
||||
// if (Application.isPlaying) {
|
||||
// if (nucleus is Cluster)
|
||||
// expandArray = !expandArray;
|
||||
// else
|
||||
// expandArray = false;
|
||||
// }
|
||||
// else {
|
||||
if (nucleus is Cluster cluster)
|
||||
OnClusterClick(cluster);
|
||||
// }
|
||||
}
|
||||
else if (nucleus.parent != null && this.currentNucleus != null && nucleus.parent != this.currentNucleus.parent) {
|
||||
// We go to a different cluster
|
||||
if (Application.isPlaying) {
|
||||
if (this.selectedSynapseNeuron == null && nucleus.parent.instanceCount > 1) {
|
||||
this.selectedSynapseNeuron = nucleus;
|
||||
expandArray = false;
|
||||
}
|
||||
else {
|
||||
this.currentNucleus = nucleus;
|
||||
if (this.currentNucleus is Neuron neuron && neuron.receivers.Count == 0)
|
||||
this.selectedOutput = this.currentNucleus;
|
||||
this.selectedSynapseNeuron = null;
|
||||
expandArray = false;
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
// select the cluster, not the neuron in the cluster
|
||||
this.currentNucleus = nucleus.parent;
|
||||
expandArray = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.currentNucleus = nucleus;
|
||||
if (this.currentNucleus is Neuron neuron && neuron.receivers.Count == 0)
|
||||
this.selectedOutput = this.currentNucleus;
|
||||
expandArray = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected void OnClusterClick(Cluster subCluster) {
|
||||
// May be used with storedPrefab...
|
||||
Selection.activeObject = subCluster.prefab;
|
||||
EditorGUIUtility.PingObject(subCluster.prefab);
|
||||
ClusterViewer.previousPrefab = this.currentCluster.prefab;
|
||||
ClusterEditor newEditor = CreateEditor(subCluster.prefab) as ClusterEditor;
|
||||
}
|
||||
|
||||
protected void OnAllOutputsClick() {
|
||||
this.currentNucleus = null;
|
||||
this.selectedOutput = null;
|
||||
expandArray = false;
|
||||
}
|
||||
*/
|
||||
#endregion Graph
|
||||
|
||||
void OnSceneGUI(SceneView sceneView) {
|
||||
if (this.gameObject != null) {
|
||||
|
||||
Handles.color = Color.yellow;
|
||||
if (this.selectedSynapseNeuron != null) {
|
||||
foreach (Cluster sibling in this.selectedSynapseNeuron.parent.instances) {
|
||||
Neuron siblingNeuron = sibling.GetNucleus(this.selectedSynapseNeuron.name) as Neuron;
|
||||
Vector3 worldVector = this.gameObject.transform.TransformVector(siblingNeuron.outputValue);
|
||||
Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector);
|
||||
}
|
||||
// if (this.currentNucleus is Cluster cluster) {
|
||||
// foreach (Cluster sibling in cluster.siblingClusters) {
|
||||
|
||||
// }
|
||||
// }
|
||||
// // if (this.currentNucleus is IReceptor receptor) {
|
||||
// // foreach (Nucleus nucleus in receptor.nucleiArray) {
|
||||
// // if (nucleus is Neuron neuron) {
|
||||
// // Vector3 worldVector = this.gameObject.transform.TransformVector(neuron.outputValue);
|
||||
// // Handles.color = Color.yellow;
|
||||
// // Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector);
|
||||
// // }
|
||||
// // }
|
||||
}
|
||||
else {
|
||||
if (this.currentNucleus is Neuron currentNeuron) {
|
||||
Vector3 worldVector = this.gameObject.transform.TransformVector(currentNeuron.outputValue);
|
||||
Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class NeuroidLayer {
|
||||
public int ix = 0;
|
||||
public List<Nucleus> neuroids = new();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
308
Editor/Cluster_Drawer.cs
Normal file
308
Editor/Cluster_Drawer.cs
Normal file
@ -0,0 +1,308 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using System;
|
||||
|
||||
namespace NanoBrain.Unity {
|
||||
|
||||
[CustomPropertyDrawer(typeof(Cluster))]
|
||||
class Cluster_Drawer : PropertyDrawer {
|
||||
|
||||
static Cluster_Drawer() {
|
||||
if (Application.isPlaying)
|
||||
SceneView.duringSceneGui += OnSceneGUI;
|
||||
|
||||
Selection.selectionChanged += OnSelectionChanged;
|
||||
if (clusterView != null)
|
||||
clusterView.initialized = false;
|
||||
}
|
||||
|
||||
private const float padding = 4f;
|
||||
private const float elementHeight = 64f; // height reserved for the VisualElement
|
||||
private static ClusterView clusterView;
|
||||
private static UnityEngine.Object selectedTarget;
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
|
||||
if (Cluster_Drawer.clusterView == null)
|
||||
return 0;
|
||||
|
||||
float height = EditorGUIUtility.singleLineHeight + padding;
|
||||
|
||||
SerializedProperty prefabProp = property.FindPropertyRelative(nameof(Cluster.prefab));
|
||||
if (prefabProp.objectReferenceValue != null && Cluster_Drawer.clusterView.isOpen) {
|
||||
height += padding + elementHeight;
|
||||
height = 500;
|
||||
}
|
||||
else
|
||||
height = 18;
|
||||
return height;
|
||||
}
|
||||
|
||||
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
|
||||
// Not sure if this works properly with multiple Clusters in a single gameObject
|
||||
ClusterView clusterView = Cluster_Drawer.clusterView;
|
||||
if (Cluster_Drawer.clusterView == null)
|
||||
clusterView = ClusterView.GetClusterView(property);
|
||||
|
||||
label = EditorGUI.BeginProperty(position, label, property);
|
||||
|
||||
// Begin indent block
|
||||
int indent = EditorGUI.indentLevel;
|
||||
EditorGUI.indentLevel = 0;
|
||||
|
||||
SerializedProperty prefabProp = property.FindPropertyRelative(nameof(Cluster.prefab));
|
||||
|
||||
// Draw the object field on the top line
|
||||
Rect fieldRect = new(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
|
||||
EditorGUI.BeginChangeCheck();
|
||||
EditorGUI.PropertyField(fieldRect, prefabProp, label);
|
||||
if (EditorGUI.EndChangeCheck()) {
|
||||
prefabProp.serializedObject.ApplyModifiedProperties();
|
||||
ClusterPrefab clusterPrefab = prefabProp.objectReferenceValue as ClusterPrefab;
|
||||
if (clusterPrefab != null) {
|
||||
Cluster newCluster = new(clusterPrefab);
|
||||
|
||||
SerializedObject serializedObject = property.serializedObject;
|
||||
foreach (UnityEngine.Object targetObject in serializedObject.targetObjects) {
|
||||
var parent = SerializedPropertyUtility.GetParentObjectAndMember(targetObject, property.propertyPath, out var memberInfo, out int outIndex);
|
||||
if (parent != null && memberInfo is FieldInfo fieldInfo) {
|
||||
fieldInfo.SetValue(parent, newCluster);
|
||||
EditorUtility.SetDirty(targetObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prefabProp.objectReferenceValue != null) {
|
||||
// Graph is not shown when multi-editing
|
||||
if (property.serializedObject.targetObjects.Length == 1) {
|
||||
UnityEngine.Object targetObject = property.serializedObject.targetObject;
|
||||
Cluster_Drawer.selectedTarget = targetObject;
|
||||
|
||||
Cluster cluster;
|
||||
if (clusterView.initialized || Application.isPlaying) {
|
||||
cluster = SerializedPropertyUtility.GetManagedObjectForProperty(targetObject, property.propertyPath) as Cluster;
|
||||
}
|
||||
else {
|
||||
Debug.Log("New cluster instance");
|
||||
// This does not work properly yet it seems
|
||||
ClusterPrefab clusterPrefab = prefabProp.objectReferenceValue as ClusterPrefab;
|
||||
cluster = new(clusterPrefab);
|
||||
object parent = SerializedPropertyUtility.GetParentObjectAndMember(targetObject, property.propertyPath, out var memberInfo, out int outIndex);
|
||||
if (parent != null && memberInfo is FieldInfo fieldInfo) {
|
||||
fieldInfo.SetValue(parent, cluster);
|
||||
EditorUtility.SetDirty(targetObject);
|
||||
}
|
||||
clusterView.initialized = true;
|
||||
}
|
||||
|
||||
// foldout header rect
|
||||
Rect headerRect = new(fieldRect.x, fieldRect.yMax + 4f, fieldRect.width, EditorGUIUtility.singleLineHeight);
|
||||
clusterView.isOpen = EditorGUI.Foldout(headerRect, clusterView.isOpen, "Graph", true);
|
||||
|
||||
if (clusterView.isOpen) {
|
||||
// content rect below header
|
||||
Rect drawRect = new(fieldRect.x, headerRect.yMax + 2f, fieldRect.width, 450f);
|
||||
|
||||
if (clusterView.currentCluster == null || clusterView.currentCluster != cluster) {
|
||||
clusterView.currentCluster = cluster;
|
||||
clusterView.currentNucleus = cluster.defaultOutput;
|
||||
clusterView.selectedOutput = clusterView.currentNucleus;
|
||||
}
|
||||
Cluster_Drawer.clusterView = clusterView;
|
||||
clusterView.Render(drawRect);
|
||||
//Debug.Log(prefab.cluster.defaultOutput.outputMagnitude);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel = indent;
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
|
||||
private static void OnSceneGUI(SceneView sceneView) {
|
||||
if (Application.isPlaying == false || selectedTarget == null)
|
||||
return;
|
||||
|
||||
GameObject gameObject = null;
|
||||
if (selectedTarget is Component c)
|
||||
gameObject = c.gameObject;
|
||||
else if (selectedTarget is GameObject g)
|
||||
gameObject = g;
|
||||
|
||||
Handles.color = Color.yellow;
|
||||
if (Cluster_Drawer.clusterView.selectedSynapseNeuron != null) {
|
||||
foreach (Cluster sibling in Cluster_Drawer.clusterView.selectedSynapseNeuron.parent.instances) {
|
||||
Neuron siblingNeuron = sibling.GetNeuron(Cluster_Drawer.clusterView.selectedSynapseNeuron.name);
|
||||
Vector3 worldVector = gameObject.transform.TransformVector(siblingNeuron.outputValue);
|
||||
Handles.DrawLine(gameObject.transform.position, gameObject.transform.position + worldVector);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (Cluster_Drawer.clusterView.currentNucleus is Neuron currentNeuron) {
|
||||
Vector3 worldVector = gameObject.transform.TransformVector(currentNeuron.outputValue);
|
||||
Handles.DrawLine(gameObject.transform.position, gameObject.transform.position + worldVector);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnSelectionChanged() {
|
||||
UnityEngine.Object active = Selection.activeObject;
|
||||
if (active == null)
|
||||
return;
|
||||
|
||||
foreach (ClusterView clusterView in ClusterView.clusterViews.Values)
|
||||
clusterView.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static class SerializedPropertyUtility {
|
||||
// Walks path like "myField.nested.arrayField.Array.data[0].value"
|
||||
// Returns parent object that contains the final member, and sets outMember (FieldInfo or PropertyInfo) for the final member.
|
||||
// If final target is an array/list element, returns parent as the IList and outMember = null, and outIndex set to element index.
|
||||
public static object GetParentObjectAndMember(object root, string propertyPath, out MemberInfo outMember, out int outIndex) {
|
||||
outMember = null;
|
||||
outIndex = -1;
|
||||
if (root == null || string.IsNullOrEmpty(propertyPath)) return null;
|
||||
|
||||
object obj = root;
|
||||
var parts = propertyPath.Split('.');
|
||||
for (int i = 0; i < parts.Length; i++) {
|
||||
string part = parts[i];
|
||||
|
||||
// handle array/list: Unity path uses "Array" then "data[index]"
|
||||
if (part == "Array" && i + 2 < parts.Length && parts[i + 1].StartsWith("data[")) {
|
||||
// previous part name is the list/array field name
|
||||
string listFieldName = parts[i - 1];
|
||||
// get the field info for listFieldName on current obj's type
|
||||
var fiList = GetFieldInTypeHierarchy(obj.GetType(), listFieldName);
|
||||
object listObj = fiList != null ? fiList.GetValue(obj) : GetPropValue(obj, listFieldName);
|
||||
|
||||
if (listObj == null) return null;
|
||||
|
||||
// parse index from e.g. "data[3]"
|
||||
string indexPart = parts[i + 1]; // like data[3]
|
||||
int start = indexPart.IndexOf('[') + 1;
|
||||
int end = indexPart.IndexOf(']');
|
||||
int index = int.Parse(indexPart.Substring(start, end - start));
|
||||
|
||||
// if this is the last element in the path, return parent=the IList and outIndex=index
|
||||
if (i + 2 == parts.Length - 0) {
|
||||
outMember = null;
|
||||
outIndex = index;
|
||||
return listObj as IList;
|
||||
}
|
||||
|
||||
// otherwise step into the element and continue
|
||||
if (listObj is IList ilist) {
|
||||
obj = ilist[index];
|
||||
}
|
||||
else {
|
||||
// not an IList — cannot continue
|
||||
return null;
|
||||
}
|
||||
|
||||
i += 1; // skip the data[...] part (we already consumed it)
|
||||
continue;
|
||||
}
|
||||
|
||||
// normal field/property access: if this is the last part, return parent and member
|
||||
bool isLast = (i == parts.Length - 1);
|
||||
|
||||
var fi = GetFieldInTypeHierarchy(obj.GetType(), part);
|
||||
if (fi != null) {
|
||||
if (isLast) {
|
||||
outMember = fi;
|
||||
return obj;
|
||||
}
|
||||
else {
|
||||
obj = fi.GetValue(obj);
|
||||
if (obj == null) return null;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var pi = GetPropertyInTypeHierarchy(obj.GetType(), part);
|
||||
if (pi != null) {
|
||||
if (isLast) {
|
||||
outMember = pi;
|
||||
return obj;
|
||||
}
|
||||
else {
|
||||
obj = pi.GetValue(obj);
|
||||
if (obj == null) return null;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// if nothing found, fail
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static object GetManagedObjectForProperty(object root, string propertyPath) {
|
||||
var obj = root;
|
||||
var parts = propertyPath.Split('.');
|
||||
for (int i = 0; i < parts.Length; i++) {
|
||||
var part = parts[i];
|
||||
|
||||
// handle array: "arrayField.Array.data[index]"
|
||||
if (part == "Array") {
|
||||
i += 2; // skip "Array" and "data[index]"
|
||||
var indexPart = parts[i]; // like "data[0]"
|
||||
int start = indexPart.IndexOf('[') + 1;
|
||||
int end = indexPart.IndexOf(']');
|
||||
int idx = int.Parse(indexPart.Substring(start, end - start));
|
||||
var listField = parts[i - 2];
|
||||
var fi = obj.GetType().GetField(listField, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
var list = fi.GetValue(obj) as System.Collections.IList;
|
||||
obj = list[idx];
|
||||
continue;
|
||||
}
|
||||
|
||||
var field = obj.GetType().GetField(part, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
if (field == null) {
|
||||
var prop = obj.GetType().GetProperty(part, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
if (prop == null) return null;
|
||||
obj = prop.GetValue(obj);
|
||||
}
|
||||
else {
|
||||
obj = field.GetValue(obj);
|
||||
}
|
||||
|
||||
if (obj == null) return null;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
static FieldInfo GetFieldInTypeHierarchy(Type type, string fieldName) {
|
||||
while (type != null) {
|
||||
var fi = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (fi != null) return fi;
|
||||
type = type.BaseType;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static PropertyInfo GetPropertyInTypeHierarchy(Type type, string propName) {
|
||||
while (type != null) {
|
||||
var pi = type.GetProperty(propName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (pi != null) return pi;
|
||||
type = type.BaseType;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static object GetPropValue(object obj, string name) {
|
||||
var pi = GetPropertyInTypeHierarchy(obj.GetType(), name);
|
||||
if (pi != null) return pi.GetValue(obj);
|
||||
var fi = GetFieldInTypeHierarchy(obj.GetType(), name);
|
||||
if (fi != null) return fi.GetValue(obj);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4fe58945c76d153edacc220597474ad2
|
||||
guid: 18e075a03ca2efdb2895079f63eb333a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@ -1,2 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a755ac8461bd0c714a852df47331048e
|
||||
guid: a755ac8461bd0c714a852df47331048e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@ -1,85 +0,0 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using Unity.Mathematics;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Collections;
|
||||
|
||||
namespace NanoBrain.Unity {
|
||||
|
||||
[CustomPropertyDrawer(typeof(Neuron))]
|
||||
class Neuron_Drawer : PropertyDrawer {
|
||||
public static void Insepctor(SerializedObject serializedObject, string propertyName ) {
|
||||
EditorGUILayout.PropertyField(serializedObject.FindProperty(propertyName));
|
||||
}
|
||||
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
|
||||
// Draw foldout + properties
|
||||
label = EditorGUI.BeginProperty(position, label, property);
|
||||
|
||||
// Begin indent block
|
||||
int indent = EditorGUI.indentLevel;
|
||||
EditorGUI.indentLevel = 0;
|
||||
|
||||
object instance = GetTargetObjectOfProperty(property);
|
||||
|
||||
float lineHeight = EditorGUIUtility.singleLineHeight;
|
||||
Rect r = new(position.x, position.y, position.width, lineHeight);
|
||||
if (instance != null) {
|
||||
FieldInfo field = typeof(Neuron).GetField("_outputValue", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (field != null) {
|
||||
float3 val = (float3)field.GetValue(instance);
|
||||
EditorGUI.Vector3Field(r, $"Neuron: {label}", val);
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel = indent;
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
|
||||
// height for 1 line
|
||||
return (EditorGUIUtility.singleLineHeight * 1) + (EditorGUIUtility.standardVerticalSpacing * 0);
|
||||
}
|
||||
|
||||
public static object GetTargetObjectOfProperty(SerializedProperty prop) {
|
||||
var path = prop.propertyPath.Replace(".Array.data[", "[");
|
||||
object obj = prop.serializedObject.targetObject;
|
||||
var elements = path.Split('.');
|
||||
foreach (var element in elements) {
|
||||
if (element.Contains("[")) {
|
||||
var elementName = element.Substring(0, element.IndexOf("["));
|
||||
var index = Convert.ToInt32(element.Substring(element.IndexOf("[")).Replace("[", "").Replace("]", ""));
|
||||
obj = GetValue_Imp(obj, elementName, index);
|
||||
}
|
||||
else {
|
||||
obj = GetValue_Imp(obj, element);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
static object GetValue_Imp(object source, string name) {
|
||||
if (source == null)
|
||||
return null;
|
||||
|
||||
Type t = source.GetType();
|
||||
FieldInfo f = t.GetField(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
|
||||
if (f != null)
|
||||
return f.GetValue(source);
|
||||
PropertyInfo p = t.GetProperty(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
|
||||
return p?.GetValue(source, null);
|
||||
}
|
||||
static object GetValue_Imp(object source, string name, int index) {
|
||||
if (GetValue_Imp(source, name) is not IEnumerable enumerable)
|
||||
return null;
|
||||
IEnumerator en = enumerable.GetEnumerator();
|
||||
for (int i = 0; i <= index; i++) {
|
||||
if (!en.MoveNext())
|
||||
return null;
|
||||
}
|
||||
return en.Current;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa0e340763ca6299e93d514b271ae38d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -1,69 +0,0 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
/*
|
||||
namespace NanoBrain.Unity {
|
||||
|
||||
/// <summary>
|
||||
/// A NanoBrain which can be used to control a gameobject
|
||||
/// </summary>
|
||||
/// A NanoBrain is a small neural network which can be used to implement functional behaviour.
|
||||
/// The network consists of neurons which are connected together with synapses.
|
||||
/// The output values of the neurons are of type Vector3 to support spatial computing.
|
||||
///
|
||||
/// This component is basically a Unity representation of a nanobrain cluster.
|
||||
/// \sa
|
||||
/// - \ref NanoBrain::Cluster "Cluster"
|
||||
/// - \ref NanoBrain::Neuron "Neuron"
|
||||
[HelpURL("https://passer.life/documentation/nanobrain/Documentation/html/class_nano_brain_1_1_unity_1_1_brain.html")]
|
||||
public class Brain : MonoBehaviour {
|
||||
/// <summary>
|
||||
/// The Cluster prefab from which the cluster is created
|
||||
/// </summary>
|
||||
public ClusterPrefab brainPrefab;
|
||||
|
||||
[NonSerialized]
|
||||
private Cluster brainInstance;
|
||||
/// <summary>
|
||||
/// The cluster isntance
|
||||
/// </summary>
|
||||
public Cluster brain {
|
||||
get {
|
||||
if (brainInstance == null && brainPrefab != null) {
|
||||
brainInstance = new Cluster(brainPrefab) {
|
||||
name = brainPrefab.name
|
||||
};
|
||||
} else if (brainInstance != null && brainPrefab == null) {
|
||||
brainInstance = null;
|
||||
}
|
||||
return brainInstance;
|
||||
}
|
||||
}
|
||||
|
||||
// public Cluster InitializeBrain() {
|
||||
// brainInstance = new Cluster(brainPrefab) {
|
||||
// name = brainPrefab.name
|
||||
// };
|
||||
// return brainInstance;
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// Update the weight for all Synapses coming from the Neuron with the given name
|
||||
/// </summary>
|
||||
/// <param name="brain">The cluster in which the synapses are updated</param>
|
||||
/// <param name="name">The name of the Neuron for which the weights are updated</param>
|
||||
/// <param name="weight">The new Synapse weight</param>
|
||||
public static void UpdateWeight(Cluster brain, string name, float weight) {
|
||||
Neuron root = brain.defaultOutput;
|
||||
foreach (Synapse synapse in root.synapses) {
|
||||
if (synapse.neuron.name == name) {
|
||||
if (synapse.weight != weight) {
|
||||
synapse.weight = weight;
|
||||
// Debug.Log($"Updated weight for {name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
*/
|
||||
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 92f34a5e4027a1dc39efd8ce63cf6aba
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -24,6 +24,9 @@ namespace NanoBrain {
|
||||
/// Cluster should always be created from prefabs
|
||||
public ClusterPrefab prefab;
|
||||
|
||||
//[HideInInspector]
|
||||
public int version;
|
||||
|
||||
/// <summary>
|
||||
/// The base name of the cluster. I don't think this is actively used at this moment
|
||||
/// </summary>
|
||||
@ -42,7 +45,9 @@ namespace NanoBrain {
|
||||
/// A cluster is a multi-cluster when there is more than one instance.
|
||||
/// The actual instances are only created at runtime.
|
||||
/// The value instanceCount determines how many instances will be present at runtime.
|
||||
[NonSerialized]
|
||||
//[NonSerialized]
|
||||
[SerializeReference]
|
||||
[HideInInspector]
|
||||
public Cluster[] instances;
|
||||
|
||||
/// <summary>
|
||||
@ -62,11 +67,8 @@ namespace NanoBrain {
|
||||
/// All nuclei in this cluster
|
||||
/// </summary>
|
||||
[SerializeReference]
|
||||
[HideInInspector]
|
||||
public List<Nucleus> nuclei = new();
|
||||
// the nuclei sorted using topological sorting
|
||||
// to ensure that the cluster is computer in the right order
|
||||
|
||||
//public List<Nucleus> sortedNuclei;
|
||||
|
||||
#region Init
|
||||
|
||||
@ -77,13 +79,12 @@ namespace NanoBrain {
|
||||
/// <param name="parent">The cluster in which this new cluster will be placed</param>
|
||||
public Cluster(ClusterPrefab prefab, Cluster parent) {
|
||||
this.prefab = prefab;
|
||||
this.version = prefab.version;
|
||||
this.name = prefab.name;
|
||||
|
||||
this.parent = parent;
|
||||
this.parent?.nuclei.Add(this);
|
||||
ClonePrefab();
|
||||
// _ = this.inputs;
|
||||
//this.sortedNuclei = TopologicalSort(this.nuclei);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -93,13 +94,12 @@ namespace NanoBrain {
|
||||
/// <param name="parent">The prefab in which the new copy is placed</param>
|
||||
public Cluster(ClusterPrefab prefab, ClusterPrefab parent = null) {
|
||||
this.prefab = prefab;
|
||||
this.version = prefab.version;
|
||||
this.name = prefab.name;
|
||||
if (parent != null)
|
||||
this.parent = parent.cluster;
|
||||
|
||||
ClonePrefab();
|
||||
// _ = this.inputs;
|
||||
//this.sortedNuclei = TopologicalSort(this.nuclei);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -108,6 +108,10 @@ namespace NanoBrain {
|
||||
/// Strange that this does not take any parameters or return values.
|
||||
/// Where which the clone be found???
|
||||
private void ClonePrefab() {
|
||||
//Debug.Log($"Clone Prefab {this.prefab.name} -> {this.name}");
|
||||
if (this.prefab == null || this.prefab.cluster == null || this.prefab.cluster.nuclei == null)
|
||||
return;
|
||||
|
||||
Nucleus[] prefabNuclei = this.prefab.cluster.nuclei.ToArray();
|
||||
|
||||
// first clone the nuclei without their connections
|
||||
@ -161,36 +165,30 @@ namespace NanoBrain {
|
||||
}
|
||||
}
|
||||
|
||||
if (Application.isPlaying) {
|
||||
// Only create cluster siblings at runtime
|
||||
foreach (Nucleus clonedNucleus in clonedNuclei) {
|
||||
if (clonedNucleus is not Cluster clonedCluster)
|
||||
continue;
|
||||
foreach (Nucleus clonedNucleus in clonedNuclei) {
|
||||
if (clonedNucleus is not Cluster clonedCluster)
|
||||
continue;
|
||||
|
||||
List<Cluster> siblings = new() {
|
||||
clonedCluster
|
||||
List<Cluster> siblings = new() { clonedCluster };
|
||||
for (int instanceIx = 1; instanceIx < clonedCluster.instanceCount; instanceIx++) {
|
||||
// Create another sibling
|
||||
Cluster sibling = new(clonedCluster.prefab, this) {
|
||||
name = $"{clonedCluster.baseName}: {instanceIx}",
|
||||
parent = this.parent,
|
||||
instanceCount = this.instanceCount,
|
||||
};
|
||||
for (int instanceIx = 1; instanceIx < clonedCluster.instanceCount; instanceIx++) {
|
||||
// Create another sibling
|
||||
Debug.Log($"create {clonedCluster.prefab.name} sibling");
|
||||
Cluster sibling = new(clonedCluster.prefab, this) {
|
||||
name = $"{clonedCluster.baseName}: {instanceIx}",
|
||||
parent = this.parent,
|
||||
instanceCount = this.instanceCount,
|
||||
};
|
||||
siblings.Add(sibling);
|
||||
CopyAllExternalReceivers(clonedCluster, sibling, clonedCluster.prefab, this);
|
||||
}
|
||||
Cluster[] siblingClusters = siblings.ToArray();
|
||||
foreach (Cluster sibling in siblings)
|
||||
sibling.instances = siblingClusters;
|
||||
siblings.Add(sibling);
|
||||
CopyAllExternalReceivers(clonedCluster, sibling, this);
|
||||
}
|
||||
Cluster[] siblingClusters = siblings.ToArray();
|
||||
foreach (Cluster sibling in siblings)
|
||||
sibling.instances = siblingClusters;
|
||||
}
|
||||
|
||||
// Ensure that all neurons are computed to initialize bias
|
||||
foreach (Nucleus clonedNucleus in clonedNuclei) {
|
||||
if (clonedNucleus is not Cluster)
|
||||
clonedNucleus.UpdateStateIsolated();
|
||||
}
|
||||
// Ensure that all neurons are computed to initialize bias
|
||||
foreach (Nucleus clonedNucleus in clonedNuclei) {
|
||||
if (clonedNucleus is not Cluster)
|
||||
clonedNucleus.UpdateStateIsolated();
|
||||
}
|
||||
}
|
||||
|
||||
@ -202,13 +200,11 @@ namespace NanoBrain {
|
||||
parent = this.parent,
|
||||
instanceCount = this.instanceCount,
|
||||
};
|
||||
// Somehow siblingClusters should be cloned too. Believe I do this in ClonePrefab right now.
|
||||
|
||||
return clone;
|
||||
}
|
||||
|
||||
private static void CopyAllExternalReceivers(Cluster sourceCluster, Cluster sibling, ClusterPrefab prefabParent, Cluster clonedParent) {
|
||||
|
||||
private static void CopyAllExternalReceivers(Cluster sourceCluster, Cluster sibling, Cluster clonedParent) {
|
||||
for (int nucleusIx = 0; nucleusIx < sourceCluster.nuclei.Count; nucleusIx++) {
|
||||
Nucleus sourceNucleus = sourceCluster.nuclei[nucleusIx];
|
||||
if (sourceNucleus is not Neuron sourceNeuron)
|
||||
@ -237,7 +233,7 @@ namespace NanoBrain {
|
||||
}
|
||||
|
||||
clonedNeuron.AddReceiver(receiver, weight);
|
||||
Debug.Log($"external: {receiver.name} receives from {clonedNeuron.name} {clonedNeuron.GetHashCode()}");
|
||||
// Debug.Log($"external: {receiver.name} receives from {clonedNeuron.name} {clonedNeuron.GetHashCode()}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -460,7 +456,7 @@ namespace NanoBrain {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The neurons without outgoing connections
|
||||
/// </summary>
|
||||
@ -500,6 +496,8 @@ namespace NanoBrain {
|
||||
foreach (Nucleus receptor in this.nuclei) {
|
||||
if (receptor is Nucleus nucleus)
|
||||
if (nucleus.name == nucleusName) {
|
||||
// if (nucleus is Cluster cluster)
|
||||
// cluster.CheckInstances();
|
||||
foundNucleus = nucleus;
|
||||
return true;
|
||||
}
|
||||
@ -509,52 +507,56 @@ namespace NanoBrain {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a nucleus in this cluster
|
||||
/// Get a neuron in this cluster
|
||||
/// </summary>
|
||||
/// <param name="nucleusName">The name of the nucleus to find</param>
|
||||
/// <returns>The found nucleus or null when it is not found</returns>
|
||||
public Nucleus GetNucleus(string nucleusName) {
|
||||
int dotPosition = nucleusName.IndexOf('.');
|
||||
/// <param name="neuronName">The name of the neuron to find</param>
|
||||
/// <returns>The found neuron or null when it is not found</returns>
|
||||
public Neuron GetNeuron(string neuronName) {
|
||||
if (this.nuclei == null)
|
||||
return null;
|
||||
|
||||
foreach (Nucleus nucleus in this.nuclei) {
|
||||
if (nucleus is Neuron neuron && neuron.name == neuronName)
|
||||
return neuron;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a subcluster in this cluster
|
||||
/// </summary>
|
||||
/// <param name="clusterName">The name of the cluster to find</param>
|
||||
/// <returns>The found cluster or null when it is not found</returns>
|
||||
public Cluster GetCluster(string clusterName) {
|
||||
int dotPosition = clusterName.IndexOf('.');
|
||||
if (dotPosition >= 0) {
|
||||
string clusterName = nucleusName[..dotPosition];
|
||||
string clusterName0 = clusterName + ": 0";
|
||||
string clusterBaseName = clusterName[..dotPosition];
|
||||
string clusterName0 = clusterBaseName + ": 0";
|
||||
foreach (Nucleus nucleus in this.nuclei) {
|
||||
if (nucleus is Cluster cluster) {
|
||||
if (cluster.name == clusterName || cluster.name == clusterName0) {
|
||||
string subNucleusName = nucleusName[(dotPosition + 1)..];
|
||||
return cluster.GetNucleus(subNucleusName);
|
||||
if (cluster.name == clusterBaseName || cluster.name == clusterName0) {
|
||||
// cluster.CheckInstances();
|
||||
string subNucleusName = clusterName[(dotPosition + 1)..];
|
||||
return cluster.GetCluster(subNucleusName);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
string nucleusName0 = nucleusName + ": 0";
|
||||
string nucleusName0 = clusterName + ": 0";
|
||||
foreach (Nucleus nucleus in this.nuclei) {
|
||||
if (nucleus is Cluster) {
|
||||
if (nucleus.name == nucleusName || nucleus.name == nucleusName0)
|
||||
return nucleus;
|
||||
if (nucleus is Cluster cluster) {
|
||||
if (nucleus.name == clusterName || nucleus.name == nucleusName0) {
|
||||
// cluster.CheckInstances();
|
||||
return cluster;
|
||||
}
|
||||
}
|
||||
else if (nucleus.name == nucleusName)
|
||||
return nucleus;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a neuron in this cluster
|
||||
/// </summary>
|
||||
/// <param name="neuronName">The name of the neuron to find</param>
|
||||
/// <returns>The found neuron or null when it is not found</returns>
|
||||
public Neuron GetNeuron(string neuronName) {
|
||||
foreach (Nucleus nucleus in this.nuclei) {
|
||||
if (nucleus is Neuron neuron && neuron.name == neuronName)
|
||||
return neuron;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a neuron in an instance of a multi-cluster
|
||||
/// </summary>
|
||||
@ -568,6 +570,7 @@ namespace NanoBrain {
|
||||
return this.GetNeuron(neuronName);
|
||||
|
||||
// See if we are already using a cluster for thingId
|
||||
thingClusters ??= new();
|
||||
if (thingClusters.TryGetValue(thingId, out Cluster cluster))
|
||||
return cluster.GetNeuron(neuronName);
|
||||
|
||||
|
||||
@ -42,6 +42,7 @@ namespace NanoBrain {
|
||||
/// </summary>
|
||||
/// The bias which a value which is always added to the combined value of the neuron
|
||||
/// It does not have a synapse and therefore no weight of source nucleus
|
||||
[HideInInspector]
|
||||
public Vector3 bias = Vector3.zero;
|
||||
|
||||
#region Synapses
|
||||
@ -111,6 +112,7 @@ namespace NanoBrain {
|
||||
/// <summary>
|
||||
/// The type of combinator used for this Neuron
|
||||
/// </summary>
|
||||
[HideInInspector]
|
||||
public CombinatorType combinator = CombinatorType.Sum;
|
||||
|
||||
/// <summary>
|
||||
@ -130,6 +132,7 @@ namespace NanoBrain {
|
||||
/// The activation function
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
[HideInInspector]
|
||||
public ActivationType _activator;
|
||||
/// <summary>
|
||||
/// The activation funtion
|
||||
@ -138,145 +141,7 @@ namespace NanoBrain {
|
||||
get { return _activator; }
|
||||
set {
|
||||
_activator = value;
|
||||
this.curve = GenerateCurve();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// The curve representing the activation function
|
||||
/// </summary>
|
||||
public AnimationCurve curve;
|
||||
/// <summary>
|
||||
/// The maximum value of the curve
|
||||
/// </summary>
|
||||
public float curveMax = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Generate the curve for the current activation function
|
||||
/// </summary>
|
||||
/// <returns>The curve </returns>
|
||||
public AnimationCurve GenerateCurve() {
|
||||
switch (this.activator) {
|
||||
case ActivationType.Linear:
|
||||
this.curveMax = 1;
|
||||
return Presets.Linear(1);
|
||||
case ActivationType.Power:
|
||||
this.curveMax = 1;
|
||||
return Presets.Power(2.0f, 1);
|
||||
case ActivationType.Sqrt:
|
||||
this.curveMax = 1;
|
||||
return Presets.Power(0.5f, 1);
|
||||
case ActivationType.Reciprocal:
|
||||
this.curveMax = 1 / 0.01f * 1;
|
||||
return Presets.Reciprocal(1);
|
||||
case ActivationType.Tanh:
|
||||
this.curveMax = 1;
|
||||
return Presets.Tanh(1);
|
||||
case ActivationType.Binary:
|
||||
this.curveMax = 1;
|
||||
return Presets.Binary();
|
||||
case ActivationType.Normalized:
|
||||
this.curveMax = 1;
|
||||
return Presets.Binary();
|
||||
default:
|
||||
this.curveMax = 1;
|
||||
return this.curve;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The curve presets for the activation functions
|
||||
/// </summary>
|
||||
public static class Presets {
|
||||
/// <summary>
|
||||
/// The number of samples in the curve
|
||||
/// </summary>
|
||||
private const int samples = 32;
|
||||
/// <summary>
|
||||
/// Generate a curve for the linear activation function
|
||||
/// </summary>
|
||||
/// <param name="weight">The maximum value for the function</param>
|
||||
/// <returns>The curve preset</returns>
|
||||
public static AnimationCurve Linear(float weight) {
|
||||
return AnimationCurve.Linear(0f, 0f, 1000f, weight * 1000);
|
||||
}
|
||||
/// <summary>
|
||||
/// Generate a curve for the power activation function
|
||||
/// </summary>
|
||||
/// <param name="exponent">The exponent of the power function</param>
|
||||
/// <param name="weight">The maximum value for the function</param>
|
||||
/// <returns>The curve preset</returns>
|
||||
public static AnimationCurve Power(float exponent, float weight) {
|
||||
// build keyframes
|
||||
Keyframe[] keys = new Keyframe[samples];
|
||||
for (int i = 0; i < samples; i++) {
|
||||
float t = i / (float)(samples - 1);
|
||||
float v = Mathf.Pow(t, exponent) * weight;
|
||||
keys[i] = new Keyframe(t, v);
|
||||
}
|
||||
|
||||
AnimationCurve curve = new(keys);
|
||||
|
||||
// set tangent modes for each key to Auto (smooth). Use Linear if you prefer straight segments.
|
||||
for (int i = 0; i < curve.length; i++) {
|
||||
AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.Auto);
|
||||
AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.Auto);
|
||||
}
|
||||
|
||||
return curve;
|
||||
}
|
||||
/// <summary>
|
||||
/// Generate a curve for the reciprocal activation function
|
||||
/// </summary>
|
||||
/// <param name="weight">The maximum value for the function</param>
|
||||
/// <returns>The curve preset</returns>
|
||||
public static AnimationCurve Reciprocal(float weight) {
|
||||
int samples = 128;
|
||||
float xMin = 0.001f;
|
||||
float xMax = 1;
|
||||
var keys = new Keyframe[samples];
|
||||
for (int i = 0; i < samples; i++) {
|
||||
float t = i / (float)(samples - 1);
|
||||
float x = Mathf.Lerp(xMin, xMax, t);
|
||||
float y = 1f / x * weight;
|
||||
keys[i] = new Keyframe(x, y);
|
||||
}
|
||||
var curve = new AnimationCurve(keys);
|
||||
for (int i = 0; i < curve.length; i++) {
|
||||
AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.Linear);
|
||||
AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.Linear);
|
||||
}
|
||||
return curve;
|
||||
}
|
||||
/// <summary>
|
||||
/// Generate a curve for the tanh activation function
|
||||
/// </summary>
|
||||
/// <param name="weight">The maximum value for the function</param>
|
||||
/// <returns>The curve preset</returns>
|
||||
public static AnimationCurve Tanh(float weight) {
|
||||
//int samples = 128;
|
||||
float xMin = 0.001f;
|
||||
float xMax = 1;
|
||||
var keys = new Keyframe[samples];
|
||||
for (int i = 0; i < samples; i++) {
|
||||
float t = i / (float)(samples - 1);
|
||||
float x = Mathf.Lerp(xMin, xMax, t);
|
||||
float y = MathF.Tanh(x * weight);
|
||||
keys[i] = new Keyframe(x, y);
|
||||
}
|
||||
var curve = new AnimationCurve(keys);
|
||||
for (int i = 0; i < curve.length; i++) {
|
||||
AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.Linear);
|
||||
AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.Linear);
|
||||
}
|
||||
return curve;
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// Generate a curve for the binary activation function
|
||||
/// </summary>
|
||||
/// <returns>The curve preset</returns>
|
||||
public static AnimationCurve Binary() {
|
||||
return AnimationCurve.Linear(0, 0, 1, 1);
|
||||
//this.curve = GenerateCurve();
|
||||
}
|
||||
}
|
||||
|
||||
@ -287,6 +152,7 @@ namespace NanoBrain {
|
||||
/// <summary>
|
||||
/// The output value of the neuron
|
||||
/// </summary>
|
||||
[HideInInspector]
|
||||
protected float3 _outputValue;
|
||||
/// <summary>
|
||||
/// The output value of the neuron
|
||||
@ -351,13 +217,13 @@ namespace NanoBrain {
|
||||
/// <summary>
|
||||
/// True when the neuron is not persisting and has not be updated for timeToSleep seconds
|
||||
/// </summary>
|
||||
public virtual bool isSleeping => !persistOutput && (Time.time - this.lastUpdate > this.timeToSleep);
|
||||
public virtual bool isSleeping => !persistOutput && (Time.time - this.lastUpdate > timeToSleep);
|
||||
/// <summary>
|
||||
/// Check if the neuron is sleeping.
|
||||
/// </summary>
|
||||
/// This will reset the output value if it is sleeping
|
||||
public void SleepCheck() {
|
||||
if (this.isSleeping) {
|
||||
if (this.isSleeping && this.outputSqrMagnitude > 0) {
|
||||
#if UNITY_MATHEMATICS
|
||||
this._outputValue = new float3(0, 0, 0);
|
||||
#else
|
||||
@ -369,11 +235,14 @@ namespace NanoBrain {
|
||||
/// <summary>
|
||||
/// The time at which the last update has been done
|
||||
/// </summary>
|
||||
[HideInInspector]
|
||||
public float lastUpdate = 0;
|
||||
/// <summary>
|
||||
/// Time in seconds after the last update the neuron can go to sleep
|
||||
/// </summary>
|
||||
public readonly float timeToSleep = 1f;
|
||||
public static readonly float timeToSleep = 0.5f;
|
||||
|
||||
public bool breakOnUpdate = false;
|
||||
|
||||
/// \copydoc NanoBrain::Nucleus::ShallowCloneTo
|
||||
public override Nucleus ShallowCloneTo(Cluster parent) {
|
||||
@ -392,9 +261,8 @@ namespace NanoBrain {
|
||||
clone.bias = this.bias;
|
||||
clone.persistOutput = this.persistOutput;
|
||||
clone.combinator = this.combinator;
|
||||
clone.curve = this.curve;
|
||||
clone.activator = this.activator;
|
||||
clone.curveMax = this.curveMax;
|
||||
clone.breakOnUpdate = this.breakOnUpdate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -445,6 +313,9 @@ namespace NanoBrain {
|
||||
|
||||
/// \copydoc NanoBrain::Nucleus::UpdateStateIsolated
|
||||
public override void UpdateStateIsolated() {
|
||||
if (breakOnUpdate) {
|
||||
Debug.Break();
|
||||
}
|
||||
var combination = Combinator(this.bias, this.synapses);
|
||||
this.outputValue = Activator(combination);
|
||||
this.lastUpdate = Time.time;
|
||||
@ -731,6 +602,7 @@ namespace NanoBrain {
|
||||
/// The nuclei which have a synapse to this neuron
|
||||
/// </summary>
|
||||
[SerializeReference]
|
||||
[HideInInspector]
|
||||
private List<Nucleus> _receivers = new();
|
||||
/// <summary>
|
||||
/// The nuclei which have a synapse to this neuron
|
||||
|
||||
@ -14,12 +14,14 @@ namespace NanoBrain {
|
||||
/// <summary>
|
||||
/// The name of the Nucleus
|
||||
/// </summary>
|
||||
[HideInInspector]
|
||||
public string name;
|
||||
|
||||
/// <summary>
|
||||
/// The cluster instance in which the nucleus is located
|
||||
/// </summary>
|
||||
[SerializeReference]
|
||||
[HideInInspector]
|
||||
public Cluster parent;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -14,14 +14,8 @@ namespace NanoBrain.Unity {
|
||||
/// </summary>
|
||||
public Cluster cluster;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a nucleus in this cluster
|
||||
/// </summary>
|
||||
/// <param name="nucleusName">The name of the nucleus</param>
|
||||
/// <returns>The Nucleus with the given name or null if no such Nucleus could be found</returns>
|
||||
public Nucleus GetNucleus(string nucleusName) {
|
||||
return cluster.GetNucleus(nucleusName);
|
||||
}
|
||||
//[HideInInspector]
|
||||
public int version;
|
||||
|
||||
/// <summary>
|
||||
/// Call this function to ensure that there is at least one nucleus
|
||||
@ -36,6 +30,12 @@ namespace NanoBrain.Unity {
|
||||
new Neuron(this.cluster, "Output"); // Every cluster should have at least 1 neuron
|
||||
this.cluster.instanceCount = 1;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnValidate() {
|
||||
version++;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ namespace NanoBrain.Braitenberg {
|
||||
protected override float SampleSensor() {
|
||||
float sum = 0f;
|
||||
// Get all active lights in scene (Point lights only)
|
||||
Light[] lights = FindObjectsByType<Light>();
|
||||
Light[] lights = FindObjectsByType<Light>(FindObjectsSortMode.None);
|
||||
Vector3 pos = transform.position;
|
||||
Vector3 forward = transform.forward;
|
||||
|
||||
|
||||
@ -1,2 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fbdba2c00e2271d7eae755fa49a7958c
|
||||
guid: fbdba2c00e2271d7eae755fa49a7958c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@ -10,7 +10,7 @@ namespace NanoBrain.Braitenberg {
|
||||
|
||||
public WheelCollider wheelCollider;
|
||||
|
||||
protected ClusterPrefab brain;
|
||||
protected Cluster brain;
|
||||
public Neuron motorNeuron;
|
||||
|
||||
protected virtual void Awake() {
|
||||
@ -18,7 +18,7 @@ namespace NanoBrain.Braitenberg {
|
||||
if (vehicle != null)
|
||||
brain = vehicle.brain;
|
||||
if (brain != null)
|
||||
motorNeuron = brain.GetNucleus(outputNeuronName) as Neuron;
|
||||
motorNeuron = brain.GetNeuron(outputNeuronName);
|
||||
wheelCollider = GetComponent<WheelCollider>();
|
||||
}
|
||||
|
||||
|
||||
@ -1,2 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 07c6bf9674b9f9f0bbbf4a37f570ef4d
|
||||
guid: 07c6bf9674b9f9f0bbbf4a37f570ef4d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@ -25,7 +25,7 @@ namespace NanoBrain.Braitenberg {
|
||||
public float _output;
|
||||
|
||||
protected Vehicle vehicle;
|
||||
protected ClusterPrefab brain;
|
||||
protected Cluster brain;
|
||||
public Neuron sensoryNeuron;
|
||||
|
||||
protected virtual void Awake() {
|
||||
@ -34,7 +34,7 @@ namespace NanoBrain.Braitenberg {
|
||||
if (vehicle != null)
|
||||
brain = vehicle.brain;
|
||||
if (brain != null)
|
||||
sensoryNeuron = brain.GetNucleus(this.name) as Neuron;
|
||||
sensoryNeuron = brain.GetNeuron(this.name);
|
||||
}
|
||||
|
||||
void OnEnable() => StartCoroutine(SampleRoutine());
|
||||
|
||||
@ -1,2 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b3d0457beca5df1bba076ac65df90b59
|
||||
guid: b3d0457beca5df1bba076ac65df90b59
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@ -4,7 +4,7 @@ namespace NanoBrain.Braitenberg {
|
||||
|
||||
[RequireComponent(typeof(Rigidbody))]
|
||||
public class Vehicle : MonoBehaviour {
|
||||
public Unity.ClusterPrefab brain;
|
||||
public Cluster brain;
|
||||
|
||||
[Header("Motors")]
|
||||
public Motor motorLeft;
|
||||
|
||||
@ -1,2 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a8d53357b6673864d91bbc5c595d48b9
|
||||
guid: a8d53357b6673864d91bbc5c595d48b9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user