Compare commits

..

No commits in common. "133804a154650e543cf9a8db553a20193e41a894" and "37261bdce63aef42e9f984837e0b1cab671573b3" have entirely different histories.

29 changed files with 1568 additions and 532 deletions

75
Editor/Brain_Editor.cs Normal file
View File

@ -0,0 +1,75 @@
/*
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;
}
}
}
*/

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 18e075a03ca2efdb2895079f63eb333a
guid: f05072314d39990639a2dbf99f322664
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@ -76,15 +76,8 @@ namespace NanoBrain.Unity {
margin = new RectOffset(10, 0, 4, 4)
};
// Nucleus type
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);
string nucleusType = this.view.currentNucleus.GetType().Name;
GUILayout.Label(nucleusType, headerStyle);
// Nucleus name
string newName = EditorGUILayout.TextField(this.view.currentNucleus.name, boldTextFieldStyle);
@ -311,10 +304,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;

View File

@ -0,0 +1,129 @@
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;
}
}
*/
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5f43b401b7d59dec7ac7d493cbc4927d

View File

@ -7,13 +7,6 @@ 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;
@ -24,17 +17,17 @@ namespace NanoBrain.Unity {
}
public Mode mode = Mode.Focus;
public static readonly Dictionary<string, ClusterView> clusterViews = new();
static readonly Dictionary<string, ClusterView> clusterViews = new();
public static ClusterView GetClusterView(SerializedProperty property) {
string key = property.propertyPath + "_" + property.serializedObject.targetObject.GetInstanceID();//GetEntityId();
string key = property.propertyPath + "_" + property.serializedObject.targetObject.GetEntityId();
if (!clusterViews.TryGetValue(key, out ClusterView clusterView))
clusterView = new(key);// { key = key };
clusterView = new() { key = key };
return clusterView;
}
public static ClusterView GetClusterView(SerializedObject serializedObject) {
string key = serializedObject.targetObject.GetInstanceID().ToString(); //GetEntityId().ToString();
string key = serializedObject.targetObject.GetEntityId().ToString();
if (!clusterViews.TryGetValue(key, out ClusterView clusterView))
clusterView = new(key); // { key = key };
clusterView = new() { key = key };
return clusterView;
}
@ -44,7 +37,7 @@ namespace NanoBrain.Unity {
public static void Render(Rect drawRect, Cluster cluster, SerializedProperty property) {
ClusterView clusterView = GetClusterView(property);
if (clusterView.currentCluster == null || clusterView.currentCluster != cluster) {
if (clusterView.currentCluster == null) {
clusterView.currentCluster = cluster;
clusterView.currentNucleus = cluster.defaultOutput;
clusterView.selectedOutput = clusterView.currentNucleus;
@ -53,7 +46,7 @@ namespace NanoBrain.Unity {
}
public static void Render(Rect drawRect, Cluster cluster, SerializedObject obj) {
ClusterView clusterView = GetClusterView(obj);
if (clusterView.currentCluster == null || clusterView.currentCluster != cluster) {
if (clusterView.currentCluster == null) {
clusterView.currentCluster = cluster;
clusterView.currentNucleus = cluster.defaultOutput;
clusterView.selectedOutput = clusterView.currentNucleus;
@ -108,8 +101,6 @@ 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
@ -208,7 +199,7 @@ namespace NanoBrain.Unity {
DrawOutputs(position);
return;
}
Dag dag = GenerateGraph(this.selectedOutput);
Dag.ComputeLayout(dag);
@ -276,9 +267,6 @@ 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) {
@ -319,7 +307,7 @@ namespace NanoBrain.Unity {
if (nucleus == this.selectedOutput) {
// Add link to 'Outpus'
nodeCount++;
if (ClusterView.previousPrefab != null && ClusterView.previousPrefab != nucleus.parent.prefab)
if (ClusterViewer.previousPrefab != null)
// Add link to previous editor
nodeCount++;
}
@ -353,9 +341,9 @@ namespace NanoBrain.Unity {
}
if (nucleus == this.selectedOutput) {
Vector3 pos = new(50, margin + row * spacing, 0);
if (ClusterView.previousPrefab != null && ClusterView.previousPrefab != nucleus.parent.prefab) {
if (ClusterViewer.previousPrefab != null) {
DrawEdge(parentPos, pos);
DrawClusterPrefab(ClusterView.previousPrefab, pos);
DrawClusterPrefab(ClusterViewer.previousPrefab, pos);
row++;
}
pos = new(50, margin + row * spacing, 0);
@ -445,7 +433,7 @@ namespace NanoBrain.Unity {
float maxValue = 0;
foreach (Cluster sibling in nucleus.parent.instances) {
Neuron siblingNeuron = sibling.GetNeuron(nucleus.name);
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;
@ -456,10 +444,9 @@ 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.GetNeuron(nucleus.name);
position = new(250, margin + row * spacing, 0.0f);
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) {
@ -469,16 +456,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,
};
Handles.Label(labelPos, name, style);
Vector3 labelPos = position - Vector3.down * (discRadius + 5); // below neuron
string name = $"{sibling.baseName}\n{nucleus.name}";
Handles.Label(labelPos, name, style);
row++;
}
expandArray = false;
}
@ -536,6 +523,7 @@ namespace NanoBrain.Unity {
}
}
protected void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue) {
maxValue = 1;
Color color;
@ -550,6 +538,7 @@ namespace NanoBrain.Unity {
DrawNucleus(nucleus, position, color);
}
protected void DrawNucleus(Nucleus nucleus, Vector2 position, Color color) {
if (nucleus == null)
return;
@ -707,7 +696,7 @@ namespace NanoBrain.Unity {
e.Use();
Selection.activeObject = prefab;
EditorGUIUtility.PingObject(prefab);
ClusterView.previousPrefab = null;
ClusterViewer.previousPrefab = null;
Editor.CreateEditor(prefab);
}
}
@ -773,16 +762,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
@ -803,7 +792,6 @@ namespace NanoBrain.Unity {
else {
// select the cluster, not the neuron in the cluster
this.currentNucleus = nucleus.parent;
this.selectedSynapseNeuron = null;
this.expandArray = false;
}
}
@ -811,7 +799,6 @@ 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;
}
}
@ -820,7 +807,7 @@ namespace NanoBrain.Unity {
// May be used with storedPrefab...
Selection.activeObject = subCluster.prefab;
EditorGUIUtility.PingObject(subCluster.prefab);
ClusterView.previousPrefab = this.currentCluster.prefab;
ClusterViewer.previousPrefab = this.currentCluster.prefab;
Editor.CreateEditor(subCluster.prefab);
}

View File

@ -1,11 +1,2 @@
fileFormatVersion: 2
guid: 8a7663ccd347fd78dbdba393c03ed7c7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
guid: 8a7663ccd347fd78dbdba393c03ed7c7

902
Editor/ClusterViewer.cs Normal file
View File

@ -0,0 +1,902 @@
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();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4fe58945c76d153edacc220597474ad2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,308 +0,0 @@
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;
}
}
}

View File

@ -1,11 +1,2 @@
fileFormatVersion: 2
guid: a755ac8461bd0c714a852df47331048e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
guid: a755ac8461bd0c714a852df47331048e

85
Editor/Neuron_Drawer.cs Normal file
View File

@ -0,0 +1,85 @@
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;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: aa0e340763ca6299e93d514b271ae38d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

69
Runtime/Scripts/Brain.cs Normal file
View File

@ -0,0 +1,69 @@
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}");
}
}
}
}
}
}
*/

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 92f34a5e4027a1dc39efd8ce63cf6aba
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -24,9 +24,6 @@ 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>
@ -45,9 +42,7 @@ 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]
[SerializeReference]
[HideInInspector]
[NonSerialized]
public Cluster[] instances;
/// <summary>
@ -67,8 +62,11 @@ 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
@ -79,12 +77,13 @@ 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>
@ -94,12 +93,13 @@ 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,10 +108,6 @@ 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
@ -165,30 +161,36 @@ namespace NanoBrain {
}
}
foreach (Nucleus clonedNucleus in clonedNuclei) {
if (clonedNucleus is not Cluster clonedCluster)
continue;
if (Application.isPlaying) {
// Only create cluster siblings at runtime
foreach (Nucleus clonedNucleus in clonedNuclei) {
if (clonedNucleus is not Cluster clonedCluster)
continue;
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,
List<Cluster> siblings = new() {
clonedCluster
};
siblings.Add(sibling);
CopyAllExternalReceivers(clonedCluster, sibling, this);
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;
}
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();
}
}
}
@ -200,11 +202,13 @@ 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, Cluster clonedParent) {
private static void CopyAllExternalReceivers(Cluster sourceCluster, Cluster sibling, ClusterPrefab prefabParent, Cluster clonedParent) {
for (int nucleusIx = 0; nucleusIx < sourceCluster.nuclei.Count; nucleusIx++) {
Nucleus sourceNucleus = sourceCluster.nuclei[nucleusIx];
if (sourceNucleus is not Neuron sourceNeuron)
@ -233,7 +237,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()}");
}
}
@ -456,7 +460,7 @@ namespace NanoBrain {
return null;
}
}
/// <summary>
/// The neurons without outgoing connections
/// </summary>
@ -496,8 +500,6 @@ 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;
}
@ -507,56 +509,52 @@ namespace NanoBrain {
}
/// <summary>
/// Get a neuron in this cluster
/// Get a nucleus 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) {
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('.');
/// <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('.');
if (dotPosition >= 0) {
string clusterBaseName = clusterName[..dotPosition];
string clusterName0 = clusterBaseName + ": 0";
string clusterName = nucleusName[..dotPosition];
string clusterName0 = clusterName + ": 0";
foreach (Nucleus nucleus in this.nuclei) {
if (nucleus is Cluster cluster) {
if (cluster.name == clusterBaseName || cluster.name == clusterName0) {
// cluster.CheckInstances();
string subNucleusName = clusterName[(dotPosition + 1)..];
return cluster.GetCluster(subNucleusName);
if (cluster.name == clusterName || cluster.name == clusterName0) {
string subNucleusName = nucleusName[(dotPosition + 1)..];
return cluster.GetNucleus(subNucleusName);
}
}
}
return null;
}
else {
string nucleusName0 = clusterName + ": 0";
string nucleusName0 = nucleusName + ": 0";
foreach (Nucleus nucleus in this.nuclei) {
if (nucleus is Cluster cluster) {
if (nucleus.name == clusterName || nucleus.name == nucleusName0) {
// cluster.CheckInstances();
return cluster;
}
if (nucleus is Cluster) {
if (nucleus.name == nucleusName || nucleus.name == nucleusName0)
return nucleus;
}
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>
@ -570,7 +568,6 @@ 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);

View File

@ -42,7 +42,6 @@ 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
@ -112,7 +111,6 @@ namespace NanoBrain {
/// <summary>
/// The type of combinator used for this Neuron
/// </summary>
[HideInInspector]
public CombinatorType combinator = CombinatorType.Sum;
/// <summary>
@ -132,7 +130,6 @@ namespace NanoBrain {
/// The activation function
/// </summary>
[SerializeField]
[HideInInspector]
public ActivationType _activator;
/// <summary>
/// The activation funtion
@ -141,7 +138,145 @@ namespace NanoBrain {
get { return _activator; }
set {
_activator = value;
//this.curve = GenerateCurve();
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);
}
}
@ -152,7 +287,6 @@ namespace NanoBrain {
/// <summary>
/// The output value of the neuron
/// </summary>
[HideInInspector]
protected float3 _outputValue;
/// <summary>
/// The output value of the neuron
@ -217,13 +351,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 > timeToSleep);
public virtual bool isSleeping => !persistOutput && (Time.time - this.lastUpdate > this.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 && this.outputSqrMagnitude > 0) {
if (this.isSleeping) {
#if UNITY_MATHEMATICS
this._outputValue = new float3(0, 0, 0);
#else
@ -235,14 +369,11 @@ 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 static readonly float timeToSleep = 0.5f;
public bool breakOnUpdate = false;
public readonly float timeToSleep = 1f;
/// \copydoc NanoBrain::Nucleus::ShallowCloneTo
public override Nucleus ShallowCloneTo(Cluster parent) {
@ -261,8 +392,9 @@ namespace NanoBrain {
clone.bias = this.bias;
clone.persistOutput = this.persistOutput;
clone.combinator = this.combinator;
clone.curve = this.curve;
clone.activator = this.activator;
clone.breakOnUpdate = this.breakOnUpdate;
clone.curveMax = this.curveMax;
}
/// <summary>
@ -313,9 +445,6 @@ 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;
@ -602,7 +731,6 @@ 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

View File

@ -14,14 +14,12 @@ 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>

View File

@ -14,8 +14,14 @@ namespace NanoBrain.Unity {
/// </summary>
public Cluster cluster;
//[HideInInspector]
public int version;
/// <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);
}
/// <summary>
/// Call this function to ensure that there is at least one nucleus
@ -30,12 +36,6 @@ 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
}
}

View File

@ -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>(FindObjectsSortMode.None);
Light[] lights = FindObjectsByType<Light>();
Vector3 pos = transform.position;
Vector3 forward = transform.forward;

View File

@ -1,11 +1,2 @@
fileFormatVersion: 2
guid: fbdba2c00e2271d7eae755fa49a7958c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
guid: fbdba2c00e2271d7eae755fa49a7958c

View File

@ -10,7 +10,7 @@ namespace NanoBrain.Braitenberg {
public WheelCollider wheelCollider;
protected Cluster brain;
protected ClusterPrefab 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.GetNeuron(outputNeuronName);
motorNeuron = brain.GetNucleus(outputNeuronName) as Neuron;
wheelCollider = GetComponent<WheelCollider>();
}

View File

@ -1,11 +1,2 @@
fileFormatVersion: 2
guid: 07c6bf9674b9f9f0bbbf4a37f570ef4d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
guid: 07c6bf9674b9f9f0bbbf4a37f570ef4d

View File

@ -25,7 +25,7 @@ namespace NanoBrain.Braitenberg {
public float _output;
protected Vehicle vehicle;
protected Cluster brain;
protected ClusterPrefab 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.GetNeuron(this.name);
sensoryNeuron = brain.GetNucleus(this.name) as Neuron;
}
void OnEnable() => StartCoroutine(SampleRoutine());

View File

@ -1,11 +1,2 @@
fileFormatVersion: 2
guid: b3d0457beca5df1bba076ac65df90b59
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
guid: b3d0457beca5df1bba076ac65df90b59

View File

@ -4,7 +4,7 @@ namespace NanoBrain.Braitenberg {
[RequireComponent(typeof(Rigidbody))]
public class Vehicle : MonoBehaviour {
public Cluster brain;
public Unity.ClusterPrefab brain;
[Header("Motors")]
public Motor motorLeft;

View File

@ -1,11 +1,2 @@
fileFormatVersion: 2
guid: a8d53357b6673864d91bbc5c595d48b9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
guid: a8d53357b6673864d91bbc5c595d48b9