From 18ef4cd7665edf993014883c7bdac44199cf05c6 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Tue, 14 Apr 2026 14:43:48 +0200 Subject: [PATCH 01/34] Merge commit '89017475984bbbf1899fb38846c5bb0e7775dedd' into NanoBrain --- LinearAlgebra/LinearAlgebra-csharp.sln.meta | 7 +++++++ LinearAlgebra/src.meta | 8 ++++++++ LinearAlgebra/src/Angle.cs.meta | 2 ++ LinearAlgebra/src/Decomposition.cs.meta | 2 ++ LinearAlgebra/src/Direction.cs.meta | 2 ++ LinearAlgebra/src/Float.cs.meta | 2 ++ LinearAlgebra/src/LinearAlgebra.csproj.meta | 7 +++++++ LinearAlgebra/src/Matrix.cs.meta | 2 ++ LinearAlgebra/src/Quat32.cs.meta | 2 ++ LinearAlgebra/src/Quaternion.cs.meta | 2 ++ LinearAlgebra/src/Spherical.cs.meta | 2 ++ LinearAlgebra/src/SwingTwist.cs.meta | 2 ++ LinearAlgebra/src/Vector2Float.cs.meta | 2 ++ LinearAlgebra/src/Vector2Int.cs.meta | 2 ++ LinearAlgebra/src/Vector3Float.cs.meta | 2 ++ LinearAlgebra/src/Vector3Int.cs.meta | 2 ++ LinearAlgebra/src/float16.cs.meta | 2 ++ LinearAlgebra/test.meta | 8 ++++++++ LinearAlgebra/test/AngleTest.cs.meta | 2 ++ LinearAlgebra/test/DirectionTest.cs.meta | 2 ++ LinearAlgebra/test/LinearAlgebra_Test.csproj.meta | 7 +++++++ LinearAlgebra/test/QuaternionTest.cs.meta | 2 ++ LinearAlgebra/test/SphericalTest.cs.meta | 2 ++ LinearAlgebra/test/SwingTwistTest.cs.meta | 2 ++ LinearAlgebra/test/Vector2FloatTest.cs.meta | 2 ++ LinearAlgebra/test/Vector2IntTest.cs.meta | 2 ++ LinearAlgebra/test/Vector3FloatTest.cs.meta | 2 ++ LinearAlgebra/test/Vector3IntTest.cs.meta | 2 ++ 28 files changed, 83 insertions(+) create mode 100644 LinearAlgebra/LinearAlgebra-csharp.sln.meta create mode 100644 LinearAlgebra/src.meta create mode 100644 LinearAlgebra/src/Angle.cs.meta create mode 100644 LinearAlgebra/src/Decomposition.cs.meta create mode 100644 LinearAlgebra/src/Direction.cs.meta create mode 100644 LinearAlgebra/src/Float.cs.meta create mode 100644 LinearAlgebra/src/LinearAlgebra.csproj.meta create mode 100644 LinearAlgebra/src/Matrix.cs.meta create mode 100644 LinearAlgebra/src/Quat32.cs.meta create mode 100644 LinearAlgebra/src/Quaternion.cs.meta create mode 100644 LinearAlgebra/src/Spherical.cs.meta create mode 100644 LinearAlgebra/src/SwingTwist.cs.meta create mode 100644 LinearAlgebra/src/Vector2Float.cs.meta create mode 100644 LinearAlgebra/src/Vector2Int.cs.meta create mode 100644 LinearAlgebra/src/Vector3Float.cs.meta create mode 100644 LinearAlgebra/src/Vector3Int.cs.meta create mode 100644 LinearAlgebra/src/float16.cs.meta create mode 100644 LinearAlgebra/test.meta create mode 100644 LinearAlgebra/test/AngleTest.cs.meta create mode 100644 LinearAlgebra/test/DirectionTest.cs.meta create mode 100644 LinearAlgebra/test/LinearAlgebra_Test.csproj.meta create mode 100644 LinearAlgebra/test/QuaternionTest.cs.meta create mode 100644 LinearAlgebra/test/SphericalTest.cs.meta create mode 100644 LinearAlgebra/test/SwingTwistTest.cs.meta create mode 100644 LinearAlgebra/test/Vector2FloatTest.cs.meta create mode 100644 LinearAlgebra/test/Vector2IntTest.cs.meta create mode 100644 LinearAlgebra/test/Vector3FloatTest.cs.meta create mode 100644 LinearAlgebra/test/Vector3IntTest.cs.meta diff --git a/LinearAlgebra/LinearAlgebra-csharp.sln.meta b/LinearAlgebra/LinearAlgebra-csharp.sln.meta new file mode 100644 index 0000000..63fb374 --- /dev/null +++ b/LinearAlgebra/LinearAlgebra-csharp.sln.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c4268c3b21e354ec3ae370ec539d8ee3 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LinearAlgebra/src.meta b/LinearAlgebra/src.meta new file mode 100644 index 0000000..6476da7 --- /dev/null +++ b/LinearAlgebra/src.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6a602cec2c4009925b1d19ed36a98c6a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LinearAlgebra/src/Angle.cs.meta b/LinearAlgebra/src/Angle.cs.meta new file mode 100644 index 0000000..09d19a5 --- /dev/null +++ b/LinearAlgebra/src/Angle.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a2f41e870e2eef1d39f90b1848619436 \ No newline at end of file diff --git a/LinearAlgebra/src/Decomposition.cs.meta b/LinearAlgebra/src/Decomposition.cs.meta new file mode 100644 index 0000000..3a3ea8c --- /dev/null +++ b/LinearAlgebra/src/Decomposition.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8dd93a11ee49c67379fd32231a215f91 \ No newline at end of file diff --git a/LinearAlgebra/src/Direction.cs.meta b/LinearAlgebra/src/Direction.cs.meta new file mode 100644 index 0000000..b3d75f2 --- /dev/null +++ b/LinearAlgebra/src/Direction.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a1105b4168c2d27439a98db639de90ad \ No newline at end of file diff --git a/LinearAlgebra/src/Float.cs.meta b/LinearAlgebra/src/Float.cs.meta new file mode 100644 index 0000000..9dcf397 --- /dev/null +++ b/LinearAlgebra/src/Float.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 91a98066243cf684e92cac7e10eca327 \ No newline at end of file diff --git a/LinearAlgebra/src/LinearAlgebra.csproj.meta b/LinearAlgebra/src/LinearAlgebra.csproj.meta new file mode 100644 index 0000000..d02f9ab --- /dev/null +++ b/LinearAlgebra/src/LinearAlgebra.csproj.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d0824c1277d5a085c8f666fc6e655888 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LinearAlgebra/src/Matrix.cs.meta b/LinearAlgebra/src/Matrix.cs.meta new file mode 100644 index 0000000..475ac0c --- /dev/null +++ b/LinearAlgebra/src/Matrix.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bb3810889269ae287b19e0e76face9cd \ No newline at end of file diff --git a/LinearAlgebra/src/Quat32.cs.meta b/LinearAlgebra/src/Quat32.cs.meta new file mode 100644 index 0000000..bccb1e3 --- /dev/null +++ b/LinearAlgebra/src/Quat32.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 586960852781b8e8db3a204813001eea \ No newline at end of file diff --git a/LinearAlgebra/src/Quaternion.cs.meta b/LinearAlgebra/src/Quaternion.cs.meta new file mode 100644 index 0000000..e2c5950 --- /dev/null +++ b/LinearAlgebra/src/Quaternion.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6d91f32cb61c1a419839b25859bed632 \ No newline at end of file diff --git a/LinearAlgebra/src/Spherical.cs.meta b/LinearAlgebra/src/Spherical.cs.meta new file mode 100644 index 0000000..ac32bd9 --- /dev/null +++ b/LinearAlgebra/src/Spherical.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 787b6a91147861301bbe87ad8e285069 \ No newline at end of file diff --git a/LinearAlgebra/src/SwingTwist.cs.meta b/LinearAlgebra/src/SwingTwist.cs.meta new file mode 100644 index 0000000..adcca29 --- /dev/null +++ b/LinearAlgebra/src/SwingTwist.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 025033ab8a1b8ad0f80f69017322e3aa \ No newline at end of file diff --git a/LinearAlgebra/src/Vector2Float.cs.meta b/LinearAlgebra/src/Vector2Float.cs.meta new file mode 100644 index 0000000..48b0a03 --- /dev/null +++ b/LinearAlgebra/src/Vector2Float.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 55826088c0deb60c68b6ad760d2deac3 \ No newline at end of file diff --git a/LinearAlgebra/src/Vector2Int.cs.meta b/LinearAlgebra/src/Vector2Int.cs.meta new file mode 100644 index 0000000..9d5849b --- /dev/null +++ b/LinearAlgebra/src/Vector2Int.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 09113558bbf8fca1fb15dfea26e2f84e \ No newline at end of file diff --git a/LinearAlgebra/src/Vector3Float.cs.meta b/LinearAlgebra/src/Vector3Float.cs.meta new file mode 100644 index 0000000..12d2d45 --- /dev/null +++ b/LinearAlgebra/src/Vector3Float.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2fc7d49f736899f929849abdb7198c64 \ No newline at end of file diff --git a/LinearAlgebra/src/Vector3Int.cs.meta b/LinearAlgebra/src/Vector3Int.cs.meta new file mode 100644 index 0000000..9111e81 --- /dev/null +++ b/LinearAlgebra/src/Vector3Int.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: cbfd0f4db235b07808071761cb870341 \ No newline at end of file diff --git a/LinearAlgebra/src/float16.cs.meta b/LinearAlgebra/src/float16.cs.meta new file mode 100644 index 0000000..7c7cc58 --- /dev/null +++ b/LinearAlgebra/src/float16.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d0a40c05486e38668b208e7d24496037 \ No newline at end of file diff --git a/LinearAlgebra/test.meta b/LinearAlgebra/test.meta new file mode 100644 index 0000000..7b069c9 --- /dev/null +++ b/LinearAlgebra/test.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9b84f664459d02b90894e460de42c219 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LinearAlgebra/test/AngleTest.cs.meta b/LinearAlgebra/test/AngleTest.cs.meta new file mode 100644 index 0000000..d7f9e27 --- /dev/null +++ b/LinearAlgebra/test/AngleTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1fc9a3dab7a1d403bb35f344d1c14c4b \ No newline at end of file diff --git a/LinearAlgebra/test/DirectionTest.cs.meta b/LinearAlgebra/test/DirectionTest.cs.meta new file mode 100644 index 0000000..8180aac --- /dev/null +++ b/LinearAlgebra/test/DirectionTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 063c06dc57686a591b9bebb2fec6e0fb \ No newline at end of file diff --git a/LinearAlgebra/test/LinearAlgebra_Test.csproj.meta b/LinearAlgebra/test/LinearAlgebra_Test.csproj.meta new file mode 100644 index 0000000..f20fb3f --- /dev/null +++ b/LinearAlgebra/test/LinearAlgebra_Test.csproj.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: da3ceb471667a85d098212b0f045487c +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/LinearAlgebra/test/QuaternionTest.cs.meta b/LinearAlgebra/test/QuaternionTest.cs.meta new file mode 100644 index 0000000..0c7e9f0 --- /dev/null +++ b/LinearAlgebra/test/QuaternionTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9f445a5425b0a13c98edd71c50eccb42 \ No newline at end of file diff --git a/LinearAlgebra/test/SphericalTest.cs.meta b/LinearAlgebra/test/SphericalTest.cs.meta new file mode 100644 index 0000000..38d60ae --- /dev/null +++ b/LinearAlgebra/test/SphericalTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7e4c15b444612085b9600a77cc22ea5b \ No newline at end of file diff --git a/LinearAlgebra/test/SwingTwistTest.cs.meta b/LinearAlgebra/test/SwingTwistTest.cs.meta new file mode 100644 index 0000000..2cb700d --- /dev/null +++ b/LinearAlgebra/test/SwingTwistTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 26ebd10731635e83a89740f446a887b8 \ No newline at end of file diff --git a/LinearAlgebra/test/Vector2FloatTest.cs.meta b/LinearAlgebra/test/Vector2FloatTest.cs.meta new file mode 100644 index 0000000..c47339c --- /dev/null +++ b/LinearAlgebra/test/Vector2FloatTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7cbdb061bb22f4701b7cfeb62cb4b842 \ No newline at end of file diff --git a/LinearAlgebra/test/Vector2IntTest.cs.meta b/LinearAlgebra/test/Vector2IntTest.cs.meta new file mode 100644 index 0000000..c9eae74 --- /dev/null +++ b/LinearAlgebra/test/Vector2IntTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 779761c64b4d2321c8d43f70ba3c8597 \ No newline at end of file diff --git a/LinearAlgebra/test/Vector3FloatTest.cs.meta b/LinearAlgebra/test/Vector3FloatTest.cs.meta new file mode 100644 index 0000000..056fc0a --- /dev/null +++ b/LinearAlgebra/test/Vector3FloatTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 630da56ad3729422593994fbc4a53b00 \ No newline at end of file diff --git a/LinearAlgebra/test/Vector3IntTest.cs.meta b/LinearAlgebra/test/Vector3IntTest.cs.meta new file mode 100644 index 0000000..35a07a6 --- /dev/null +++ b/LinearAlgebra/test/Vector3IntTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c01e8107df0530b13bdb528e14bfed27 \ No newline at end of file From db4365527c8a3319480f6d686a26b22d9ac25a1d Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Tue, 14 Apr 2026 16:00:01 +0200 Subject: [PATCH 02/34] Pew pew! --- .../LinearAlgebra-csharp.sln.meta => LinearAlgebra.meta | 3 ++- LinearAlgebra/src/Angle.cs.meta | 2 -- LinearAlgebra/src/Decomposition.cs.meta | 2 -- LinearAlgebra/src/Direction.cs.meta | 2 -- LinearAlgebra/src/Float.cs.meta | 2 -- LinearAlgebra/src/LinearAlgebra.csproj.meta | 7 ------- LinearAlgebra/src/Matrix.cs.meta | 2 -- LinearAlgebra/src/Quat32.cs.meta | 2 -- LinearAlgebra/src/Quaternion.cs.meta | 2 -- LinearAlgebra/src/Spherical.cs.meta | 2 -- LinearAlgebra/src/SwingTwist.cs.meta | 2 -- LinearAlgebra/src/Vector2Float.cs.meta | 2 -- LinearAlgebra/src/Vector2Int.cs.meta | 2 -- LinearAlgebra/src/Vector3Float.cs.meta | 2 -- LinearAlgebra/src/Vector3Int.cs.meta | 2 -- LinearAlgebra/src/float16.cs.meta | 2 -- LinearAlgebra/test/AngleTest.cs.meta | 2 -- LinearAlgebra/test/DirectionTest.cs.meta | 2 -- LinearAlgebra/test/LinearAlgebra_Test.csproj.meta | 7 ------- LinearAlgebra/test/QuaternionTest.cs.meta | 2 -- LinearAlgebra/test/SphericalTest.cs.meta | 2 -- LinearAlgebra/test/SwingTwistTest.cs.meta | 2 -- LinearAlgebra/test/Vector2FloatTest.cs.meta | 2 -- LinearAlgebra/test/Vector2IntTest.cs.meta | 2 -- LinearAlgebra/test/Vector3FloatTest.cs.meta | 2 -- LinearAlgebra/test/Vector3IntTest.cs.meta | 2 -- 26 files changed, 2 insertions(+), 61 deletions(-) rename LinearAlgebra/LinearAlgebra-csharp.sln.meta => LinearAlgebra.meta (67%) delete mode 100644 LinearAlgebra/src/Angle.cs.meta delete mode 100644 LinearAlgebra/src/Decomposition.cs.meta delete mode 100644 LinearAlgebra/src/Direction.cs.meta delete mode 100644 LinearAlgebra/src/Float.cs.meta delete mode 100644 LinearAlgebra/src/LinearAlgebra.csproj.meta delete mode 100644 LinearAlgebra/src/Matrix.cs.meta delete mode 100644 LinearAlgebra/src/Quat32.cs.meta delete mode 100644 LinearAlgebra/src/Quaternion.cs.meta delete mode 100644 LinearAlgebra/src/Spherical.cs.meta delete mode 100644 LinearAlgebra/src/SwingTwist.cs.meta delete mode 100644 LinearAlgebra/src/Vector2Float.cs.meta delete mode 100644 LinearAlgebra/src/Vector2Int.cs.meta delete mode 100644 LinearAlgebra/src/Vector3Float.cs.meta delete mode 100644 LinearAlgebra/src/Vector3Int.cs.meta delete mode 100644 LinearAlgebra/src/float16.cs.meta delete mode 100644 LinearAlgebra/test/AngleTest.cs.meta delete mode 100644 LinearAlgebra/test/DirectionTest.cs.meta delete mode 100644 LinearAlgebra/test/LinearAlgebra_Test.csproj.meta delete mode 100644 LinearAlgebra/test/QuaternionTest.cs.meta delete mode 100644 LinearAlgebra/test/SphericalTest.cs.meta delete mode 100644 LinearAlgebra/test/SwingTwistTest.cs.meta delete mode 100644 LinearAlgebra/test/Vector2FloatTest.cs.meta delete mode 100644 LinearAlgebra/test/Vector2IntTest.cs.meta delete mode 100644 LinearAlgebra/test/Vector3FloatTest.cs.meta delete mode 100644 LinearAlgebra/test/Vector3IntTest.cs.meta diff --git a/LinearAlgebra/LinearAlgebra-csharp.sln.meta b/LinearAlgebra.meta similarity index 67% rename from LinearAlgebra/LinearAlgebra-csharp.sln.meta rename to LinearAlgebra.meta index 63fb374..d18b73b 100644 --- a/LinearAlgebra/LinearAlgebra-csharp.sln.meta +++ b/LinearAlgebra.meta @@ -1,5 +1,6 @@ fileFormatVersion: 2 -guid: c4268c3b21e354ec3ae370ec539d8ee3 +guid: a4c7dfe43bdf504e29c5c97919d7a1c0 +folderAsset: yes DefaultImporter: externalObjects: {} userData: diff --git a/LinearAlgebra/src/Angle.cs.meta b/LinearAlgebra/src/Angle.cs.meta deleted file mode 100644 index 09d19a5..0000000 --- a/LinearAlgebra/src/Angle.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: a2f41e870e2eef1d39f90b1848619436 \ No newline at end of file diff --git a/LinearAlgebra/src/Decomposition.cs.meta b/LinearAlgebra/src/Decomposition.cs.meta deleted file mode 100644 index 3a3ea8c..0000000 --- a/LinearAlgebra/src/Decomposition.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 8dd93a11ee49c67379fd32231a215f91 \ No newline at end of file diff --git a/LinearAlgebra/src/Direction.cs.meta b/LinearAlgebra/src/Direction.cs.meta deleted file mode 100644 index b3d75f2..0000000 --- a/LinearAlgebra/src/Direction.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: a1105b4168c2d27439a98db639de90ad \ No newline at end of file diff --git a/LinearAlgebra/src/Float.cs.meta b/LinearAlgebra/src/Float.cs.meta deleted file mode 100644 index 9dcf397..0000000 --- a/LinearAlgebra/src/Float.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 91a98066243cf684e92cac7e10eca327 \ No newline at end of file diff --git a/LinearAlgebra/src/LinearAlgebra.csproj.meta b/LinearAlgebra/src/LinearAlgebra.csproj.meta deleted file mode 100644 index d02f9ab..0000000 --- a/LinearAlgebra/src/LinearAlgebra.csproj.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: d0824c1277d5a085c8f666fc6e655888 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/LinearAlgebra/src/Matrix.cs.meta b/LinearAlgebra/src/Matrix.cs.meta deleted file mode 100644 index 475ac0c..0000000 --- a/LinearAlgebra/src/Matrix.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: bb3810889269ae287b19e0e76face9cd \ No newline at end of file diff --git a/LinearAlgebra/src/Quat32.cs.meta b/LinearAlgebra/src/Quat32.cs.meta deleted file mode 100644 index bccb1e3..0000000 --- a/LinearAlgebra/src/Quat32.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 586960852781b8e8db3a204813001eea \ No newline at end of file diff --git a/LinearAlgebra/src/Quaternion.cs.meta b/LinearAlgebra/src/Quaternion.cs.meta deleted file mode 100644 index e2c5950..0000000 --- a/LinearAlgebra/src/Quaternion.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 6d91f32cb61c1a419839b25859bed632 \ No newline at end of file diff --git a/LinearAlgebra/src/Spherical.cs.meta b/LinearAlgebra/src/Spherical.cs.meta deleted file mode 100644 index ac32bd9..0000000 --- a/LinearAlgebra/src/Spherical.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 787b6a91147861301bbe87ad8e285069 \ No newline at end of file diff --git a/LinearAlgebra/src/SwingTwist.cs.meta b/LinearAlgebra/src/SwingTwist.cs.meta deleted file mode 100644 index adcca29..0000000 --- a/LinearAlgebra/src/SwingTwist.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 025033ab8a1b8ad0f80f69017322e3aa \ No newline at end of file diff --git a/LinearAlgebra/src/Vector2Float.cs.meta b/LinearAlgebra/src/Vector2Float.cs.meta deleted file mode 100644 index 48b0a03..0000000 --- a/LinearAlgebra/src/Vector2Float.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 55826088c0deb60c68b6ad760d2deac3 \ No newline at end of file diff --git a/LinearAlgebra/src/Vector2Int.cs.meta b/LinearAlgebra/src/Vector2Int.cs.meta deleted file mode 100644 index 9d5849b..0000000 --- a/LinearAlgebra/src/Vector2Int.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 09113558bbf8fca1fb15dfea26e2f84e \ No newline at end of file diff --git a/LinearAlgebra/src/Vector3Float.cs.meta b/LinearAlgebra/src/Vector3Float.cs.meta deleted file mode 100644 index 12d2d45..0000000 --- a/LinearAlgebra/src/Vector3Float.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 2fc7d49f736899f929849abdb7198c64 \ No newline at end of file diff --git a/LinearAlgebra/src/Vector3Int.cs.meta b/LinearAlgebra/src/Vector3Int.cs.meta deleted file mode 100644 index 9111e81..0000000 --- a/LinearAlgebra/src/Vector3Int.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: cbfd0f4db235b07808071761cb870341 \ No newline at end of file diff --git a/LinearAlgebra/src/float16.cs.meta b/LinearAlgebra/src/float16.cs.meta deleted file mode 100644 index 7c7cc58..0000000 --- a/LinearAlgebra/src/float16.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: d0a40c05486e38668b208e7d24496037 \ No newline at end of file diff --git a/LinearAlgebra/test/AngleTest.cs.meta b/LinearAlgebra/test/AngleTest.cs.meta deleted file mode 100644 index d7f9e27..0000000 --- a/LinearAlgebra/test/AngleTest.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 1fc9a3dab7a1d403bb35f344d1c14c4b \ No newline at end of file diff --git a/LinearAlgebra/test/DirectionTest.cs.meta b/LinearAlgebra/test/DirectionTest.cs.meta deleted file mode 100644 index 8180aac..0000000 --- a/LinearAlgebra/test/DirectionTest.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 063c06dc57686a591b9bebb2fec6e0fb \ No newline at end of file diff --git a/LinearAlgebra/test/LinearAlgebra_Test.csproj.meta b/LinearAlgebra/test/LinearAlgebra_Test.csproj.meta deleted file mode 100644 index f20fb3f..0000000 --- a/LinearAlgebra/test/LinearAlgebra_Test.csproj.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: da3ceb471667a85d098212b0f045487c -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/LinearAlgebra/test/QuaternionTest.cs.meta b/LinearAlgebra/test/QuaternionTest.cs.meta deleted file mode 100644 index 0c7e9f0..0000000 --- a/LinearAlgebra/test/QuaternionTest.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 9f445a5425b0a13c98edd71c50eccb42 \ No newline at end of file diff --git a/LinearAlgebra/test/SphericalTest.cs.meta b/LinearAlgebra/test/SphericalTest.cs.meta deleted file mode 100644 index 38d60ae..0000000 --- a/LinearAlgebra/test/SphericalTest.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 7e4c15b444612085b9600a77cc22ea5b \ No newline at end of file diff --git a/LinearAlgebra/test/SwingTwistTest.cs.meta b/LinearAlgebra/test/SwingTwistTest.cs.meta deleted file mode 100644 index 2cb700d..0000000 --- a/LinearAlgebra/test/SwingTwistTest.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 26ebd10731635e83a89740f446a887b8 \ No newline at end of file diff --git a/LinearAlgebra/test/Vector2FloatTest.cs.meta b/LinearAlgebra/test/Vector2FloatTest.cs.meta deleted file mode 100644 index c47339c..0000000 --- a/LinearAlgebra/test/Vector2FloatTest.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 7cbdb061bb22f4701b7cfeb62cb4b842 \ No newline at end of file diff --git a/LinearAlgebra/test/Vector2IntTest.cs.meta b/LinearAlgebra/test/Vector2IntTest.cs.meta deleted file mode 100644 index c9eae74..0000000 --- a/LinearAlgebra/test/Vector2IntTest.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 779761c64b4d2321c8d43f70ba3c8597 \ No newline at end of file diff --git a/LinearAlgebra/test/Vector3FloatTest.cs.meta b/LinearAlgebra/test/Vector3FloatTest.cs.meta deleted file mode 100644 index 056fc0a..0000000 --- a/LinearAlgebra/test/Vector3FloatTest.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 630da56ad3729422593994fbc4a53b00 \ No newline at end of file diff --git a/LinearAlgebra/test/Vector3IntTest.cs.meta b/LinearAlgebra/test/Vector3IntTest.cs.meta deleted file mode 100644 index 35a07a6..0000000 --- a/LinearAlgebra/test/Vector3IntTest.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: c01e8107df0530b13bdb528e14bfed27 \ No newline at end of file From a99d40c5c91dc1a76827823e8015d3531676f38e Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Wed, 15 Apr 2026 08:58:58 +0200 Subject: [PATCH 03/34] BrainViewer added --- .../{NanoBrain_Editor.cs => Brain_Editor.cs} | 26 +- ...in_Editor.cs.meta => Brain_Editor.cs.meta} | 0 Editor/ClusterViewer.cs | 537 ++++++++++++++++++ Editor/ClusterViewer.cs.meta | 2 + 4 files changed, 564 insertions(+), 1 deletion(-) rename Editor/{NanoBrain_Editor.cs => Brain_Editor.cs} (58%) rename Editor/{NanoBrain_Editor.cs.meta => Brain_Editor.cs.meta} (100%) create mode 100644 Editor/ClusterViewer.cs create mode 100644 Editor/ClusterViewer.cs.meta diff --git a/Editor/NanoBrain_Editor.cs b/Editor/Brain_Editor.cs similarity index 58% rename from Editor/NanoBrain_Editor.cs rename to Editor/Brain_Editor.cs index 7e6b441..426990b 100644 --- a/Editor/NanoBrain_Editor.cs +++ b/Editor/Brain_Editor.cs @@ -41,13 +41,37 @@ namespace NanoBrain { } if (brain != null) - ClusterInspector.CreateInspector(root, brain.prefab, brain.defaultOutput, component.gameObject); + CreateViewer(root, brain.prefab, brain.defaultOutput, component.gameObject); if (Application.isPlaying == false) serializedObject.ApplyModifiedProperties(); return root; } + public static ClusterViewer.GraphView CreateViewer(VisualElement root, ClusterPrefab cluster, Nucleus output, GameObject gameObject) { + root.style.paddingLeft = 0; + root.style.paddingRight = 0; + root.style.paddingTop = 0; + root.style.paddingBottom = 0; + + root.styleSheets.Add(Resources.Load("GraphStyles")); + + 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, output); + + return graph; + } } } \ No newline at end of file diff --git a/Editor/NanoBrain_Editor.cs.meta b/Editor/Brain_Editor.cs.meta similarity index 100% rename from Editor/NanoBrain_Editor.cs.meta rename to Editor/Brain_Editor.cs.meta diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs new file mode 100644 index 0000000..7edadff --- /dev/null +++ b/Editor/ClusterViewer.cs @@ -0,0 +1,537 @@ +using System.Collections.Generic; +using System.Linq; + +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +namespace NanoBrain { + + public class ClusterViewer : Editor { + + public class GraphView : VisualElement { + readonly ClusterPrefab prefab; + SerializedObject serializedBrain; + Nucleus currentNucleus; + GameObject gameObject; + private List layers = new(); + private readonly Dictionary neuroidPositions = new(); + private bool expandArray = false; + + ClusterPrefab prefabAsset; + readonly PopupField outputsField; + + public GraphView(ClusterPrefab prefab) { + this.prefab = prefab; + + name = "content"; + style.flexGrow = 1; + + IMGUIContainer graphContainer = new(OnIMGUI); + graphContainer.style.position = Position.Absolute; + graphContainer.style.left = 0; graphContainer.style.top = 0; + graphContainer.style.right = 0; graphContainer.style.bottom = 0; + graphContainer.pickingMode = PickingMode.Position; + graphContainer.focusable = true; + Add(graphContainer); + + VisualElement outputContainer = new() { + style = { + flexDirection = FlexDirection.Row, + alignItems = Align.Center, + } + }; + + List names = this.prefab.outputs.Select(output => output.name).ToList(); + if (names.Count > 0 && names.First() != null) { + outputsField = new(names, names.First()) { + style = { flexGrow = 1 } + }; + outputsField.RegisterValueChangedCallback(evt => OnOutputChanged(evt.newValue)); + outputContainer.Add(outputsField); + } + + Add(outputContainer); + + // Subscribe when added to panel (editor UI ready) + RegisterCallback(evt => Subscribe()); + RegisterCallback(evt => Unsubscribe()); + } + + void OnOutputChanged(string outputName) { + if (this.currentNucleus.parent != null) + // Get nucleus in the parent instance + this.currentNucleus = this.currentNucleus.parent.GetNucleus(outputName); + else + // Get nucleus in the prefab + this.currentNucleus = this.prefab.GetNucleus(outputName); + } + + 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, Nucleus nucleus) { //}, VisualElement inspectorContainer) { + this.gameObject = gameObject; + //this.cluster = brain; + if (Application.isPlaying == false) + this.serializedBrain = new SerializedObject(this.prefab); + this.currentNucleus = nucleus; + Rebuild(); //inspectorContainer); + } + + void Rebuild() { //VisualElement inspectorContainer) { + BuildLayers(); + + if (this.currentNucleus == null) { + // inspectorContainer.Clear(); + return; + } + + string path = AssetDatabase.GetAssetPath(this.prefab); // or known path + this.prefabAsset = AssetDatabase.LoadAssetAtPath(path); + if (this.prefabAsset == null) { + // create in memory save if it doesn't exist + this.prefabAsset = CreateInstance(); + //Debug.LogError("Cluster Prefab is not found on disk"); + } + //DrawInspector(inspectorContainer); + } + + private void BuildLayers() { + // A temporary list to track what's been added to layers + this.layers = new(); + int layerIx = 0; + + Nucleus selectedNucleus = this.currentNucleus; + if (selectedNucleus == null) + return; + NeuroidLayer currentLayer = new() { ix = layerIx }; + + if (selectedNucleus is Neuron selectedNeuron && selectedNeuron.receivers != null) { + foreach (Nucleus receiver in selectedNeuron.receivers) { + Nucleus outputNeuroid = receiver; + if (outputNeuroid != null) { + AddToLayer(currentLayer, outputNeuroid); + // Debug.Log($"layer {layerIx} nucleus {outputNeuroid.name}"); + } + } + } + if (currentLayer.neuroids.Count > 0) { + this.layers.Add(currentLayer); + layerIx++; + currentLayer = new() { ix = layerIx }; + } + + AddToLayer(currentLayer, selectedNucleus); + this.layers.Add(currentLayer); + // Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}"); + + layerIx++; + currentLayer = new() { ix = layerIx }; + + if (selectedNucleus.synapses != null) { + foreach (Synapse synapse in selectedNucleus.synapses) { + Nucleus input = synapse.neuron; + AddToLayer(currentLayer, input); + // Debug.Log($"layer {layerIx} nucleus {input.name}"); + } + } + if (currentLayer.neuroids.Count > 0) { + this.layers.Add(currentLayer); + } + } + + private void AddToLayer(NeuroidLayer layer, Nucleus nucleus) { + if (nucleus == null) + return; + layer.neuroids.Add(nucleus); + //nucleus.layerIx = layer.ix; + // Store its position + Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1); + neuroidPositions[nucleus] = neuroidPosition; + + } + + public void OnIMGUI() { + if (currentNucleus == null) + return; + + if (Application.isPlaying == false) + serializedBrain.Update(); + + Handles.BeginGUI(); + DrawGraph(); + Handles.EndGUI(); + + } + + private void DrawGraph() { + float size = 20; + Vector3 position = new(150, 210, 0); + + DrawReceivers(this.currentNucleus, position, size); + DrawSynapses(this.currentNucleus, position, size); + + // Draw selected Nucleus + if (expandArray) { + if (this.currentNucleus is IReceptor receptor1) { + float maxValue = 0; + foreach (Nucleus nucleus in receptor1.nucleiArray) { + if (nucleus is Neuron neuron) { + float value = neuron.outputMagnitude; + if (value > maxValue) + maxValue = value; + } + } + + float spacing = 400f / receptor1.nucleiArray.Count(); + float margin = 10 + spacing / 2; + float xMin = 150 - size; + float xMax = 150 + size; + float yMin = 10 + margin - size / 2; + float yMax = 400 - margin + size; + Vector3[] verts = new Vector3[4] { + new(xMin, yMin, 0), + new(xMax, yMin, 0), + new(xMax, yMax, 0), + new(xMin, yMax, 0) + }; + Handles.color = Color.black; + Handles.DrawAAConvexPolygon(verts); + int row = 0; + foreach (Nucleus nucleus in receptor1.nucleiArray) { + Vector3 pos = new(150, margin + row * spacing, 0.0f); + Handles.color = Color.white; + // The selected nucleus highlight ring + Handles.DrawSolidDisc(pos, Vector3.forward, size + 2); + DrawNucleus(nucleus, pos, maxValue, size); + row++; + } + GUIStyle style = new(EditorStyles.label) { + alignment = TextAnchor.UpperCenter, + normal = { textColor = Color.white }, + fontStyle = FontStyle.Bold, + }; + Vector3 labelPos = new(150, yMax + size + 5, 0); + string receptorName = receptor1.GetName(); + int colonPos = receptorName.IndexOf(":"); + if (colonPos > 0) { + string baseName = receptorName[..colonPos]; + Handles.Label(labelPos, baseName, style); + } + else + Handles.Label(labelPos, receptorName, style); + } + else { + Handles.color = Color.white; + // The selected nucleus highlight ring + Handles.DrawSolidDisc(position, Vector3.forward, size + 2); + 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 { + Handles.color = Color.white; + // The selected nucleus highlight ring + Handles.DrawSolidDisc(position, Vector3.forward, size + 2); + 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); + } + } + + private void DrawReceivers(Nucleus nucleus, Vector3 parentPos, float size) { + List receivers; + if (nucleus is Neuron neuron) + receivers = neuron.receivers; + else if (nucleus is Cluster cluster) + receivers = cluster.CollectReceivers(); + else + return; + + int nodeCount = receivers.Count(); //neuron != null ? neuron.receivers.Count() : 1; + + // 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; + List drawnArrays = new(); + foreach (Nucleus receiver in receivers) { + if (receiver is Receptor receptor) { + if (drawnArrays.Contains(receptor.nucleiArray)) + continue; + drawnArrays.Add(receptor.nucleiArray); + } + + Nucleus receiverNucleus = receiver; + if (receiverNucleus == null) + continue; + + Vector3 pos = new(50, margin + row * spacing, 0.0f); + Handles.color = Color.white; + Handles.DrawLine(parentPos, pos); + + DrawNucleus(receiverNucleus, pos, maxValue, size); + row++; + } + } + + private void DrawSynapses(Nucleus nucleus, Vector3 parentPos, float size) { + int nodeCount = nucleus.synapses.Count; + + // Determine the maximum value in this layer + // This is used to 'scale' the output value colors of the nuclei + float maxValue = 0; + int neuronCount = 0; + List drawnArrays = new(); + foreach (Synapse synapse in nucleus.synapses) { + if (synapse.neuron == null) + continue; + + if (synapse.neuron is Receptor receptor) { + if (drawnArrays.Contains(receptor.nucleiArray)) + continue; + drawnArrays.Add(receptor.nucleiArray); + } + else if (synapse.neuron.parent is ClusterReceptor clusterReceptor) { + if (drawnArrays.Contains(clusterReceptor.nucleiArray)) + continue; + drawnArrays.Add(clusterReceptor.nucleiArray); + } + if (synapse.neuron is Neuron synapseNeuron) { + float value = synapseNeuron.outputMagnitude * synapse.weight; + // Debug.Log($"{synapse.nucleus.name}: {value} {length(synapse.nucleus.outputValue)} {synapse.weight}"); + if (value > maxValue) + maxValue = value; + } + neuronCount++; + } + + // Determine the spacing of the nuclei in the layer + float spacing = 400f / neuronCount; + float margin = 10 + spacing / 2; + + int row = 0; + drawnArrays = new(); + foreach (Synapse synapse in nucleus.synapses) { + if (synapse.neuron is null) + continue; + + if (synapse.neuron is Receptor neuron) { + if (drawnArrays.Contains(neuron.nucleiArray)) + continue; + drawnArrays.Add(neuron.nucleiArray); + } + else if (synapse.neuron.parent is ClusterReceptor clusterReceptor) { + if (drawnArrays.Contains(clusterReceptor.nucleiArray)) + continue; + drawnArrays.Add(clusterReceptor.nucleiArray); + } + Vector3 pos = new(250, margin + row * spacing, 0.0f); + 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 = 0; + if (synapse.neuron is Neuron synapseNeuron) + brightness = synapseNeuron.outputMagnitude * synapse.weight / maxValue; + color = new Color(brightness, brightness, brightness, 1f); + } + if (synapse.neuron.parent != null && synapse.neuron.parent != this.currentNucleus.parent) { + // the synapse nucleus is part of a subcluster + DrawNucleus(synapse.neuron.parent, pos, maxValue, size, color); + } + // else if (synapse.nucleus.cluster != null && synapse.nucleus.cluster != this.currentNucleus.cluster) { + // DrawNucleus(synapse.nucleus.parent, pos, maxValue, size, color); + // } + else { + DrawNucleus(synapse.neuron, pos, maxValue, size, color); + } + row++; + } + } + + private 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, maxValue, size, color); + } + + private void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size, Color color) { + 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 is IReceptor receptor1) { + if (expandArray) { + // Put array indices above elements + style.alignment = TextAnchor.LowerCenter; + Vector3 labelPos1 = position + Vector3.down * (size + 5); // below disc + int colonPos1 = nucleus.name.IndexOf(":"); + if (colonPos1 > 0) { + string extName = nucleus.name[(colonPos1 + 2)..]; + Handles.Label(labelPos1, extName, style); + } + } + else { + // draw the array size label + if (color.grayscale > 0.5f) + style.normal.textColor = Color.black; + else + style.normal.textColor = Color.white; + Handles.Label(labelPosition, receptor1.nucleiArray.Length.ToString(), style); + style.normal.textColor = Color.white; + } + } + + if (expandArray == false || nucleus is not IReceptor) { + // put name below nucleus + Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron + style.alignment = TextAnchor.UpperCenter; + + 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); + + } + + // Draw Cluster ring + if (nucleus is Cluster) { + Handles.color = Color.white; + Handles.DrawWireDisc(position, Vector3.forward, size + 5); + } + + // 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(); + HandleClicked(nucleus); + } + } + } + + private 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); + } + + private void HandleClicked(Nucleus nucleus) { + if (nucleus == this.currentNucleus) { + if (nucleus is Receptor || nucleus is ClusterReceptor) + expandArray = !expandArray; + else + expandArray = false; + } + else { + this.currentNucleus = nucleus; + expandArray = false; + BuildLayers(); + } + } + + void OnSceneGUI(SceneView sceneView) { + if (this.gameObject != null) { + 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.color = Color.yellow; + Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector); + } + } + } + } + + } + } +} \ No newline at end of file diff --git a/Editor/ClusterViewer.cs.meta b/Editor/ClusterViewer.cs.meta new file mode 100644 index 0000000..ac68b91 --- /dev/null +++ b/Editor/ClusterViewer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4fe58945c76d153edacc220597474ad2 \ No newline at end of file From 1c7b8e794074d81a7e99a08bf2ee3b09d8fc04c4 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Wed, 15 Apr 2026 09:50:36 +0200 Subject: [PATCH 04/34] Added Tanh Activation --- Editor/ClusterInspector.cs | 1563 ++++++++++++++++++++++---------- Editor/ClusterViewer.cs | 22 +- Runtime/Scripts/Core/Neuron.cs | 52 +- 3 files changed, 1133 insertions(+), 504 deletions(-) diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterInspector.cs index b081cb3..d7a4008 100644 --- a/Editor/ClusterInspector.cs +++ b/Editor/ClusterInspector.cs @@ -8,8 +8,7 @@ using UnityEngine.UIElements; namespace NanoBrain { [CustomEditor(typeof(ClusterPrefab))] - public class ClusterInspector : Editor { - + public class ClusterInspector : ClusterViewer { public override VisualElement CreateInspectorGUI() { ClusterPrefab prefab = target as ClusterPrefab; if (prefab != null) @@ -37,11 +36,11 @@ namespace NanoBrain { flexDirection = FlexDirection.Row } }; - GraphView graph = new(cluster); + GraphEditor graph = new(cluster); graph.style.flexGrow = 1; - VisualElement inspectorContainer = new VisualElement { - name = "inspector", + VisualElement inspectorContainer = new() { + name = "inspector", style = { alignSelf = Align.Stretch, minHeight = 450, @@ -59,47 +58,10 @@ namespace NanoBrain { return graph; } - public class GraphView : VisualElement { - readonly ClusterPrefab prefab; - SerializedObject serializedBrain; - Nucleus currentNucleus; - GameObject gameObject; - private List layers = new(); - private readonly Dictionary neuroidPositions = new(); - private bool expandArray = false; + public class GraphEditor : GraphView { - ClusterPrefab prefabAsset; - readonly PopupField outputsField; + public GraphEditor(ClusterPrefab prefab) : base(prefab) { - public GraphView(ClusterPrefab prefab) { - this.prefab = prefab; - - name = "content"; - style.flexGrow = 1; - - IMGUIContainer graphContainer = new(OnIMGUI); - graphContainer.style.position = Position.Absolute; - graphContainer.style.left = 0; graphContainer.style.top = 0; - graphContainer.style.right = 0; graphContainer.style.bottom = 0; - graphContainer.pickingMode = PickingMode.Position; - graphContainer.focusable = true; - Add(graphContainer); - - VisualElement outputContainer = new() { - style = { - flexDirection = FlexDirection.Row, - alignItems = Align.Center, - } - }; - - List names = this.prefab.outputs.Select(output => output.name).ToList(); - if (names.Count > 0 && names.First() != null) { - outputsField = new(names, names.First()) { - style = { flexGrow = 1 } - }; - outputsField.RegisterValueChangedCallback(evt => OnOutputChanged(evt.newValue)); - outputContainer.Add(outputsField); - } Button addButton = new(() => OnAddClusterOutput()) { text = "Add" @@ -108,18 +70,6 @@ namespace NanoBrain { Add(outputContainer); - // Subscribe when added to panel (editor UI ready) - RegisterCallback(evt => Subscribe()); - RegisterCallback(evt => Unsubscribe()); - } - - void OnOutputChanged(string outputName) { - if (this.currentNucleus.parent != null) - // Get nucleus in the parent instance - this.currentNucleus = this.currentNucleus.parent.GetNucleus(outputName); - else - // Get nucleus in the prefab - this.currentNucleus = this.prefab.GetNucleus(outputName); } void OnAddClusterOutput() { @@ -131,19 +81,6 @@ namespace NanoBrain { this.currentNucleus = newOutput; } - 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, Nucleus nucleus, VisualElement inspectorContainer) { this.gameObject = gameObject; @@ -172,413 +109,7 @@ namespace NanoBrain { DrawInspector(inspectorContainer); } - private void BuildLayers() { - // A temporary list to track what's been added to layers - this.layers = new(); - int layerIx = 0; - - Nucleus selectedNucleus = this.currentNucleus; - if (selectedNucleus == null) - return; - NeuroidLayer currentLayer = new() { ix = layerIx }; - - if (selectedNucleus is Neuron selectedNeuron && selectedNeuron.receivers != null) { - foreach (Nucleus receiver in selectedNeuron.receivers) { - Nucleus outputNeuroid = receiver; - if (outputNeuroid != null) { - AddToLayer(currentLayer, outputNeuroid); - // Debug.Log($"layer {layerIx} nucleus {outputNeuroid.name}"); - } - } - } - if (currentLayer.neuroids.Count > 0) { - this.layers.Add(currentLayer); - layerIx++; - currentLayer = new() { ix = layerIx }; - } - - AddToLayer(currentLayer, selectedNucleus); - this.layers.Add(currentLayer); - // Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}"); - - layerIx++; - currentLayer = new() { ix = layerIx }; - - if (selectedNucleus.synapses != null) { - foreach (Synapse synapse in selectedNucleus.synapses) { - Nucleus input = synapse.neuron; - AddToLayer(currentLayer, input); - // Debug.Log($"layer {layerIx} nucleus {input.name}"); - } - } - if (currentLayer.neuroids.Count > 0) { - this.layers.Add(currentLayer); - } - } - - private void AddToLayer(NeuroidLayer layer, Nucleus nucleus) { - if (nucleus == null) - return; - layer.neuroids.Add(nucleus); - //nucleus.layerIx = layer.ix; - // Store its position - Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1); - neuroidPositions[nucleus] = neuroidPosition; - - } - - public void OnIMGUI() { - if (currentNucleus == null) - return; - - if (Application.isPlaying == false) - serializedBrain.Update(); - - Handles.BeginGUI(); - DrawGraph(); - Handles.EndGUI(); - - } - - private void DrawGraph() { - float size = 20; - Vector3 position = new(150, 210, 0); - - DrawReceivers(this.currentNucleus, position, size); - DrawSynapses(this.currentNucleus, position, size); - - // Draw selected Nucleus - if (expandArray) { - if (this.currentNucleus is IReceptor receptor1) { - float maxValue = 0; - foreach (Nucleus nucleus in receptor1.nucleiArray) { - if (nucleus is Neuron neuron) { - float value = neuron.outputMagnitude; - if (value > maxValue) - maxValue = value; - } - } - - float spacing = 400f / receptor1.nucleiArray.Count(); - float margin = 10 + spacing / 2; - float xMin = 150 - size; - float xMax = 150 + size; - float yMin = 10 + margin - size / 2; - float yMax = 400 - margin + size; - Vector3[] verts = new Vector3[4] { - new(xMin, yMin, 0), - new(xMax, yMin, 0), - new(xMax, yMax, 0), - new(xMin, yMax, 0) - }; - Handles.color = Color.black; - Handles.DrawAAConvexPolygon(verts); - int row = 0; - foreach (Nucleus nucleus in receptor1.nucleiArray) { - Vector3 pos = new(150, margin + row * spacing, 0.0f); - Handles.color = Color.white; - // The selected nucleus highlight ring - Handles.DrawSolidDisc(pos, Vector3.forward, size + 2); - DrawNucleus(nucleus, pos, maxValue, size); - row++; - } - GUIStyle style = new(EditorStyles.label) { - alignment = TextAnchor.UpperCenter, - normal = { textColor = Color.white }, - fontStyle = FontStyle.Bold, - }; - Vector3 labelPos = new(150, yMax + size + 5, 0); - string receptorName = receptor1.GetName(); - int colonPos = receptorName.IndexOf(":"); - if (colonPos > 0) { - string baseName = receptorName[..colonPos]; - Handles.Label(labelPos, baseName, style); - } - else - Handles.Label(labelPos, receptorName, style); - } - else { - Handles.color = Color.white; - // The selected nucleus highlight ring - Handles.DrawSolidDisc(position, Vector3.forward, size + 2); - 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 { - Handles.color = Color.white; - // The selected nucleus highlight ring - Handles.DrawSolidDisc(position, Vector3.forward, size + 2); - 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); - } - } - - private void DrawReceivers(Nucleus nucleus, Vector3 parentPos, float size) { - List receivers; - if (nucleus is Neuron neuron) - receivers = neuron.receivers; - else if (nucleus is Cluster cluster) - receivers = cluster.CollectReceivers(); - else - return; - - int nodeCount = receivers.Count(); //neuron != null ? neuron.receivers.Count() : 1; - - // 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; - List drawnArrays = new(); - foreach (Nucleus receiver in receivers) { - if (receiver is Receptor receptor) { - if (drawnArrays.Contains(receptor.nucleiArray)) - continue; - drawnArrays.Add(receptor.nucleiArray); - } - - Nucleus receiverNucleus = receiver; - if (receiverNucleus == null) - continue; - - Vector3 pos = new(50, margin + row * spacing, 0.0f); - Handles.color = Color.white; - Handles.DrawLine(parentPos, pos); - - DrawNucleus(receiverNucleus, pos, maxValue, size); - row++; - } - } - - private void DrawSynapses(Nucleus nucleus, Vector3 parentPos, float size) { - int nodeCount = nucleus.synapses.Count; - - // Determine the maximum value in this layer - // This is used to 'scale' the output value colors of the nuclei - float maxValue = 0; - int neuronCount = 0; - List drawnArrays = new(); - foreach (Synapse synapse in nucleus.synapses) { - if (synapse.neuron == null) - continue; - - if (synapse.neuron is Receptor receptor) { - if (drawnArrays.Contains(receptor.nucleiArray)) - continue; - drawnArrays.Add(receptor.nucleiArray); - } - else if (synapse.neuron.parent is ClusterReceptor clusterReceptor) { - if (drawnArrays.Contains(clusterReceptor.nucleiArray)) - continue; - drawnArrays.Add(clusterReceptor.nucleiArray); - } - if (synapse.neuron is Neuron synapseNeuron) { - float value = synapseNeuron.outputMagnitude * synapse.weight; - // Debug.Log($"{synapse.nucleus.name}: {value} {length(synapse.nucleus.outputValue)} {synapse.weight}"); - if (value > maxValue) - maxValue = value; - } - neuronCount++; - } - - // Determine the spacing of the nuclei in the layer - float spacing = 400f / neuronCount; - float margin = 10 + spacing / 2; - - int row = 0; - drawnArrays = new(); - foreach (Synapse synapse in nucleus.synapses) { - if (synapse.neuron is null) - continue; - - if (synapse.neuron is Receptor neuron) { - if (drawnArrays.Contains(neuron.nucleiArray)) - continue; - drawnArrays.Add(neuron.nucleiArray); - } - else if (synapse.neuron.parent is ClusterReceptor clusterReceptor) { - if (drawnArrays.Contains(clusterReceptor.nucleiArray)) - continue; - drawnArrays.Add(clusterReceptor.nucleiArray); - } - Vector3 pos = new(250, margin + row * spacing, 0.0f); - 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 = 0; - if (synapse.neuron is Neuron synapseNeuron) - brightness = synapseNeuron.outputMagnitude * synapse.weight / maxValue; - color = new Color(brightness, brightness, brightness, 1f); - } - if (synapse.neuron.parent != null && synapse.neuron.parent != this.currentNucleus.parent) { - // the synapse nucleus is part of a subcluster - DrawNucleus(synapse.neuron.parent, pos, maxValue, size, color); - } - // else if (synapse.nucleus.cluster != null && synapse.nucleus.cluster != this.currentNucleus.cluster) { - // DrawNucleus(synapse.nucleus.parent, pos, maxValue, size, color); - // } - else { - DrawNucleus(synapse.neuron, pos, maxValue, size, color); - } - row++; - } - } - - private 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, maxValue, size, color); - } - - private void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size, Color color) { - 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 is IReceptor receptor1) { - if (expandArray) { - // Put array indices above elements - style.alignment = TextAnchor.LowerCenter; - Vector3 labelPos1 = position + Vector3.down * (size + 5); // below disc - int colonPos1 = nucleus.name.IndexOf(":"); - if (colonPos1 > 0) { - string extName = nucleus.name[(colonPos1 + 2)..]; - Handles.Label(labelPos1, extName, style); - } - } - else { - // draw the array size label - if (color.grayscale > 0.5f) - style.normal.textColor = Color.black; - else - style.normal.textColor = Color.white; - Handles.Label(labelPosition, receptor1.nucleiArray.Length.ToString(), style); - style.normal.textColor = Color.white; - } - } - - if (expandArray == false || nucleus is not IReceptor) { - // put name below nucleus - Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron - style.alignment = TextAnchor.UpperCenter; - - 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); - - } - - // Draw Cluster ring - if (nucleus is Cluster) { - Handles.color = Color.white; - Handles.DrawWireDisc(position, Vector3.forward, size + 5); - } - - // 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(); - HandleClicked(nucleus); - } - } - } - - private 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); - } - - private void HandleClicked(Nucleus nucleus) { - if (nucleus == this.currentNucleus) { - if (nucleus is Receptor || nucleus is ClusterReceptor) - expandArray = !expandArray; - else - expandArray = false; - } - // else if (nucleus is ReceptorInstance receptor) { - // this.currentNucleus = receptor.receptor; - // expandArray = false; - // BuildLayers(); - // } - else { - this.currentNucleus = nucleus; - expandArray = false; - BuildLayers(); - } - } + #region Inspector private VisualElement inspectorIMGUIContainer; private bool showSynapses = true; @@ -785,7 +316,7 @@ namespace NanoBrain { EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, 0, 1, neuron.curveMax)); else EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, neuron.curveMax, 1, -neuron.curveMax)); - Neuron.CurvePresets newPreset = (Neuron.CurvePresets)EditorGUILayout.EnumPopup(neuron.curvePreset, GUILayout.Width(100)); + Neuron.ActivationFunction newPreset = (Neuron.ActivationFunction)EditorGUILayout.EnumPopup(neuron.curvePreset, GUILayout.Width(100)); anythingChanged |= newPreset != neuron.curvePreset; neuron.curvePreset = newPreset; EditorGUILayout.EndHorizontal(); @@ -1063,14 +594,1076 @@ namespace NanoBrain { } } - #endregion Synapses + #endregion Synapses + + #endregion Inspector + } + } + /* + [CustomEditor(typeof(ClusterPrefab))] + public class ClusterInspector : Editor { + + public override VisualElement CreateInspectorGUI() { + ClusterPrefab prefab = target as ClusterPrefab; + if (prefab != null) + prefab.EnsureInitialization(); + + serializedObject.Update(); + + VisualElement root = new(); + CreateInspector(root, prefab, prefab.output, null); + + serializedObject.ApplyModifiedProperties(); + return root; + } + + public static GraphView CreateInspector(VisualElement root, ClusterPrefab cluster, Nucleus output, GameObject gameObject) { + root.style.paddingLeft = 0; + root.style.paddingRight = 0; + root.style.paddingTop = 0; + root.style.paddingBottom = 0; + + root.styleSheets.Add(Resources.Load("GraphStyles")); + + VisualElement mainContainer = new() { + style = { + flexDirection = FlexDirection.Row + } + }; + GraphView graph = new(cluster); + graph.style.flexGrow = 1; + + VisualElement inspectorContainer = new VisualElement { + name = "inspector", + style = { + alignSelf = Align.Stretch, + minHeight = 450, + width = 300, + flexGrow = 0 + } + }; + + mainContainer.Add(graph); + mainContainer.Add(inspectorContainer); + root.Add(mainContainer); + + graph.SetGraph(gameObject, output, inspectorContainer); + + return graph; + } + + public class GraphView : VisualElement { + readonly ClusterPrefab prefab; + SerializedObject serializedBrain; + Nucleus currentNucleus; + GameObject gameObject; + private List layers = new(); + private readonly Dictionary neuroidPositions = new(); + private bool expandArray = false; + + ClusterPrefab prefabAsset; + readonly PopupField outputsField; + + public GraphView(ClusterPrefab prefab) { + this.prefab = prefab; + + name = "content"; + style.flexGrow = 1; + + IMGUIContainer graphContainer = new(OnIMGUI); + graphContainer.style.position = Position.Absolute; + graphContainer.style.left = 0; graphContainer.style.top = 0; + graphContainer.style.right = 0; graphContainer.style.bottom = 0; + graphContainer.pickingMode = PickingMode.Position; + graphContainer.focusable = true; + Add(graphContainer); + + VisualElement outputContainer = new() { + style = { + flexDirection = FlexDirection.Row, + alignItems = Align.Center, + } + }; + + List names = this.prefab.outputs.Select(output => output.name).ToList(); + if (names.Count > 0 && names.First() != null) { + outputsField = new(names, names.First()) { + style = { flexGrow = 1 } + }; + outputsField.RegisterValueChangedCallback(evt => OnOutputChanged(evt.newValue)); + outputContainer.Add(outputsField); + } + + Button addButton = new(() => OnAddClusterOutput()) { + text = "Add" + }; + outputContainer.Add(addButton); + + Add(outputContainer); + + // Subscribe when added to panel (editor UI ready) + RegisterCallback(evt => Subscribe()); + RegisterCallback(evt => Unsubscribe()); + } + + void OnOutputChanged(string outputName) { + if (this.currentNucleus.parent != null) + // Get nucleus in the parent instance + this.currentNucleus = this.currentNucleus.parent.GetNucleus(outputName); + else + // Get nucleus in the prefab + this.currentNucleus = this.prefab.GetNucleus(outputName); + } + + void OnAddClusterOutput() { + Nucleus newOutput = new Neuron(this.prefab, "New Output"); + this.prefab.RefreshOutputs(); + outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); + outputsField.value = newOutput.name; + + this.currentNucleus = newOutput; + } + + 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, Nucleus nucleus, VisualElement inspectorContainer) { + this.gameObject = gameObject; + //this.cluster = brain; + if (Application.isPlaying == false) + this.serializedBrain = new SerializedObject(this.prefab); + this.currentNucleus = nucleus; + Rebuild(inspectorContainer); + } + + void Rebuild(VisualElement inspectorContainer) { + BuildLayers(); + + if (this.currentNucleus == null) { + inspectorContainer.Clear(); + return; + } + + string path = AssetDatabase.GetAssetPath(this.prefab); // or known path + this.prefabAsset = AssetDatabase.LoadAssetAtPath(path); + if (this.prefabAsset == null) { + // create in memory save if it doesn't exist + this.prefabAsset = CreateInstance(); + //Debug.LogError("Cluster Prefab is not found on disk"); + } + DrawInspector(inspectorContainer); + } + + private void BuildLayers() { + // A temporary list to track what's been added to layers + this.layers = new(); + int layerIx = 0; + + Nucleus selectedNucleus = this.currentNucleus; + if (selectedNucleus == null) + return; + NeuroidLayer currentLayer = new() { ix = layerIx }; + + if (selectedNucleus is Neuron selectedNeuron && selectedNeuron.receivers != null) { + foreach (Nucleus receiver in selectedNeuron.receivers) { + Nucleus outputNeuroid = receiver; + if (outputNeuroid != null) { + AddToLayer(currentLayer, outputNeuroid); + // Debug.Log($"layer {layerIx} nucleus {outputNeuroid.name}"); + } + } + } + if (currentLayer.neuroids.Count > 0) { + this.layers.Add(currentLayer); + layerIx++; + currentLayer = new() { ix = layerIx }; + } + + AddToLayer(currentLayer, selectedNucleus); + this.layers.Add(currentLayer); + // Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}"); + + layerIx++; + currentLayer = new() { ix = layerIx }; + + if (selectedNucleus.synapses != null) { + foreach (Synapse synapse in selectedNucleus.synapses) { + Nucleus input = synapse.neuron; + AddToLayer(currentLayer, input); + // Debug.Log($"layer {layerIx} nucleus {input.name}"); + } + } + if (currentLayer.neuroids.Count > 0) { + this.layers.Add(currentLayer); + } + } + + private void AddToLayer(NeuroidLayer layer, Nucleus nucleus) { + if (nucleus == null) + return; + layer.neuroids.Add(nucleus); + //nucleus.layerIx = layer.ix; + // Store its position + Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1); + neuroidPositions[nucleus] = neuroidPosition; + + } + + public void OnIMGUI() { + if (currentNucleus == null) + return; + + if (Application.isPlaying == false) + serializedBrain.Update(); + + Handles.BeginGUI(); + DrawGraph(); + Handles.EndGUI(); + + } + + private void DrawGraph() { + float size = 20; + Vector3 position = new(150, 210, 0); + + DrawReceivers(this.currentNucleus, position, size); + DrawSynapses(this.currentNucleus, position, size); + + // Draw selected Nucleus + if (expandArray) { + if (this.currentNucleus is IReceptor receptor1) { + float maxValue = 0; + foreach (Nucleus nucleus in receptor1.nucleiArray) { + if (nucleus is Neuron neuron) { + float value = neuron.outputMagnitude; + if (value > maxValue) + maxValue = value; + } + } + + float spacing = 400f / receptor1.nucleiArray.Count(); + float margin = 10 + spacing / 2; + float xMin = 150 - size; + float xMax = 150 + size; + float yMin = 10 + margin - size / 2; + float yMax = 400 - margin + size; + Vector3[] verts = new Vector3[4] { + new(xMin, yMin, 0), + new(xMax, yMin, 0), + new(xMax, yMax, 0), + new(xMin, yMax, 0) + }; + Handles.color = Color.black; + Handles.DrawAAConvexPolygon(verts); + int row = 0; + foreach (Nucleus nucleus in receptor1.nucleiArray) { + Vector3 pos = new(150, margin + row * spacing, 0.0f); + Handles.color = Color.white; + // The selected nucleus highlight ring + Handles.DrawSolidDisc(pos, Vector3.forward, size + 2); + DrawNucleus(nucleus, pos, maxValue, size); + row++; + } + GUIStyle style = new(EditorStyles.label) { + alignment = TextAnchor.UpperCenter, + normal = { textColor = Color.white }, + fontStyle = FontStyle.Bold, + }; + Vector3 labelPos = new(150, yMax + size + 5, 0); + string receptorName = receptor1.GetName(); + int colonPos = receptorName.IndexOf(":"); + if (colonPos > 0) { + string baseName = receptorName[..colonPos]; + Handles.Label(labelPos, baseName, style); + } + else + Handles.Label(labelPos, receptorName, style); + } + else { + Handles.color = Color.white; + // The selected nucleus highlight ring + Handles.DrawSolidDisc(position, Vector3.forward, size + 2); + 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 { + Handles.color = Color.white; + // The selected nucleus highlight ring + Handles.DrawSolidDisc(position, Vector3.forward, size + 2); + 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); + } + } + + private void DrawReceivers(Nucleus nucleus, Vector3 parentPos, float size) { + List receivers; + if (nucleus is Neuron neuron) + receivers = neuron.receivers; + else if (nucleus is Cluster cluster) + receivers = cluster.CollectReceivers(); + else + return; + + int nodeCount = receivers.Count(); //neuron != null ? neuron.receivers.Count() : 1; + + // 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; + List drawnArrays = new(); + foreach (Nucleus receiver in receivers) { + if (receiver is Receptor receptor) { + if (drawnArrays.Contains(receptor.nucleiArray)) + continue; + drawnArrays.Add(receptor.nucleiArray); + } + + Nucleus receiverNucleus = receiver; + if (receiverNucleus == null) + continue; + + Vector3 pos = new(50, margin + row * spacing, 0.0f); + Handles.color = Color.white; + Handles.DrawLine(parentPos, pos); + + DrawNucleus(receiverNucleus, pos, maxValue, size); + row++; + } + } + + private void DrawSynapses(Nucleus nucleus, Vector3 parentPos, float size) { + int nodeCount = nucleus.synapses.Count; + + // Determine the maximum value in this layer + // This is used to 'scale' the output value colors of the nuclei + float maxValue = 0; + int neuronCount = 0; + List drawnArrays = new(); + foreach (Synapse synapse in nucleus.synapses) { + if (synapse.neuron == null) + continue; + + if (synapse.neuron is Receptor receptor) { + if (drawnArrays.Contains(receptor.nucleiArray)) + continue; + drawnArrays.Add(receptor.nucleiArray); + } + else if (synapse.neuron.parent is ClusterReceptor clusterReceptor) { + if (drawnArrays.Contains(clusterReceptor.nucleiArray)) + continue; + drawnArrays.Add(clusterReceptor.nucleiArray); + } + if (synapse.neuron is Neuron synapseNeuron) { + float value = synapseNeuron.outputMagnitude * synapse.weight; + // Debug.Log($"{synapse.nucleus.name}: {value} {length(synapse.nucleus.outputValue)} {synapse.weight}"); + if (value > maxValue) + maxValue = value; + } + neuronCount++; + } + + // Determine the spacing of the nuclei in the layer + float spacing = 400f / neuronCount; + float margin = 10 + spacing / 2; + + int row = 0; + drawnArrays = new(); + foreach (Synapse synapse in nucleus.synapses) { + if (synapse.neuron is null) + continue; + + if (synapse.neuron is Receptor neuron) { + if (drawnArrays.Contains(neuron.nucleiArray)) + continue; + drawnArrays.Add(neuron.nucleiArray); + } + else if (synapse.neuron.parent is ClusterReceptor clusterReceptor) { + if (drawnArrays.Contains(clusterReceptor.nucleiArray)) + continue; + drawnArrays.Add(clusterReceptor.nucleiArray); + } + Vector3 pos = new(250, margin + row * spacing, 0.0f); + 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 = 0; + if (synapse.neuron is Neuron synapseNeuron) + brightness = synapseNeuron.outputMagnitude * synapse.weight / maxValue; + color = new Color(brightness, brightness, brightness, 1f); + } + if (synapse.neuron.parent != null && synapse.neuron.parent != this.currentNucleus.parent) { + // the synapse nucleus is part of a subcluster + DrawNucleus(synapse.neuron.parent, pos, maxValue, size, color); + } + // else if (synapse.nucleus.cluster != null && synapse.nucleus.cluster != this.currentNucleus.cluster) { + // DrawNucleus(synapse.nucleus.parent, pos, maxValue, size, color); + // } + else { + DrawNucleus(synapse.neuron, pos, maxValue, size, color); + } + row++; + } + } + + private 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, maxValue, size, color); + } + + private void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size, Color color) { + 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 is IReceptor receptor1) { + if (expandArray) { + // Put array indices above elements + style.alignment = TextAnchor.LowerCenter; + Vector3 labelPos1 = position + Vector3.down * (size + 5); // below disc + int colonPos1 = nucleus.name.IndexOf(":"); + if (colonPos1 > 0) { + string extName = nucleus.name[(colonPos1 + 2)..]; + Handles.Label(labelPos1, extName, style); + } + } + else { + // draw the array size label + if (color.grayscale > 0.5f) + style.normal.textColor = Color.black; + else + style.normal.textColor = Color.white; + Handles.Label(labelPosition, receptor1.nucleiArray.Length.ToString(), style); + style.normal.textColor = Color.white; + } + } + + if (expandArray == false || nucleus is not IReceptor) { + // put name below nucleus + Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron + style.alignment = TextAnchor.UpperCenter; + + 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); + + } + + // Draw Cluster ring + if (nucleus is Cluster) { + Handles.color = Color.white; + Handles.DrawWireDisc(position, Vector3.forward, size + 5); + } + + // 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(); + HandleClicked(nucleus); + } + } + } + + private 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); + } + + private void HandleClicked(Nucleus nucleus) { + if (nucleus == this.currentNucleus) { + if (nucleus is Receptor || nucleus is ClusterReceptor) + expandArray = !expandArray; + else + expandArray = false; + } + // else if (nucleus is ReceptorInstance receptor) { + // this.currentNucleus = receptor.receptor; + // expandArray = false; + // BuildLayers(); + // } + else { + this.currentNucleus = nucleus; + expandArray = false; + BuildLayers(); + } + } + + private VisualElement inspectorIMGUIContainer; + private bool showSynapses = true; + private bool showActivation = true; + protected bool breakOnWake = false; + protected bool trace = false; + void DrawInspector(VisualElement inspectorContainer) { + if (inspectorContainer == null) + return; + + inspectorContainer.Clear(); + if (this.currentNucleus == null) + return; + + // create a SerializedObject wrapper so Unity inspector controls work (and Undo) + SerializedObject so = new(prefabAsset); + this.inspectorIMGUIContainer = new IMGUIContainer(() => InspectorHandler(so)); + + inspectorContainer.Add(inspectorIMGUIContainer); + } + + void InspectorHandler(SerializedObject serializedObject) { + bool anythingChanged = false; + + if (serializedObject == null || serializedObject.targetObject == null) + return; + + if (this.currentNucleus == null) + return; + + serializedObject.Update(); + + GUIStyle headerStyle = new(EditorStyles.boldLabel) { + alignment = TextAnchor.MiddleLeft, + margin = new RectOffset(10, 0, 4, 4) + }; + GUIStyle boldTextFieldStyle = new(EditorStyles.textField) { + fontStyle = FontStyle.Bold + }; + + GUILayout.Label(this.currentNucleus.GetType().ToString(), headerStyle); + string newName = EditorGUILayout.TextField(this.currentNucleus.name, boldTextFieldStyle); + if (newName != this.currentNucleus.name) { + this.currentNucleus.name = newName; + this.prefab.RefreshOutputs(); + outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); + anythingChanged = true; + } + + if (Application.isPlaying) { + if (currentNucleus is Neuron currentNeuron1) { + GUIContent nameLabel = new("Output", currentNeuron1.outputValue.ToString()); + EditorGUILayout.FloatField(nameLabel, currentNeuron1.outputMagnitude); + } + else + EditorGUILayout.LabelField(" "); + } + else + EditorGUILayout.LabelField(" "); + + if (this.currentNucleus is MemoryCell memory) { + memory.staticMemory = EditorGUILayout.Toggle("Static Memory", memory.staticMemory); + } + + if (this.currentNucleus is IReceptor receptor1) { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.IntField("Array size", receptor1.nucleiArray.Count()); + if (GUILayout.Button("Add")) { + Undo.RecordObject(prefabAsset, "Array add " + prefabAsset.name); + receptor1.AddReceptorElement(this.prefab); + anythingChanged = true; + } + if (GUILayout.Button("Del")) { + Undo.RecordObject(prefabAsset, "Array delete " + prefabAsset.name); + receptor1.RemoveReceptorElement(); + anythingChanged = true; + } + EditorGUILayout.EndHorizontal(); + } + + // Synapses + + if (this.currentNucleus is not Receptor && this.currentNucleus is not ClusterReceptor) { + showSynapses = EditorGUILayout.BeginFoldoutHeaderGroup(showSynapses, "Synapses"); + if (showSynapses) { + if (this.currentNucleus is Neuron neuron2) { + Neuron.CombinatorType newCombinator = (Neuron.CombinatorType)EditorGUILayout.EnumPopup("Combinator", neuron2.combinator); + anythingChanged |= newCombinator != neuron2.combinator; + neuron2.combinator = newCombinator; + } + + EditorGUIUtility.wideMode = true; + EditorGUIUtility.labelWidth = 100; + Vector3 newBias = EditorGUILayout.Vector3Field("Bias", this.currentNucleus.bias); + anythingChanged |= newBias != this.currentNucleus.bias; + this.currentNucleus.bias = newBias; + + Nucleus[] array = null; + int elementIx = -1; + if (this.currentNucleus.synapses.Count > 0) { + Synapse[] synapses = this.currentNucleus.synapses.ToArray(); + foreach (Synapse synapse in synapses) { + if (synapse.neuron == null) + continue; + + if (array != null) { + if (synapse.neuron.parent is Cluster iCluster && elementIx > 0) { + int thisElementIx = Cluster.GetNucleusIndex(iCluster.clusterNuclei, synapse.neuron); + if (thisElementIx == elementIx) + continue; + else + elementIx = thisElementIx; + } + // if (array.Contains(synapse.nucleus)) + // continue; + else if (array.Contains(synapse.neuron.parent)) + continue; + } + else { + if (synapse.neuron.parent is IReceptor iReceptor) { + array = iReceptor.nucleiArray; + if (iReceptor is Cluster iCluster) + elementIx = Cluster.GetNucleusIndex(iCluster.clusterNuclei, synapse.neuron); + } + // else if (synapse.nucleus is Receptor receptor2) // && receptor2.array != null && receptor2.array.nuclei.Length > 1) + // array = receptor2.nucleiArray; + } + + EditorGUILayout.Space(); + + if (Application.isPlaying) { + if (synapse.neuron is Neuron synapseNeuron) { + Vector3 value = synapseNeuron.outputValue * synapse.weight; + GUIContent synapseValueLabel = new(synapse.neuron.name, synapseNeuron.outputValue.ToString()); + EditorGUILayout.FloatField(synapseValueLabel, synapseNeuron.outputMagnitude); + } + } + else { + EditorGUILayout.BeginHorizontal(); + + if (synapse.neuron.parent != null && synapse.neuron.parent != this.currentNucleus) { + // If it is a cluster + GUIStyle labelStyle = new(GUI.skin.label); + float labelWidth = 200; + if (synapse.neuron.clusterPrefab != null) { + labelWidth = labelStyle.CalcSize(new GUIContent($"{synapse.neuron.parent.baseName}.")).x; + GUILayout.Label($"{synapse.neuron.parent.baseName}", GUILayout.Width(labelWidth)); + } + string[] options = synapse.neuron.parent.clusterNuclei.Select(n => n.name).ToArray(); + int selectedIndex = System.Array.IndexOf(options, synapse.neuron.name); + int newIndex = EditorGUILayout.Popup(selectedIndex, options); + if (newIndex != selectedIndex && synapse.neuron.parent.clusterNuclei[newIndex] is Neuron newNeuron) + ChangeSynapse(synapse, newNeuron); + } + else + GUILayout.Label(synapse.neuron.name); + + bool disconnecting = GUILayout.Button("Disconnect", GUILayout.Width(80)); + if (disconnecting && synapse.neuron is Neuron synapseNeuron) { + synapseNeuron.RemoveReceiver(this.currentNucleus); + this.prefab.GarbageCollection(); + anythingChanged = true; + } + EditorGUILayout.EndHorizontal(); + + } + + EditorGUI.indentLevel++; + float newWeight = EditorGUILayout.FloatField("Weight", synapse.weight); + if (newWeight != synapse.weight) { + if (synapse.neuron.parent is IReceptor receptor) { + Nucleus[] receptorArray = receptor.nucleiArray; + foreach (Synapse s in this.currentNucleus.synapses) { + if (s.neuron.parent is IReceptor r && r.nucleiArray == receptorArray) + s.weight = newWeight; + } + } + else + synapse.weight = newWeight; + anythingChanged = true; + } + EditorGUI.indentLevel--; + } + } + + EditorGUILayout.Space(); + anythingChanged |= ConnectNucleus(this.prefab, this.currentNucleus); + anythingChanged |= AddSynapse(this.prefab, this.currentNucleus); + } + EditorGUILayout.EndFoldoutHeaderGroup(); + } + + // Activation + + if (this.currentNucleus is not Cluster) { + EditorGUILayout.Space(); + showActivation = EditorGUILayout.BeginFoldoutHeaderGroup(showActivation, "Activation"); + if (showActivation) { + if (this.currentNucleus is Neuron neuron) { + if (this.currentNucleus is not MemoryCell) { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Activation Curve", GUILayout.Width(150)); + if (neuron.curveMax > 0) + EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, 0, 1, neuron.curveMax)); + else + EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, neuron.curveMax, 1, -neuron.curveMax)); + Neuron.CurvePresets newPreset = (Neuron.CurvePresets)EditorGUILayout.EnumPopup(neuron.curvePreset, GUILayout.Width(100)); + anythingChanged |= newPreset != neuron.curvePreset; + neuron.curvePreset = newPreset; + EditorGUILayout.EndHorizontal(); + } + if (neuron is Receptor receptor2) { + if (receptor2.nucleiArray == null || receptor2.nucleiArray.Count() == 0) + receptor2.array = new NucleusArray(neuron); + } + } + + EditorGUILayout.Space(); + } + EditorGUILayout.EndFoldoutHeaderGroup(); + } + + if (GUILayout.Button("Delete this neuron")) + DeleteNucleus(this.currentNucleus); + + if (this.currentNucleus is Cluster subCluster) { + if (GUILayout.Button("Edit Cluster")) + EditCluster(subCluster); + } + + EditorGUILayout.Space(); + breakOnWake = EditorGUILayout.Toggle("Break on wake", breakOnWake); + if (breakOnWake && this.currentNucleus is Neuron currentNeuron) { + if (currentNeuron.isSleeping == false) + Debug.Break(); + } + trace = EditorGUILayout.Toggle("Trace", trace); + this.currentNucleus.trace = trace; + + serializedObject.ApplyModifiedProperties(); + if (anythingChanged) { + EditorUtility.SetDirty(prefabAsset); + AssetDatabase.SaveAssets(); + } + } + + void OnSceneGUI(SceneView sceneView) { + if (this.gameObject != null) { + 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.color = Color.yellow; + Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector); + } + } + } + } + + #region Synapses + + protected virtual void AddInput(Nucleus.Type selectedType, Nucleus nucleus) { + switch (selectedType) { + case Nucleus.Type.Neuron: + AddNeuronInput(nucleus); + break; + case Nucleus.Type.MemoryCell: + AddMemoryCellInput(nucleus); + break; + // case Nucleus.Type.Selector: + // AddSelectorInput(nucleus); + // break; + case Nucleus.Type.Cluster: + AddClusterInput(nucleus); + break; + // case Nucleus.Type.Pulsar: + // AddPulsarInput(nucleus); + // break; + case Nucleus.Type.Receptor: + AddReceptorInput(nucleus); + break; + // case Nucleus.Type.ReceptorArray: + // AddReceptorArrayInput(nucleus); + // break; + case Nucleus.Type.ClusterReceptor: + AddClusterReceptorInput(nucleus); + break; + default: + break; + } + } + + protected virtual void AddNeuronInput(Nucleus nucleus) { + Neuron newNeuroid = new(this.prefab, "New neuron"); + newNeuroid.AddReceiver(nucleus); + this.currentNucleus = newNeuroid; + BuildLayers(); + } + + protected virtual void AddMemoryCellInput(Nucleus nucleus) { + MemoryCell newMemory = new(this.prefab, "New memory cell"); + newMemory.AddReceiver(nucleus); + this.currentNucleus = newMemory; + BuildLayers(); + } + + protected virtual void AddClusterInput(Nucleus nucleus) { + ClusterPickerWindow.ShowPicker(brain => OnClusterPicked(nucleus, brain), "Select Cluster"); + } + private void OnClusterPicked(Nucleus nucleus, ClusterPrefab prefab) { + Cluster subclusterInstance = new(prefab, this.prefab); + subclusterInstance.defaultOutput.AddReceiver(nucleus); + } + + protected virtual void AddReceptorInput(Nucleus nucleus) { + Receptor newReceptor = new(this.prefab, "New Receptor"); + newReceptor.AddReceiver(nucleus); + this.currentNucleus = newReceptor; + BuildLayers(); + } + + protected virtual void AddClusterReceptorInput(Nucleus nucleus) { + ClusterPickerWindow.ShowPicker(prefab => OnClusterReceptorPicked(nucleus, prefab), "Select Cluster"); + } + private void OnClusterReceptorPicked(Nucleus nucleus, ClusterPrefab selectedPrefab) { + ClusterReceptor clusterInstance = new(selectedPrefab, this.prefab, "New " + selectedPrefab.name); + clusterInstance.defaultOutput.AddReceiver(nucleus); + this.currentNucleus = clusterInstance; + BuildLayers(); + } + + private void EditCluster(Cluster subCluster) { + // May be used with storedPrefab... + Selection.activeObject = subCluster.prefab; + EditorGUIUtility.PingObject(subCluster.prefab); + var editor = Editor.CreateEditor(subCluster.prefab); + } + + int selectedConnectNucleus = -1; + // Connect to another nucleus in the same cluster + protected virtual bool ConnectNucleus(ClusterPrefab cluster, Nucleus nucleusToConnect) { + if (cluster == null) + return false; + + IEnumerable synapseNuclei = this.currentNucleus.synapses + .Where(synapse => synapse.neuron != null) + .Select(synapse => synapse.neuron); + + IEnumerable nuclei = cluster.nuclei + .Except(synapseNuclei); + IEnumerable nucleiNames = nuclei + .Select(n => { + int idx = n.name.IndexOf(':'); + return idx < 0 ? n.name : n.name[..idx]; + }) + .Distinct(); + + string[] names = nucleiNames.ToArray(); + EditorGUILayout.BeginHorizontal(); + selectedConnectNucleus = EditorGUILayout.Popup(selectedConnectNucleus, names); + bool connecting = GUILayout.Button("Connect", GUILayout.Width(80)); + EditorGUILayout.EndHorizontal(); + if (connecting) { + Nucleus nucleus = nuclei.ElementAt(selectedConnectNucleus); + if (nucleus is IReceptor receptor) + receptor.AddArrayReceiver(this.currentNucleus); + else if (nucleus is Neuron neuron) + neuron.AddReceiver(this.currentNucleus); + else if (nucleus is Cluster subCluster) + subCluster.defaultOutput.AddReceiver(this.currentNucleus); + + } + return connecting; + } + + protected virtual void DeleteNucleus(Nucleus nucleus) { + if (nucleus == null) + return; + + if (nucleus is Neuron neuron) { + foreach (Nucleus receiver in neuron.receivers) { + if (receiver != null) { + this.currentNucleus = receiver; + break; + } + } + } + this.prefab.nuclei.Remove(nucleus); + + if (outputsField.value == nucleus.name) { + this.prefab.RefreshOutputs(); + outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); + outputsField.index = 0; + } + + Neuron.Delete(nucleus); + + this.currentNucleus = this.prefab.output; + BuildLayers(); + } + + Nucleus.Type selectedType = Nucleus.Type.None; + protected virtual bool AddSynapse(ClusterPrefab cluster, Nucleus nucleus) { + if (cluster == null) + return false; + + EditorGUILayout.BeginHorizontal(); + selectedType = (Nucleus.Type)EditorGUILayout.EnumPopup(selectedType); + bool connecting = GUILayout.Button("Add", GUILayout.Width(80)); + EditorGUILayout.EndHorizontal(); + + if (connecting) { + AddInput(selectedType, this.currentNucleus); + } + return connecting; + // if (selectedType == Nucleus.Type.None) + // return false; + + // AddInput(selectedType, this.currentNucleus); + // return true; + } + + protected virtual void ChangeSynapse(Synapse synapse, Neuron newNucleus) { + Neuron synapseNeuron = synapse.neuron as Neuron; + if (synapse.neuron.parent is Cluster subCluster && subCluster.prefab != this.prefab) { + if (synapse.neuron.parent is ClusterReceptor receptor) { + // the new nucleus is part of a (cluster) receptor, + // so we have to change all synapses to this nucleus array elements + int oldNucleusIx = Cluster.GetNucleusIndex(subCluster.clusterNuclei, synapse.neuron); + int newNucleusIx = Cluster.GetNucleusIndex(subCluster.clusterNuclei, newNucleus); + foreach (Nucleus element in receptor.nucleiArray) { + if (element is not ClusterReceptor clusterReceptor) + continue; + // Get the same neuron as the synapse.nucleus in a different element + // of the ClusterReceptor array + Nucleus oldElementNucleus = clusterReceptor.clusterNuclei[oldNucleusIx]; + if (oldElementNucleus is not Neuron oldElementNeuron) + continue; + // Get the same neuron as newNucleus in a different element + // of the ClusterReceptor array + Nucleus newElementNucleus = clusterReceptor.clusterNuclei[newNucleusIx]; + if (newElementNucleus is not Neuron newElementNeuron) + continue; + + oldElementNeuron.RemoveReceiver(this.currentNucleus); + newElementNeuron.AddReceiver(this.currentNucleus); + // Now find the synapse which pointed to the old Neuron + // Synapse synapseForUpdate = this.currentNucleus.GetSynapse(oldElementNeuron); + // synapseForUpdate.nucleus = newElementNeuron; + } + } + else { + // it is a neuron in a subcluster + synapseNeuron.RemoveReceiver(this.currentNucleus); + newNucleus.AddReceiver(this.currentNucleus); + } + } + else { + synapseNeuron.RemoveReceiver(this.currentNucleus); + newNucleus.AddReceiver(this.currentNucleus); + } + } + + protected virtual void DisconnectNucleus(Neuron nucleus) { + if (this.currentNucleus.clusterPrefab == null) + return; + string[] names = this.currentNucleus.synapses.Select(synapse => synapse.neuron.name).ToArray(); + int selectedIndex = -1; + selectedIndex = EditorGUILayout.Popup("Disconnect from", selectedIndex, names); + if (selectedIndex >= 0 && selectedIndex < this.currentNucleus.clusterPrefab.nuclei.Count) { + Synapse synapse = this.currentNucleus.synapses[selectedIndex]; + Neuron synapseNeuron = synapse.neuron as Neuron; + synapseNeuron.RemoveReceiver(this.currentNucleus); + } + } + + #endregion Synapses + } + } - } - - public class NeuroidLayer { - public int ix = 0; - public List neuroids = new(); - } - + public class NeuroidLayer { + public int ix = 0; + public List neuroids = new(); + } + */ } \ No newline at end of file diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index 7edadff..510de71 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -10,16 +10,17 @@ namespace NanoBrain { public class ClusterViewer : Editor { public class GraphView : VisualElement { - readonly ClusterPrefab prefab; - SerializedObject serializedBrain; - Nucleus currentNucleus; - GameObject gameObject; + protected readonly ClusterPrefab prefab; + protected SerializedObject serializedBrain; + protected Nucleus currentNucleus; + protected GameObject gameObject; private List layers = new(); private readonly Dictionary neuroidPositions = new(); private bool expandArray = false; - ClusterPrefab prefabAsset; - readonly PopupField outputsField; + protected ClusterPrefab prefabAsset; + protected VisualElement outputContainer; + protected readonly PopupField outputsField; public GraphView(ClusterPrefab prefab) { this.prefab = prefab; @@ -35,7 +36,7 @@ namespace NanoBrain { graphContainer.focusable = true; Add(graphContainer); - VisualElement outputContainer = new() { + outputContainer = new() { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, @@ -108,7 +109,7 @@ namespace NanoBrain { //DrawInspector(inspectorContainer); } - private void BuildLayers() { + protected void BuildLayers() { // A temporary list to track what's been added to layers this.layers = new(); int layerIx = 0; @@ -534,4 +535,9 @@ namespace NanoBrain { } } + + public class NeuroidLayer { + public int ix = 0; + public List neuroids = new(); + } } \ No newline at end of file diff --git a/Runtime/Scripts/Core/Neuron.cs b/Runtime/Scripts/Core/Neuron.cs index fcadfea..9cc021f 100644 --- a/Runtime/Scripts/Core/Neuron.cs +++ b/Runtime/Scripts/Core/Neuron.cs @@ -61,16 +61,17 @@ namespace NanoBrain { /// /// The type of /// - public enum CurvePresets { + public enum ActivationFunction { Linear, Power, Sqrt, Reciprocal, + Tanh, Custom } [SerializeField] - public CurvePresets _curvePreset; - public CurvePresets curvePreset { + public ActivationFunction _curvePreset; + public ActivationFunction curvePreset { get { return _curvePreset; } set { _curvePreset = value; @@ -82,18 +83,21 @@ namespace NanoBrain { public AnimationCurve GenerateCurve() { switch (this.curvePreset) { - case CurvePresets.Linear: + case ActivationFunction.Linear: this.curveMax = 1; return Presets.Linear(1); - case CurvePresets.Power: + case ActivationFunction.Power: this.curveMax = 1; return Presets.Power(2.0f, 1); - case CurvePresets.Sqrt: + case ActivationFunction.Sqrt: this.curveMax = 1; return Presets.Power(0.5f, 1); - case CurvePresets.Reciprocal: + case ActivationFunction.Reciprocal: this.curveMax = 1 / 0.01f * 1; return Presets.Reciprocal(1); + case ActivationFunction.Tanh: + this.curveMax = 1; + return Presets.Tanh(1); default: this.curveMax = 1; return this.curve; @@ -142,6 +146,25 @@ namespace NanoBrain { } return curve; } + 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; + + } } #endregion Serialization @@ -348,10 +371,11 @@ namespace NanoBrain { #if UNITY_MATHEMATICS public Func Activator => this.curvePreset switch { - CurvePresets.Linear => ActivatorLinear, - CurvePresets.Sqrt => ActivatorSqrt, - CurvePresets.Power => ActivatorPower, - CurvePresets.Reciprocal => ActivatorReciprocal, + ActivationFunction.Linear => ActivatorLinear, + ActivationFunction.Sqrt => ActivatorSqrt, + ActivationFunction.Power => ActivatorPower, + ActivationFunction.Reciprocal => ActivatorReciprocal, + ActivationFunction.Tanh => ActivatorTanh, _ => ActivatorCustom }; @@ -378,6 +402,12 @@ namespace NanoBrain { return result; } + protected float3 ActivatorTanh(float3 input) { + float magnitude = length(input); + float3 result = normalize(input) * MathF.Tanh(magnitude); + return result; + } + protected float3 ActivatorCustom(float3 input) { float activatedValue = this.curve.Evaluate(length(input)); float3 result = normalize(input) * activatedValue; From 0023920ffa6d3798ca2b6a80bee1f487508d9b50 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Wed, 15 Apr 2026 12:23:52 +0200 Subject: [PATCH 05/34] Cover seeking(-ish) behaviour --- Editor/Brain_Editor.cs | 26 ++++++++--------- Editor/ClusterInspector.cs | 2 +- Runtime/Scripts/Brain.cs | 10 ++++--- Runtime/Scripts/Core/Neuron.cs | 51 +++++++++++++++++++++++++--------- 4 files changed, 58 insertions(+), 31 deletions(-) diff --git a/Editor/Brain_Editor.cs b/Editor/Brain_Editor.cs index 426990b..a0de070 100644 --- a/Editor/Brain_Editor.cs +++ b/Editor/Brain_Editor.cs @@ -14,13 +14,11 @@ namespace NanoBrain { protected Brain component; private SerializedProperty brainProp; - //ClusterInspector.GraphView board; - public void OnEnable() { component = target as Brain; if (Application.isPlaying == false && serializedObject != null) { - string propertyName = nameof(Brain.defaultBrain); + string propertyName = nameof(Brain.brainPrefab); brainProp = serializedObject.FindProperty(propertyName); } } @@ -32,13 +30,22 @@ namespace NanoBrain { serializedObject.Update(); - VisualElement root = new(); - if (Application.isPlaying == false) { + VisualElement root = new() { + style = { + paddingLeft = 0, + paddingRight = 0, + paddingTop = 0, + paddingBottom = 0 + } + }; + root.styleSheets.Add(Resources.Load("GraphStyles")); + + //if (Application.isPlaying == false) { PropertyField brainField = new(brainProp) { label = "Cluster Prefab" }; root.Add(brainField); - } + //} if (brain != null) CreateViewer(root, brain.prefab, brain.defaultOutput, component.gameObject); @@ -49,13 +56,6 @@ namespace NanoBrain { } public static ClusterViewer.GraphView CreateViewer(VisualElement root, ClusterPrefab cluster, Nucleus output, GameObject gameObject) { - root.style.paddingLeft = 0; - root.style.paddingRight = 0; - root.style.paddingTop = 0; - root.style.paddingBottom = 0; - - root.styleSheets.Add(Resources.Load("GraphStyles")); - VisualElement mainContainer = new() { style = { flexDirection = FlexDirection.Row, diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterInspector.cs index d7a4008..a534b6d 100644 --- a/Editor/ClusterInspector.cs +++ b/Editor/ClusterInspector.cs @@ -316,7 +316,7 @@ namespace NanoBrain { EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, 0, 1, neuron.curveMax)); else EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, neuron.curveMax, 1, -neuron.curveMax)); - Neuron.ActivationFunction newPreset = (Neuron.ActivationFunction)EditorGUILayout.EnumPopup(neuron.curvePreset, GUILayout.Width(100)); + Neuron.ActivationType newPreset = (Neuron.ActivationType)EditorGUILayout.EnumPopup(neuron.curvePreset, GUILayout.Width(100)); anythingChanged |= newPreset != neuron.curvePreset; neuron.curvePreset = newPreset; EditorGUILayout.EndHorizontal(); diff --git a/Runtime/Scripts/Brain.cs b/Runtime/Scripts/Brain.cs index cd0c668..93b05a7 100644 --- a/Runtime/Scripts/Brain.cs +++ b/Runtime/Scripts/Brain.cs @@ -11,7 +11,7 @@ namespace NanoBrain { /// /// The Cluster prefab from which the cluster is created /// - public ClusterPrefab defaultBrain; + public ClusterPrefab brainPrefab; [NonSerialized] private Cluster brainInstance; @@ -20,10 +20,12 @@ namespace NanoBrain { /// public Cluster brain { get { - if (brainInstance == null && defaultBrain != null) { - brainInstance = new Cluster(defaultBrain) { - name = defaultBrain.name + " (Instance)" + if (brainInstance == null && brainPrefab != null) { + brainInstance = new Cluster(brainPrefab) { + name = brainPrefab.name + " (Instance)" }; + } else if (brainInstance != null && brainPrefab == null) { + brainInstance = null; } return brainInstance; } diff --git a/Runtime/Scripts/Core/Neuron.cs b/Runtime/Scripts/Core/Neuron.cs index 9cc021f..fa48b56 100644 --- a/Runtime/Scripts/Core/Neuron.cs +++ b/Runtime/Scripts/Core/Neuron.cs @@ -61,17 +61,19 @@ namespace NanoBrain { /// /// The type of /// - public enum ActivationFunction { + public enum ActivationType { Linear, Power, Sqrt, Reciprocal, Tanh, + Binary, + Normalized, Custom } [SerializeField] - public ActivationFunction _curvePreset; - public ActivationFunction curvePreset { + public ActivationType _curvePreset; + public ActivationType curvePreset { get { return _curvePreset; } set { _curvePreset = value; @@ -83,21 +85,27 @@ namespace NanoBrain { public AnimationCurve GenerateCurve() { switch (this.curvePreset) { - case ActivationFunction.Linear: + case ActivationType.Linear: this.curveMax = 1; return Presets.Linear(1); - case ActivationFunction.Power: + case ActivationType.Power: this.curveMax = 1; return Presets.Power(2.0f, 1); - case ActivationFunction.Sqrt: + case ActivationType.Sqrt: this.curveMax = 1; return Presets.Power(0.5f, 1); - case ActivationFunction.Reciprocal: + case ActivationType.Reciprocal: this.curveMax = 1 / 0.01f * 1; return Presets.Reciprocal(1); - case ActivationFunction.Tanh: + 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; @@ -165,6 +173,9 @@ namespace NanoBrain { return curve; } + public static AnimationCurve Binary() { + return AnimationCurve.Linear(0, 0, 1, 1); + } } #endregion Serialization @@ -371,11 +382,13 @@ namespace NanoBrain { #if UNITY_MATHEMATICS public Func Activator => this.curvePreset switch { - ActivationFunction.Linear => ActivatorLinear, - ActivationFunction.Sqrt => ActivatorSqrt, - ActivationFunction.Power => ActivatorPower, - ActivationFunction.Reciprocal => ActivatorReciprocal, - ActivationFunction.Tanh => ActivatorTanh, + ActivationType.Linear => ActivatorLinear, + ActivationType.Sqrt => ActivatorSqrt, + ActivationType.Power => ActivatorPower, + ActivationType.Reciprocal => ActivatorReciprocal, + ActivationType.Tanh => ActivatorTanh, + ActivationType.Binary => ActivatorBinary, + ActivationType.Normalized => ActivatorNormalized, _ => ActivatorCustom }; @@ -407,6 +420,18 @@ namespace NanoBrain { float3 result = normalize(input) * MathF.Tanh(magnitude); return result; } + protected float3 ActivatorBinary(float3 input) { + float magnitude = length(input); + float value = Mathf.Clamp01(magnitude); + return float3(value, value, value); + } + + protected float3 ActivatorNormalized(float3 input) { + if (lengthsq(input) == 0) + return input; + float3 result = normalize(input); + return result; + } protected float3 ActivatorCustom(float3 input) { float activatedValue = this.curve.Evaluate(length(input)); From 1fc75a814304fc094681b8bc59992eca2a3f6735 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Wed, 15 Apr 2026 17:06:45 +0200 Subject: [PATCH 06/34] Added ClusterArray --- Editor/ClusterInspector.cs | 33 +- Editor/ClusterViewer.cs | 22 ++ Runtime/Scripts/Core/Cluster.cs | 3 + Runtime/Scripts/Core/ClusterArray.cs | 109 +++++++ Runtime/Scripts/Core/ClusterArray.cs.meta | 2 + Runtime/Scripts/Core/ClusterReceptor.cs | 367 +++++++++++----------- Runtime/Scripts/Core/Nucleus.cs | 1 + Runtime/Scripts/Core/Receptor.cs | 4 +- 8 files changed, 347 insertions(+), 194 deletions(-) create mode 100644 Runtime/Scripts/Core/ClusterArray.cs create mode 100644 Runtime/Scripts/Core/ClusterArray.cs.meta diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterInspector.cs index a534b6d..c90c5cf 100644 --- a/Editor/ClusterInspector.cs +++ b/Editor/ClusterInspector.cs @@ -188,6 +188,20 @@ namespace NanoBrain { anythingChanged = true; } EditorGUILayout.EndHorizontal(); + } else if (this.currentNucleus is Cluster cluster && cluster.clusterArray != null) { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.IntField("Array size", cluster.clusterArray.clusters.Count()); + if (GUILayout.Button("Add")) { + Undo.RecordObject(prefabAsset, "Array add " + prefabAsset.name); + cluster.clusterArray.Add(this.prefab); + anythingChanged = true; + } + if (GUILayout.Button("Del")) { + Undo.RecordObject(prefabAsset, "Array delete " + prefabAsset.name); + cluster.clusterArray.Remove(); + anythingChanged = true; + } + EditorGUILayout.EndHorizontal(); } // Synapses @@ -387,24 +401,18 @@ namespace NanoBrain { case Nucleus.Type.MemoryCell: AddMemoryCellInput(nucleus); break; - // case Nucleus.Type.Selector: - // AddSelectorInput(nucleus); - // break; case Nucleus.Type.Cluster: AddClusterInput(nucleus); break; - // case Nucleus.Type.Pulsar: - // AddPulsarInput(nucleus); - // break; case Nucleus.Type.Receptor: AddReceptorInput(nucleus); break; - // case Nucleus.Type.ReceptorArray: - // AddReceptorArrayInput(nucleus); - // break; case Nucleus.Type.ClusterReceptor: AddClusterReceptorInput(nucleus); break; + case Nucleus.Type.ClusterArray: + AddClusterArrayInput(nucleus); + break; default: break; } @@ -456,6 +464,13 @@ namespace NanoBrain { var editor = Editor.CreateEditor(subCluster.prefab); } + protected virtual void AddClusterArrayInput(Nucleus nucleus) { + ClusterPickerWindow.ShowPicker(prefab => OnPickedClusterArray(nucleus, prefab), "Select Cluster"); + } + private void OnPickedClusterArray(Nucleus nucleus, ClusterPrefab selectedPrefab) { + _ = new ClusterArray(selectedPrefab, this.prefab, 1, nucleus); + } + int selectedConnectNucleus = -1; // Connect to another nucleus in the same cluster protected virtual bool ConnectNucleus(ClusterPrefab cluster, Nucleus nucleusToConnect) { diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index 510de71..a9e5d96 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -439,12 +439,34 @@ namespace NanoBrain { style.normal.textColor = Color.white; } } + else if (nucleus is Cluster cluster && cluster.clusterArray != null) { + if (expandArray) { + // Put array indices above elements + style.alignment = TextAnchor.LowerCenter; + Vector3 labelPos1 = position + Vector3.down * (size + 5); // below disc + int colonPos1 = nucleus.name.IndexOf(":"); + if (colonPos1 > 0) { + string extName = nucleus.name[(colonPos1 + 2)..]; + Handles.Label(labelPos1, extName, style); + } + } + else { + // draw the array size label + if (color.grayscale > 0.5f) + style.normal.textColor = Color.black; + else + style.normal.textColor = Color.white; + Handles.Label(labelPosition, cluster.clusterArray.clusters.Length.ToString(), style); + style.normal.textColor = Color.white; + } + } if (expandArray == false || nucleus is not IReceptor) { // put name below nucleus Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron style.alignment = TextAnchor.UpperCenter; + 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 diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index f8ef85a..4626320 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -28,6 +28,9 @@ public class Cluster : Nucleus { } } + [SerializeReference] + public ClusterArray clusterArray; + #region Init /// diff --git a/Runtime/Scripts/Core/ClusterArray.cs b/Runtime/Scripts/Core/ClusterArray.cs new file mode 100644 index 0000000..fec9b23 --- /dev/null +++ b/Runtime/Scripts/Core/ClusterArray.cs @@ -0,0 +1,109 @@ +using UnityEngine; + +namespace NanoBrain { + + public class ClusterArray : Nucleus { + + public ClusterPrefab prefab; + public Cluster[] clusters; + + public ClusterArray(ClusterPrefab prefab, Cluster parent, int size, Nucleus receiver = null) { + this.prefab = prefab; + this.name = prefab.name; + this.clusters = new Cluster[size]; + for (int ix = 0; ix < size; ix++) { + Cluster cluster = new(prefab, parent); + cluster.defaultOutput.AddReceiver(receiver); + cluster.clusterArray = this; + this.clusters[ix] = cluster; + } + } + + public ClusterArray(ClusterPrefab prefab, ClusterPrefab parent, int size, Nucleus receiver = null) { + this.prefab = prefab; + this.name = prefab.name; + this.clusters = new Cluster[size]; + for (int ix = 0; ix < size; ix++) { + Cluster cluster = new(prefab, parent); + cluster.defaultOutput.AddReceiver(receiver); + cluster.clusterArray = this; + this.clusters[ix] = cluster; + } + } + + public override Nucleus ShallowCloneTo(Cluster parent) { + ClusterArray clone = new(this.prefab, parent, this.clusters.Length) { + clusterPrefab = this.clusterPrefab, + }; + + return clone; + } + + public override Nucleus Clone(ClusterPrefab parent) { + ClusterArray clone = new(this.prefab, parent, this.clusters.Length) { + }; + + return clone; + } + + public void Add(ClusterPrefab prefab) { + if (this.clusters.Length == 0) { + Debug.LogError("Empty perceptoid array, cannot add"); + } + int newLength = this.clusters.Length + 1; + Cluster[] newArray = new Cluster[newLength]; + + string baseName = this.name; + int colonPos = baseName.IndexOf(":"); + if (colonPos > 0) + baseName = baseName[..colonPos]; + + for (int i = 0; i < this.clusters.Length; i++) + newArray[i] = this.clusters[i]; + Cluster cluster = this.clusters[0]; + newArray[newLength - 1] = cluster.Clone(prefab) as Cluster; + newArray[newLength - 1].name = $"{baseName}: {newLength - 1}"; + } + + public void Remove() { + int newLength = this.clusters.Length - 1; + if (newLength == 0) { + Debug.LogWarning("Perceptoid array cannot be empty"); + } + Cluster[] newArray = new Cluster[newLength]; + for (int i = 0; i < newLength; i++) + newArray[i] = this.clusters[i]; + // Delete the last perception + //Cluster.Delete(nucleus); + } + + public override void UpdateStateIsolated() { + // Clusters don't do anything, + // The nuclei in them do the work + // and should be called directly, not from the cluster + } + + public virtual Cluster GetThingCluster() { + Cluster selectedCluster = SelectCluster(); + return selectedCluster; + } + + private Cluster SelectCluster() { + // Find a sleeping cluster + foreach (Cluster cluster in clusters) { + if (cluster.defaultOutput.isSleeping) + return cluster; + } + + // Otherwise find the stalest cluster? + Cluster stalestCluster = clusters[0]; + for (int ix = 1; ix < clusters.Length; ix++) { + if (clusters[ix].defaultOutput.stale > stalestCluster.defaultOutput.stale) + stalestCluster = clusters[ix]; + } + + return stalestCluster; + } + } + +} \ No newline at end of file diff --git a/Runtime/Scripts/Core/ClusterArray.cs.meta b/Runtime/Scripts/Core/ClusterArray.cs.meta new file mode 100644 index 0000000..8125246 --- /dev/null +++ b/Runtime/Scripts/Core/ClusterArray.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 837dfcffeb99804479a0887cbfc33372 \ No newline at end of file diff --git a/Runtime/Scripts/Core/ClusterReceptor.cs b/Runtime/Scripts/Core/ClusterReceptor.cs index 0dcd8ec..a6c7e52 100644 --- a/Runtime/Scripts/Core/ClusterReceptor.cs +++ b/Runtime/Scripts/Core/ClusterReceptor.cs @@ -9,194 +9,194 @@ using System.Linq; namespace NanoBrain { -[Serializable] -public class ClusterReceptor : Cluster, IReceptor { - public ClusterReceptor(ClusterPrefab prefab, Cluster parent, string name) : base(prefab, parent) { - this.name = name; - this.array = new NucleusArray(this); - if (this.name.IndexOf(":") < 0) - this.name += ": 0"; + [Serializable] + public class ClusterReceptor : Cluster, IReceptor { + public ClusterReceptor(ClusterPrefab prefab, Cluster parent, string name) : base(prefab, parent) { + this.name = name; + this.array = new NucleusArray(this); + if (this.name.IndexOf(":") < 0) + this.name += ": 0"; - } - public ClusterReceptor(ClusterPrefab prefab, ClusterPrefab parent, string name) : base(prefab, parent) { - this.name = name; - this.array = new NucleusArray(this); - } - - public string GetName() { - return this.name; - } - - public override Nucleus ShallowCloneTo(Cluster parent) { - ClusterReceptor clone = new(this.prefab, parent, this.name) { - clusterPrefab = this.clusterPrefab, - }; - - return clone; - } - - public override Nucleus Clone(ClusterPrefab parent) { - ClusterReceptor clone = new(prefab, parent, this.name) { - array = this._array - }; - - foreach (Synapse synapse in this.synapses) { - Synapse clonedSynapse = clone.AddSynapse(synapse.neuron); - clonedSynapse.weight = synapse.weight; + } + public ClusterReceptor(ClusterPrefab prefab, ClusterPrefab parent, string name) : base(prefab, parent) { + this.name = name; + this.array = new NucleusArray(this); } - this._outputs = null; // Make sure the output are regenerated - foreach (Neuron output in this.outputs) { - int ix = GetNucleusIndex(this.clusterNuclei, output); - if (ix < 0 || clone.clusterNuclei[ix] is not Neuron clonedOutput) - continue; - - foreach (Nucleus receiver in output.receivers) - clonedOutput.AddReceiver(receiver); + public string GetName() { + return this.name; } - return clone; - } - public override List CollectReceivers() { - List receivers = new(); - foreach (Nucleus element in this.nucleiArray) { - if (element is not Cluster clusterElement) - continue; + public override Nucleus ShallowCloneTo(Cluster parent) { + ClusterReceptor clone = new(this.prefab, parent, this.name) { + clusterPrefab = this.clusterPrefab, + }; - foreach (Nucleus outputNucleus in clusterElement.clusterNuclei) { - if (outputNucleus is not Neuron output) + return clone; + } + + public override Nucleus Clone(ClusterPrefab parent) { + ClusterReceptor clone = new(prefab, parent, this.name) { + array = this._array + }; + + foreach (Synapse synapse in this.synapses) { + Synapse clonedSynapse = clone.AddSynapse(synapse.neuron); + clonedSynapse.weight = synapse.weight; + } + + this._outputs = null; // Make sure the output are regenerated + foreach (Neuron output in this.outputs) { + int ix = GetNucleusIndex(this.clusterNuclei, output); + if (ix < 0 || clone.clusterNuclei[ix] is not Neuron clonedOutput) continue; - // this should be clusterElement.outputs, - // but outputs is not updated when correctly and may contain old data... - foreach (Nucleus receiver in output.receivers) { - // Only add receivers outside clusterElement cluster - if (receiver.clusterPrefab != clusterElement.prefab && - receivers.Contains(receiver) == false) - receivers.Add(receiver); + foreach (Nucleus receiver in output.receivers) + clonedOutput.AddReceiver(receiver); + } + return clone; + } + + public override List CollectReceivers() { + List receivers = new(); + foreach (Nucleus element in this.nucleiArray) { + if (element is not Cluster clusterElement) + continue; + + foreach (Nucleus outputNucleus in clusterElement.clusterNuclei) { + if (outputNucleus is not Neuron output) + continue; + + // this should be clusterElement.outputs, + // but outputs is not updated when correctly and may contain old data... + foreach (Nucleus receiver in output.receivers) { + // Only add receivers outside clusterElement cluster + if (receiver.clusterPrefab != clusterElement.prefab && + receivers.Contains(receiver) == false) + receivers.Add(receiver); + } } } - } - return receivers; - } - - [SerializeReference] - private NucleusArray _array; - public NucleusArray array { - set { _array = value; } - } - - public Nucleus[] nucleiArray { - get { return _array.nuclei; } - set { _array.nuclei = value; } - } - - public void AddReceptorElement(ClusterPrefab prefab) { - IReceptorHelpers.AddReceptorElement(this, prefab); - } - - public void RemoveReceptorElement() { - IReceptorHelpers.RemoveReceptorElement(this); - } - - public void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1) { - IReceptorHelpers.AddArrayReceiver(this, receiverToAdd, weight); - } - - public override void UpdateStateIsolated() { - // Clusters don't do anything, - // The nuclei in them do the work - // and should be called directly, not from the cluster - } - - public override void UpdateNuclei() { - foreach (Nucleus nucleus in this.clusterNuclei) - nucleus.UpdateNuclei(); - } - - public override void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) { - Debug.LogError("Process Stimulus was called on clusterreceptor without a neuron specified"); - } - - private readonly Dictionary thingReceivers = new(); - - public virtual void ProcessStimulus(Neuron input, Vector3 inputValue, int thingId = 0, string thingName = null) { - CleanupReceivers(); - - if (!thingReceivers.TryGetValue(thingId, out ClusterReceptor selectedReceiver)) - selectedReceiver = FindReceiver2(thingId, inputValue, input); - if (selectedReceiver == null) - return; - - if (thingName != null) { - string baseName = selectedReceiver.name; - int colonPos = selectedReceiver.name.IndexOf(":"); - if (colonPos > 0) - baseName = selectedReceiver.name[..colonPos]; - selectedReceiver.name = baseName + ": " + thingName; + return receivers; } - int inputIx = GetNucleusIndex(this.clusterNuclei, input); - if (inputIx < 0) - return; + [SerializeReference] + private NucleusArray _array; + public NucleusArray array { + set { _array = value; } + } - if (selectedReceiver.clusterNuclei[inputIx] is Neuron selectedNeuron) - selectedNeuron.ProcessStimulusDirect(inputValue); - } + public Nucleus[] nucleiArray { + get { return _array.nuclei; } + set { _array.nuclei = value; } + } + + public void AddReceptorElement(ClusterPrefab prefab) { + IReceptorHelpers.AddReceptorElement(this, prefab); + } + + public void RemoveReceptorElement() { + IReceptorHelpers.RemoveReceptorElement(this); + } + + public void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1) { + IReceptorHelpers.AddArrayReceiver(this, receiverToAdd, weight); + } + + public override void UpdateStateIsolated() { + // Clusters don't do anything, + // The nuclei in them do the work + // and should be called directly, not from the cluster + } + + public override void UpdateNuclei() { + foreach (Nucleus nucleus in this.clusterNuclei) + nucleus.UpdateNuclei(); + } + + public override void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) { + Debug.LogError("Process Stimulus was called on clusterreceptor without a neuron specified"); + } + + private readonly Dictionary thingReceivers = new(); + + public virtual void ProcessStimulus(Neuron input, Vector3 inputValue, int thingId = 0, string thingName = null) { + CleanupReceivers(); + + if (!thingReceivers.TryGetValue(thingId, out ClusterReceptor selectedReceiver)) + selectedReceiver = FindReceiver2(thingId, inputValue, input); + if (selectedReceiver == null) + return; + + if (thingName != null) { + string baseName = selectedReceiver.name; + int colonPos = selectedReceiver.name.IndexOf(":"); + if (colonPos > 0) + baseName = selectedReceiver.name[..colonPos]; + selectedReceiver.name = baseName + ": " + thingName; + } + + int inputIx = GetNucleusIndex(this.clusterNuclei, input); + if (inputIx < 0) + return; + + if (selectedReceiver.clusterNuclei[inputIx] is Neuron selectedNeuron) + selectedNeuron.ProcessStimulusDirect(inputValue); + } #if UNITY_MATHEMATICS - private ClusterReceptor FindReceiver2(int thingId, float3 inputValue, Neuron input) { - // No existing nucleus for this thing - ClusterReceptor selectedReceiver = null; - float selectedMagnitude = 0; - foreach (ClusterReceptor receiver in this.nucleiArray.Cast()) { - if (thingReceivers.ContainsValue(receiver) == false) { - // We found an unusued receiver - thingReceivers.Add(thingId, receiver); - return receiver; - } - else if (receiver.defaultOutput.isSleeping) { - // A sleeping receiver is not active and can therefore always be used - thingReceivers.Add(thingId, receiver); - receiver.bias = float3(0, 0, 0); - return receiver; - } - else if (selectedReceiver == null) { - // If we haven't found a receiver yet, just start by taking the first - selectedReceiver = receiver; - selectedMagnitude = length(selectedReceiver.defaultOutput.outputValue); - } - // Look for the receiver with the lowest output magnitude - else { - float magnitude = length(receiver.defaultOutput.outputValue); - - if (length(receiver.defaultOutput.outputValue) < selectedMagnitude) { + private ClusterReceptor FindReceiver2(int thingId, float3 inputValue, Neuron input) { + // No existing nucleus for this thing + ClusterReceptor selectedReceiver = null; + float selectedMagnitude = 0; + foreach (ClusterReceptor receiver in this.nucleiArray.Cast()) { + if (thingReceivers.ContainsValue(receiver) == false) { + // We found an unusued receiver + thingReceivers.Add(thingId, receiver); + return receiver; + } + else if (receiver.defaultOutput.isSleeping) { + // A sleeping receiver is not active and can therefore always be used + thingReceivers.Add(thingId, receiver); + receiver.bias = float3(0, 0, 0); + return receiver; + } + else if (selectedReceiver == null) { + // If we haven't found a receiver yet, just start by taking the first selectedReceiver = receiver; selectedMagnitude = length(selectedReceiver.defaultOutput.outputValue); } - } - } - if (selectedReceiver != null) { - // To re-initialize the cluster (esp. memory cells) - // we update the cluster neuron twice. - // Bit of a hack..... - int inputIx = GetNucleusIndex(this.clusterNuclei, input); - if (inputIx >= 0) { - if (selectedReceiver.clusterNuclei[inputIx] is Neuron selectedNeuron) - selectedNeuron.ProcessStimulusDirect(inputValue); - } + // Look for the receiver with the lowest output magnitude + else { + float magnitude = length(receiver.defaultOutput.outputValue); - // Replace the receiver - // Find the thingId current associated with the receiver - int keyToRemove = thingReceivers.FirstOrDefault(r => r.Value.Equals(selectedReceiver)).Key; - if (keyToRemove != 0 || thingReceivers.ContainsKey(keyToRemove)) - thingReceivers.Remove(keyToRemove); - // And add the new association - thingReceivers.Add(thingId, selectedReceiver); + if (length(receiver.defaultOutput.outputValue) < selectedMagnitude) { + selectedReceiver = receiver; + selectedMagnitude = length(selectedReceiver.defaultOutput.outputValue); + } + } + } + if (selectedReceiver != null) { + // To re-initialize the cluster (esp. memory cells) + // we update the cluster neuron twice. + // Bit of a hack..... + int inputIx = GetNucleusIndex(this.clusterNuclei, input); + if (inputIx >= 0) { + if (selectedReceiver.clusterNuclei[inputIx] is Neuron selectedNeuron) + selectedNeuron.ProcessStimulusDirect(inputValue); + } + + // Replace the receiver + // Find the thingId current associated with the receiver + int keyToRemove = thingReceivers.FirstOrDefault(r => r.Value.Equals(selectedReceiver)).Key; + if (keyToRemove != 0 || thingReceivers.ContainsKey(keyToRemove)) + thingReceivers.Remove(keyToRemove); + // And add the new association + thingReceivers.Add(thingId, selectedReceiver); + } + return selectedReceiver; } - return selectedReceiver; - } #else @@ -254,24 +254,25 @@ public class ClusterReceptor : Cluster, IReceptor { #endif - private void CleanupReceivers() { - // Remove a thing-receiver connection when the nucleus is inactive - List receiversToRemove = new(); - foreach (KeyValuePair item in thingReceivers) { - if (item.Value != null && item.Value.defaultOutput.isSleeping) - receiversToRemove.Add(item.Key); + private void CleanupReceivers() { + // Remove a thing-receiver connection when the nucleus is inactive + List receiversToRemove = new(); + foreach (KeyValuePair item in thingReceivers) { + if (item.Value != null && item.Value.defaultOutput.isSleeping) + receiversToRemove.Add(item.Key); + } + foreach (int thingId in receiversToRemove) { + Nucleus selectedReceiver = thingReceivers[thingId]; + + thingReceivers.Remove(thingId); + + int colonPos = selectedReceiver.name.IndexOf(":"); + if (colonPos > 0) + selectedReceiver.name = selectedReceiver.name[..colonPos]; + + } } - foreach (int thingId in receiversToRemove) { - Nucleus selectedReceiver = thingReceivers[thingId]; - thingReceivers.Remove(thingId); - - int colonPos = selectedReceiver.name.IndexOf(":"); - if (colonPos > 0) - selectedReceiver.name = selectedReceiver.name[..colonPos]; - - } } -} } \ No newline at end of file diff --git a/Runtime/Scripts/Core/Nucleus.cs b/Runtime/Scripts/Core/Nucleus.cs index d6a0952..b343d43 100644 --- a/Runtime/Scripts/Core/Nucleus.cs +++ b/Runtime/Scripts/Core/Nucleus.cs @@ -56,6 +56,7 @@ public abstract class Nucleus { Cluster, Receptor, ClusterReceptor, + ClusterArray, } #region Synapses diff --git a/Runtime/Scripts/Core/Receptor.cs b/Runtime/Scripts/Core/Receptor.cs index 38a9cdf..ef2e800 100644 --- a/Runtime/Scripts/Core/Receptor.cs +++ b/Runtime/Scripts/Core/Receptor.cs @@ -29,7 +29,7 @@ namespace NanoBrain { public Receptor(ClusterPrefab prefab, string name) : base(prefab, name) { this.array = new NucleusArray(this); } - + public string GetName() { return this.name; } @@ -108,6 +108,6 @@ namespace NanoBrain { this._array ??= new NucleusArray(this.parent); this._array.ProcessStimulus(thingId, inputValue, thingName); } + } - } \ No newline at end of file From b0f4b411e3230b4fd404293b4f0bb6de87a0fed8 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Wed, 15 Apr 2026 17:22:54 +0200 Subject: [PATCH 07/34] Status quo adding clusterArrays --- Editor/ClusterInspector.cs | 1 + Editor/ClusterViewer.cs | 15 ++++++---- Runtime/Scripts/Core/ClusterArray.cs | 45 +++++++++++++++++++++++----- 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterInspector.cs index c90c5cf..4e4d684 100644 --- a/Editor/ClusterInspector.cs +++ b/Editor/ClusterInspector.cs @@ -188,6 +188,7 @@ namespace NanoBrain { anythingChanged = true; } EditorGUILayout.EndHorizontal(); + } else if (this.currentNucleus is Cluster cluster && cluster.clusterArray != null) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.IntField("Array size", cluster.clusterArray.clusters.Count()); diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index a9e5d96..213c25a 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -203,11 +203,11 @@ namespace NanoBrain { 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) - }; + 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; @@ -331,6 +331,10 @@ namespace NanoBrain { continue; drawnArrays.Add(clusterReceptor.nucleiArray); } + // Oops... + // else if (synapse.neuron is Cluster cluster && cluster.clusterArray != null) { + + // } if (synapse.neuron is Neuron synapseNeuron) { float value = synapseNeuron.outputMagnitude * synapse.weight; // Debug.Log($"{synapse.nucleus.name}: {value} {length(synapse.nucleus.outputValue)} {synapse.weight}"); @@ -475,7 +479,6 @@ namespace NanoBrain { } else Handles.Label(labelPos, nucleus.name, style); - } // Draw Cluster ring diff --git a/Runtime/Scripts/Core/ClusterArray.cs b/Runtime/Scripts/Core/ClusterArray.cs index fec9b23..58a9e14 100644 --- a/Runtime/Scripts/Core/ClusterArray.cs +++ b/Runtime/Scripts/Core/ClusterArray.cs @@ -1,12 +1,17 @@ +using System; +using System.Collections.Generic; using UnityEngine; namespace NanoBrain { + [Serializable] public class ClusterArray : Nucleus { public ClusterPrefab prefab; public Cluster[] clusters; + public Dictionary thingClusters = new(); + public ClusterArray(ClusterPrefab prefab, Cluster parent, int size, Nucleus receiver = null) { this.prefab = prefab; this.name = prefab.name; @@ -49,9 +54,10 @@ namespace NanoBrain { public void Add(ClusterPrefab prefab) { if (this.clusters.Length == 0) { Debug.LogError("Empty perceptoid array, cannot add"); + return; } int newLength = this.clusters.Length + 1; - Cluster[] newArray = new Cluster[newLength]; + Cluster[] newClusters = new Cluster[newLength]; string baseName = this.name; int colonPos = baseName.IndexOf(":"); @@ -59,22 +65,25 @@ namespace NanoBrain { baseName = baseName[..colonPos]; for (int i = 0; i < this.clusters.Length; i++) - newArray[i] = this.clusters[i]; + newClusters[i] = this.clusters[i]; Cluster cluster = this.clusters[0]; - newArray[newLength - 1] = cluster.Clone(prefab) as Cluster; - newArray[newLength - 1].name = $"{baseName}: {newLength - 1}"; + newClusters[newLength - 1] = cluster.Clone(prefab) as Cluster; + newClusters[newLength - 1].name = $"{baseName}: {newLength - 1}"; + this.clusters = newClusters; } public void Remove() { int newLength = this.clusters.Length - 1; if (newLength == 0) { Debug.LogWarning("Perceptoid array cannot be empty"); + return; } - Cluster[] newArray = new Cluster[newLength]; + Cluster[] newClusters = new Cluster[newLength]; for (int i = 0; i < newLength; i++) - newArray[i] = this.clusters[i]; + newClusters[i] = this.clusters[i]; // Delete the last perception //Cluster.Delete(nucleus); + this.clusters = newClusters; } public override void UpdateStateIsolated() { @@ -87,12 +96,22 @@ namespace NanoBrain { Cluster selectedCluster = SelectCluster(); return selectedCluster; } + public virtual Cluster GetThingCluster(int thingId, string thingName = null) { + if (thingClusters.TryGetValue(thingId, out Cluster cluster)) + return cluster; + + Cluster selectedCluster = SelectCluster(); + thingClusters[thingId] = selectedCluster; + return selectedCluster; + } private Cluster SelectCluster() { // Find a sleeping cluster foreach (Cluster cluster in clusters) { - if (cluster.defaultOutput.isSleeping) + if (cluster.defaultOutput.isSleeping) { + RemoveThingCluster(cluster); return cluster; + } } // Otherwise find the stalest cluster? @@ -102,8 +121,20 @@ namespace NanoBrain { stalestCluster = clusters[ix]; } + RemoveThingCluster(stalestCluster); return stalestCluster; } + + private void RemoveThingCluster(Cluster cluster) { + List keysToRemove = new(); + foreach (KeyValuePair kvp in thingClusters) { + if (kvp.Value == cluster) + keysToRemove.Add(kvp.Key); + } + + foreach (int thingId in keysToRemove) + thingClusters.Remove(thingId); + } } } \ No newline at end of file From e40dd234f9a83d16141e1513e4296fd839c0f5d8 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Fri, 17 Apr 2026 09:42:03 +0200 Subject: [PATCH 08/34] Fixed clusterViewer for clusterarrays --- Editor/ClusterViewer.cs | 8 +++++--- Runtime/Scripts/Core/ClusterArray.cs | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index 213c25a..dbc6984 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -332,9 +332,11 @@ namespace NanoBrain { drawnArrays.Add(clusterReceptor.nucleiArray); } // Oops... - // else if (synapse.neuron is Cluster cluster && cluster.clusterArray != null) { - - // } + else if (synapse.neuron.parent is Cluster cluster && cluster.clusterArray != null) { + if (drawnArrays.Contains(cluster.clusterArray.clusters)) + continue; + drawnArrays.Add(cluster.clusterArray.clusters); + } if (synapse.neuron is Neuron synapseNeuron) { float value = synapseNeuron.outputMagnitude * synapse.weight; // Debug.Log($"{synapse.nucleus.name}: {value} {length(synapse.nucleus.outputValue)} {synapse.weight}"); diff --git a/Runtime/Scripts/Core/ClusterArray.cs b/Runtime/Scripts/Core/ClusterArray.cs index 58a9e14..490c216 100644 --- a/Runtime/Scripts/Core/ClusterArray.cs +++ b/Runtime/Scripts/Core/ClusterArray.cs @@ -66,9 +66,11 @@ namespace NanoBrain { for (int i = 0; i < this.clusters.Length; i++) newClusters[i] = this.clusters[i]; - Cluster cluster = this.clusters[0]; - newClusters[newLength - 1] = cluster.Clone(prefab) as Cluster; - newClusters[newLength - 1].name = $"{baseName}: {newLength - 1}"; + Cluster sourceCluster = this.clusters[0]; + Cluster newCluster = sourceCluster.Clone(prefab) as Cluster; + newCluster.name = $"{baseName}: {newLength - 1}"; + newCluster.clusterArray = this; + newClusters[newLength - 1] = newCluster; this.clusters = newClusters; } From bc0a79688d863d76748938df0420438cfaf8a605 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Fri, 17 Apr 2026 11:50:10 +0200 Subject: [PATCH 09/34] Integrated clusterarray in cluster --- Editor/ClusterInspector.cs | 40 +- Editor/ClusterViewer.cs | 28 +- Runtime/Scripts/Core/Cluster.cs | 1116 ++++++++++++++------------ Runtime/Scripts/Core/ClusterArray.cs | 11 +- Runtime/Scripts/Core/Nucleus.cs | 4 +- 5 files changed, 654 insertions(+), 545 deletions(-) diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterInspector.cs index 4e4d684..2b94851 100644 --- a/Editor/ClusterInspector.cs +++ b/Editor/ClusterInspector.cs @@ -188,21 +188,25 @@ namespace NanoBrain { anythingChanged = true; } EditorGUILayout.EndHorizontal(); - - } else if (this.currentNucleus is Cluster cluster && cluster.clusterArray != null) { + + } + else if (this.currentNucleus is Cluster cluster) { EditorGUILayout.BeginHorizontal(); - EditorGUILayout.IntField("Array size", cluster.clusterArray.clusters.Count()); + if (cluster.siblingClusters != null && cluster.siblingClusters.Length > 1) + EditorGUILayout.IntField("Array size", cluster.siblingClusters.Count()); + else + EditorGUILayout.IntField("Array size", 1); if (GUILayout.Button("Add")) { Undo.RecordObject(prefabAsset, "Array add " + prefabAsset.name); - cluster.clusterArray.Add(this.prefab); + cluster.AddInstance(this.prefab); anythingChanged = true; } if (GUILayout.Button("Del")) { Undo.RecordObject(prefabAsset, "Array delete " + prefabAsset.name); - cluster.clusterArray.Remove(); + cluster.RemoveInstance(); anythingChanged = true; } - EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndHorizontal(); } // Synapses @@ -408,12 +412,12 @@ namespace NanoBrain { case Nucleus.Type.Receptor: AddReceptorInput(nucleus); break; - case Nucleus.Type.ClusterReceptor: - AddClusterReceptorInput(nucleus); - break; - case Nucleus.Type.ClusterArray: - AddClusterArrayInput(nucleus); - break; + // case Nucleus.Type.ClusterReceptor: + // AddClusterReceptorInput(nucleus); + // break; + // case Nucleus.Type.ClusterArray: + // AddClusterArrayInput(nucleus); + // break; default: break; } @@ -465,12 +469,12 @@ namespace NanoBrain { var editor = Editor.CreateEditor(subCluster.prefab); } - protected virtual void AddClusterArrayInput(Nucleus nucleus) { - ClusterPickerWindow.ShowPicker(prefab => OnPickedClusterArray(nucleus, prefab), "Select Cluster"); - } - private void OnPickedClusterArray(Nucleus nucleus, ClusterPrefab selectedPrefab) { - _ = new ClusterArray(selectedPrefab, this.prefab, 1, nucleus); - } + // protected virtual void AddClusterArrayInput(Nucleus nucleus) { + // ClusterPickerWindow.ShowPicker(prefab => OnPickedClusterArray(nucleus, prefab), "Select Cluster"); + // } + // private void OnPickedClusterArray(Nucleus nucleus, ClusterPrefab selectedPrefab) { + // _ = new ClusterArray(selectedPrefab, this.prefab, 1, nucleus); + // } int selectedConnectNucleus = -1; // Connect to another nucleus in the same cluster diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index dbc6984..c2f55c8 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -331,12 +331,16 @@ namespace NanoBrain { continue; drawnArrays.Add(clusterReceptor.nucleiArray); } - // Oops... - else if (synapse.neuron.parent is Cluster cluster && cluster.clusterArray != null) { - if (drawnArrays.Contains(cluster.clusterArray.clusters)) + else if (synapse.neuron.parent is Cluster cluster && cluster.siblingClusters != null) { + if (drawnArrays.Contains(cluster.siblingClusters)) continue; - drawnArrays.Add(cluster.clusterArray.clusters); + drawnArrays.Add(cluster.siblingClusters); } + // else if (synapse.neuron.parent is Cluster cluster && cluster.clusterArray != null) { + // if (drawnArrays.Contains(cluster.clusterArray.clusters)) + // continue; + // drawnArrays.Add(cluster.clusterArray.clusters); + // } if (synapse.neuron is Neuron synapseNeuron) { float value = synapseNeuron.outputMagnitude * synapse.weight; // Debug.Log($"{synapse.nucleus.name}: {value} {length(synapse.nucleus.outputValue)} {synapse.weight}"); @@ -445,7 +449,7 @@ namespace NanoBrain { style.normal.textColor = Color.white; } } - else if (nucleus is Cluster cluster && cluster.clusterArray != null) { + else if (nucleus is Cluster cluster) { if (expandArray) { // Put array indices above elements style.alignment = TextAnchor.LowerCenter; @@ -457,13 +461,15 @@ namespace NanoBrain { } } else { - // draw the array size label - if (color.grayscale > 0.5f) - style.normal.textColor = Color.black; - else + if (cluster.siblingClusters != null && cluster.siblingClusters.Length > 1) { + // draw the array size label + if (color.grayscale > 0.5f) + style.normal.textColor = Color.black; + else + style.normal.textColor = Color.white; + Handles.Label(labelPosition, cluster.siblingClusters.Length.ToString(), style); style.normal.textColor = Color.white; - Handles.Label(labelPosition, cluster.clusterArray.clusters.Length.ToString(), style); - style.normal.textColor = Color.white; + } } } diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index 4626320..c96ea65 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -8,538 +8,634 @@ using static Unity.Mathematics.math; namespace NanoBrain { -/// -/// A Cluster combines a collection of Nuclei to implement reusable behaviour -/// -/// A Cluster is an instantiation of a ClusterPrefab. -/// Clusters can be nested inside other clusters. -[Serializable] -public class Cluster : Nucleus { - /// - /// The base name of the cluster. I don't think this is actively used at this moment + /// A Cluster combines a collection of Nuclei to implement reusable behaviour /// - public string baseName { - get { - int colonPositon = this.name.IndexOf(':'); - if (colonPositon < 0) - return this.name; - return this.name[..colonPositon]; - } - } + /// A Cluster is an instantiation of a ClusterPrefab. + /// Clusters can be nested inside other clusters. + [Serializable] + public class Cluster : Nucleus { - [SerializeReference] - public ClusterArray clusterArray; - - #region Init - - /// - /// Instantiate a new copy of a ClusterPrefab in the given parent - /// - /// The prefab to use - /// The cluster in which this new cluster will be placed - public Cluster(ClusterPrefab prefab, Cluster parent) { - this.prefab = prefab; - this.name = prefab.name; - - this.parent = parent; - this.parent?.clusterNuclei.Add(this); - - ClonePrefab(); - _ = this.inputs; - this.sortedNuclei = TopologicalSort(this.clusterNuclei); - } - - /// - /// Add a new cluster to a ClusterPrefab - /// - /// The prefab to copy - /// The prefab in which the new copy is placed - public Cluster(ClusterPrefab prefab, ClusterPrefab parent = null) { - this.prefab = prefab; - this.name = prefab.name; - this.clusterPrefab = parent; - - if (this.clusterPrefab != null) - this.clusterPrefab.nuclei.Add(this); - - ClonePrefab(); - _ = this.inputs; - this.sortedNuclei = TopologicalSort(this.clusterNuclei); - } - - /// - /// Clone a prefab. - /// - /// Strange that this does not take any parameters or return values. - /// Where which the clone be found??? - private void ClonePrefab() { - Nucleus[] prefabNuclei = this.prefab.nuclei.ToArray(); - // first clone the nuclei without their connections - foreach (Nucleus nucleus in this.prefab.nuclei) { - nucleus.ShallowCloneTo(this); - } - Nucleus[] clonedNuclei = this.clusterNuclei.ToArray(); - - // Now clone the connections - for (int nucleusIx = 0; nucleusIx < prefabNuclei.Length; nucleusIx++) { - Nucleus prefabNucleus = prefabNuclei[nucleusIx]; - if (prefabNucleus is not Neuron prefabNeuron) - continue; - - Nucleus clonedNucleus = clonedNuclei[nucleusIx]; - if (clonedNucleus == null || clonedNucleus is not Neuron clonedNeuron) - continue; - - // Copy the receivers, which will also create the synapses - // Clusters do not have receivers... - foreach (Nucleus receiver in prefabNeuron.receivers.ToArray()) { - int ix = GetNucleusIndex(prefabNuclei, receiver); - if (ix < 0) - continue; - - if (clonedNuclei[ix] is not Nucleus clonedReceiver) - continue; - - // Find the synapse for the weight - float weight = 1; - foreach (Synapse synapse in receiver.synapses) { - // Find the weight for this synapse - if (synapse.neuron == prefabNucleus) { - weight = synapse.weight; - break; - } - } - - clonedNeuron.AddReceiver(clonedReceiver, weight); + /// + /// The base name of the cluster. I don't think this is actively used at this moment + /// + public string baseName { + get { + int colonPositon = this.name.IndexOf(':'); + if (colonPositon < 0) + return this.name; + return this.name[..colonPositon]; } } - // Copy nucleus arrays for receptors - for (int nucleusIx = 0; nucleusIx < prefabNuclei.Length; nucleusIx++) { - Nucleus prefabNucleus = prefabNuclei[nucleusIx]; - if (prefabNucleus is not IReceptor prefabReceptor) - continue; + //[SerializeReference] + //public ClusterArray clusterArray; + [SerializeReference] + public Cluster[] siblingClusters; + public Dictionary thingClusters = new(); - if (prefabReceptor.nucleiArray == null || prefabReceptor.nucleiArray.Length == 0) - continue; + #region Init - IReceptor clonedNucleus = clonedNuclei[nucleusIx] as IReceptor; - if (prefabReceptor == prefabReceptor.nucleiArray[0]) { - // We clone the array only for the first entry - NucleusArray clonedArray = new(prefabReceptor.nucleiArray.Length); - int arrayIx = 0; - foreach (Nucleus prefabArrayNucleus in prefabReceptor.nucleiArray) { - int arrayNucleusIx = GetNucleusIndex(prefabNuclei, prefabArrayNucleus); - if (arrayNucleusIx >= 0) { - Nucleus clonedArrayNucleus = clonedNuclei[arrayNucleusIx]; - clonedArray.nuclei[arrayIx] = clonedArrayNucleus; + /// + /// Instantiate a new copy of a ClusterPrefab in the given parent + /// + /// The prefab to use + /// The cluster in which this new cluster will be placed + public Cluster(ClusterPrefab prefab, Cluster parent) { + this.prefab = prefab; + this.name = prefab.name; + + this.parent = parent; + this.parent?.clusterNuclei.Add(this); + ClonePrefab(); + _ = this.inputs; + this.sortedNuclei = TopologicalSort(this.clusterNuclei); + } + + /// + /// Add a new cluster to a ClusterPrefab + /// + /// The prefab to copy + /// The prefab in which the new copy is placed + public Cluster(ClusterPrefab prefab, ClusterPrefab parent = null) { + this.prefab = prefab; + this.name = prefab.name; + this.clusterPrefab = parent; + + if (this.clusterPrefab != null) + this.clusterPrefab.nuclei.Add(this); + + ClonePrefab(); + _ = this.inputs; + this.sortedNuclei = TopologicalSort(this.clusterNuclei); + } + + /// + /// Clone a prefab. + /// + /// Strange that this does not take any parameters or return values. + /// Where which the clone be found??? + private void ClonePrefab() { + Nucleus[] prefabNuclei = this.prefab.nuclei.ToArray(); + // first clone the nuclei without their connections + foreach (Nucleus nucleus in this.prefab.nuclei) { + nucleus.ShallowCloneTo(this); + } + Nucleus[] clonedNuclei = this.clusterNuclei.ToArray(); + + // Now clone the connections + for (int nucleusIx = 0; nucleusIx < prefabNuclei.Length; nucleusIx++) { + Nucleus prefabNucleus = prefabNuclei[nucleusIx]; + if (prefabNucleus is not Neuron prefabNeuron) + continue; + + Nucleus clonedNucleus = clonedNuclei[nucleusIx]; + if (clonedNucleus == null || clonedNucleus is not Neuron clonedNeuron) + continue; + + // Copy the receivers, which will also create the synapses + // Clusters do not have receivers... + foreach (Nucleus receiver in prefabNeuron.receivers.ToArray()) { + int ix = GetNucleusIndex(prefabNuclei, receiver); + if (ix < 0) + continue; + + if (clonedNuclei[ix] is not Nucleus clonedReceiver) + continue; + + // Find the synapse for the weight + float weight = 1; + foreach (Synapse synapse in receiver.synapses) { + // Find the weight for this synapse + if (synapse.neuron == prefabNucleus) { + weight = synapse.weight; + break; + } } - else { - Debug.LogError($" Could not find prefab nucleus {prefabNucleus.name} in the clones"); - } - arrayIx++; + + clonedNeuron.AddReceiver(clonedReceiver, weight); } - //clonedNucleus.array = clonedArray; - clonedNucleus.nucleiArray = clonedArray.nuclei; + } + + // Copy nucleus arrays for receptors + for (int nucleusIx = 0; nucleusIx < prefabNuclei.Length; nucleusIx++) { + Nucleus prefabNucleus = prefabNuclei[nucleusIx]; + if (prefabNucleus is not IReceptor prefabReceptor) + continue; + + if (prefabReceptor.nucleiArray == null || prefabReceptor.nucleiArray.Length == 0) + continue; + + IReceptor clonedNucleus = clonedNuclei[nucleusIx] as IReceptor; + if (prefabReceptor == prefabReceptor.nucleiArray[0]) { + // We clone the array only for the first entry + NucleusArray clonedArray = new(prefabReceptor.nucleiArray.Length); + int arrayIx = 0; + foreach (Nucleus prefabArrayNucleus in prefabReceptor.nucleiArray) { + int arrayNucleusIx = GetNucleusIndex(prefabNuclei, prefabArrayNucleus); + if (arrayNucleusIx >= 0) { + Nucleus clonedArrayNucleus = clonedNuclei[arrayNucleusIx]; + clonedArray.nuclei[arrayIx] = clonedArrayNucleus; + } + else { + Debug.LogError($" Could not find prefab nucleus {prefabNucleus.name} in the clones"); + } + arrayIx++; + } + //clonedNucleus.array = clonedArray; + clonedNucleus.nucleiArray = clonedArray.nuclei; + } + else { + // The others will refer to the array created for the first nucleus in the array + int firstNucleusIx = GetNucleusIndex(prefabNuclei, prefabReceptor.nucleiArray[0]); + IReceptor clonedFirstNucleus = clonedNuclei[firstNucleusIx] as IReceptor; + clonedNucleus.nucleiArray = clonedFirstNucleus.nucleiArray; + } + } + + foreach (Nucleus nucleus in this.clusterNuclei) { + if (nucleus is Cluster clonedSubCluster) + RestoreAllExternalReceivers(clonedSubCluster, this.prefab, this); + } + } + + /// + /// Sort the nuclei in a correct evaluation order + /// + /// + /// + /// + private List TopologicalSort(List nodes) { + Dictionary inDegree = new(); + foreach (Nucleus node in nodes) + inDegree[node] = 0; // Initialize in-degree to zero + + // Calculate in-degrees + foreach (Nucleus node in nodes) { + if (node is Cluster cluster) { + foreach (Nucleus receiver in cluster.CollectReceivers()) + inDegree[receiver]++; + } + else if (node is Neuron neuron) { + foreach (Nucleus receiver in neuron.receivers) + inDegree[receiver]++; + } + } + + Queue queue = new(); + foreach (Nucleus node in nodes) { + if (inDegree[node] == 0) // Nodes with no dependencies + queue.Enqueue(node); + } + // The queue basically stores all input nuclei? + + List sortedOrder = new(); + while (queue.Count > 0) { + Nucleus current = queue.Dequeue(); + sortedOrder.Add(current); // Process the node + + if (current is Neuron neuron) { + foreach (Nucleus receiver in neuron.receivers) { + inDegree[receiver]--; + if (inDegree[receiver] == 0) // If all dependencies resolved + queue.Enqueue(receiver); + } + } + else if (current is Cluster cluster) { + foreach (Nucleus receiver in cluster.CollectReceivers()) { + inDegree[receiver]--; + if (inDegree[receiver] == 0) // If all dependencies resolved + queue.Enqueue(receiver); + } + } + } + + // Check for cycles in the graph + if (sortedOrder.Count != nodes.Count) + throw new InvalidOperationException("Graph is not a DAG; a cycle exists."); + + return sortedOrder; + } + + public override Nucleus Clone(ClusterPrefab parent) { + Cluster clone = new(this.prefab, parent); + + foreach (Synapse synapse in this.synapses) { + Synapse clonedSynapse = clone.AddSynapse(synapse.neuron); + clonedSynapse.weight = synapse.weight; + } + + foreach (Neuron output in this.outputs) { + foreach (Nucleus receiver in output.receivers) { + int ix = GetNucleusIndex(this.clusterNuclei.ToArray(), output); + if (ix < 0) + continue; + + if (clone.clusterNuclei[ix] is not Neuron clonedOutput) + continue; + + clonedOutput.AddReceiver(receiver); + } + } + + return clone; + } + + public override Nucleus ShallowCloneTo(Cluster parent) { + Cluster clone = new(this.prefab, parent) { + name = this.name, + clusterPrefab = this.clusterPrefab, + }; + // Somehow siblingClusters should be cloned too. Believe I do this in ClonePrefab right now. + + return clone; + } + + private static void RestoreAllExternalReceivers(Cluster clonedCluster, ClusterPrefab prefabParent, Cluster clonedParent) { + int clonedClusterIx = GetNucleusIndex(clonedParent.clusterNuclei, clonedCluster); + if (prefabParent.nuclei[clonedClusterIx] is not Cluster sourceCluster) + return; + + for (int nucleusIx = 0; nucleusIx < sourceCluster.clusterNuclei.Count; nucleusIx++) { + Nucleus sourceNucleus = sourceCluster.clusterNuclei[nucleusIx]; + if (sourceNucleus is not Neuron sourceNeuron) + continue; + + if (clonedCluster.clusterNuclei[nucleusIx] is not Neuron clonedNeuron) + continue; + + // copy the receivers (and thus synapses) from the source to the clone + foreach (Nucleus receiver in sourceNeuron.receivers) { + int ix = GetNucleusIndex(prefabParent.nuclei, receiver); + if (ix < 0 || ix >= clonedParent.clusterNuclei.Count) + continue; + + Nucleus clonedReceiver = clonedParent.clusterNuclei[ix]; + + // Find the synapse for the weight + float weight = 1; + foreach (Synapse synapse in receiver.synapses) { + // Find the weight for this synapse + if (synapse.neuron == sourceNucleus) { + weight = synapse.weight; + break; + } + } + + clonedNeuron.AddReceiver(clonedReceiver, weight); + // Debug.Log($"external: {clonedReceiver.name} receives from {clonedNeuron.name} {clonedNeuron.GetHashCode()}"); + } + } + } + + protected int GetNucleusIndex(Nucleus[] nuclei, Nucleus nucleus) { + for (int i = 0; i < nuclei.Length; i++) { + if (nucleus == nuclei[i]) + return i; + } + return -1; + } + + public static int GetNucleusIndex(List nuclei, Nucleus nucleus) { + int i = 0; + foreach (Nucleus nucleiElement in nuclei) { + //for (int i = 0; i < nuclei.Length; i++) { + if (nucleus == nucleiElement) + return i; + i++; + } + return -1; + } + + #endregion Init + + #region Cluster Array + + + public void AddInstance(ClusterPrefab prefab) { + // if (this.siblingClusters.Length == 0) { + // Debug.LogError("Empty perceptoid array, cannot add"); + // return; + // } + this.siblingClusters ??= new Cluster[1] { this }; + + int newLength = this.siblingClusters.Length + 1; + Cluster[] newSiblings = new Cluster[newLength]; + + string baseName = this.name; + int colonPos = baseName.IndexOf(":"); + if (colonPos > 0) + baseName = baseName[..colonPos]; + + for (int i = 0; i < newSiblings.Length - 1; i++) + newSiblings[i] = this.siblingClusters[i]; + // Cluster sourceCluster = this.siblingClusters[0]; + Cluster newCluster = this.Clone(prefab) as Cluster; + newCluster.name = $"{baseName}: {newLength - 1}"; + //newCluster.clusterArray = this; + newSiblings[newLength - 1] = newCluster; + + this.siblingClusters = newSiblings; + newCluster.siblingClusters = newSiblings; + } + + public void RemoveInstance() { + int newLength = this.siblingClusters.Length - 1; + if (newLength == 0) { + Debug.LogWarning("Perceptoid array cannot be empty"); + return; + } + Cluster[] newClusters = new Cluster[newLength]; + for (int i = 0; i < newLength; i++) + newClusters[i] = this.siblingClusters[i]; + // Delete the last perception + //Cluster.Delete(nucleus); + this.siblingClusters = newClusters; + } + + public virtual Cluster GetThingCluster() { + Cluster selectedCluster = SelectCluster(); + return selectedCluster; + } + public virtual Cluster GetThingCluster(int thingId, string thingName = null) { + if (thingClusters.TryGetValue(thingId, out Cluster cluster)) + return cluster; + + Cluster selectedCluster = SelectCluster(); + thingClusters[thingId] = selectedCluster; + return selectedCluster; + } + + private Cluster SelectCluster() { + if (this.siblingClusters == null) + return this; + + // Find a sleeping cluster + foreach (Cluster cluster in this.siblingClusters) { + if (cluster.defaultOutput.isSleeping) { + RemoveThingCluster(cluster); + return cluster; + } + } + + // Otherwise find the stalest cluster? + Cluster stalestCluster = this.siblingClusters[0]; + for (int ix = 1; ix < this.siblingClusters.Length; ix++) { + if (this.siblingClusters[ix].defaultOutput.stale > stalestCluster.defaultOutput.stale) + stalestCluster = this.siblingClusters[ix]; + } + + RemoveThingCluster(stalestCluster); + return stalestCluster; + } + + private void RemoveThingCluster(Cluster cluster) { + List keysToRemove = new(); + foreach (KeyValuePair kvp in thingClusters) { + if (kvp.Value == cluster) + keysToRemove.Add(kvp.Key); + } + + foreach (int thingId in keysToRemove) + thingClusters.Remove(thingId); + } + + #endregion ClusterArray + + public ClusterPrefab prefab; + + + [SerializeReference] + public List clusterNuclei = new(); + // the nuclei sorted using topological sorting + // to ensure that the cluster is computer in the right order + public List sortedNuclei; + //public Dictionary nucleiDict = new(); + + public List _inputs = null; + public virtual List inputs { + get { + if (this._inputs == null) { + this._inputs = new(); + foreach (Nucleus nucleus in this.clusterNuclei) { + // inputs have no synapses + if (nucleus.synapses.Count == 0) + this._inputs.Add(nucleus); + } + ComputeOrders(); + } + return this._inputs; + } + } + + public Dictionary> computeOrders = new(); + private void ComputeOrders() { + foreach (Nucleus input in this._inputs) + computeOrders[input] = TopologicalSort2(input); + } + + private List TopologicalSort2(Nucleus startNode) { + Dictionary inDegree = new(); + HashSet visited = new(); + + // Initialize in-degrees and mark all nodes as unvisited + foreach (Nucleus node in this.clusterNuclei) + inDegree[node] = 0; + + // Calculate in-degrees for all nodes reachable from the start node + Queue queue = new Queue(); + queue.Enqueue(startNode); + visited.Add(startNode); + + while (queue.Count > 0) { + Nucleus current = queue.Dequeue(); + List receivers = null; + if (current is Neuron neuron) + receivers = neuron.receivers; + else if (current is Cluster cluster) + receivers = cluster.CollectReceivers(); + + // if (current is Neuron neuron) { + foreach (Nucleus receiver in receivers) { + if (!visited.Contains(receiver)) { + visited.Add(receiver); + queue.Enqueue(receiver); + } + inDegree[receiver]++; + } + // } + } + + // Perform topological sort on all reachable nodes + queue.Clear(); + foreach (Nucleus node in visited) { + if (inDegree[node] == 0) + queue.Enqueue(node); + } + + List sortedOrder = new List(); + while (queue.Count > 0) { + Nucleus current = queue.Dequeue(); + sortedOrder.Add(current); // Process the node + + List receivers = null; + if (current is Neuron neuron) + receivers = neuron.receivers; + else if (current is Cluster cluster) + receivers = cluster.CollectReceivers(); + + //if (current is Neuron neuron) { + + foreach (Nucleus receiver in receivers) { + if (visited.Contains(receiver)) { + inDegree[receiver]--; + if (inDegree[receiver] == 0) // If all dependencies resolved + queue.Enqueue(receiver); + } + } + //} + } + + // Check for cycles in the graph + if (sortedOrder.Count != visited.Count) + throw new InvalidOperationException("Graph is not a DAG; a cycle exists."); + + return sortedOrder; + } + + public virtual Neuron defaultOutput {//=> this.nuclei[0] as Nucleus; + get { + if (this.clusterNuclei.Count > 0) + return this.clusterNuclei[0] as Neuron; + return null; + } + } + protected List _outputs = null; + public List outputs { + get { + if (this._outputs == null) { + this._outputs = new(); + foreach (Nucleus nucleus in this.clusterNuclei) { + if (nucleus is Neuron neuron) // && neuron.receivers.Count == 0) + this._outputs.Add(neuron); + } + } + return this._outputs; + } + } + + public bool TryGetNucleus(string nucleusName, out Nucleus foundNucleus) { + foreach (Nucleus receptor in this.clusterNuclei) { + if (receptor is Nucleus nucleus) + if (nucleus.name == nucleusName) { + foundNucleus = nucleus; + return true; + } + } + foundNucleus = null; + return false; + } + + public Nucleus GetNucleus(string nucleusName) { + int dotPosition = nucleusName.IndexOf('.'); + if (dotPosition >= 0) { + string clusterName = nucleusName[..dotPosition]; + string clusterName0 = clusterName + ": 0"; + foreach (Nucleus nucleus in this.clusterNuclei) { + if (nucleus is Cluster cluster) { + if (cluster.name == clusterName || cluster.name == clusterName0) { + string subNucleusName = nucleusName[(dotPosition + 1)..]; + return cluster.GetNucleus(subNucleusName); + } + } + } + return null; } else { - // The others will refer to the array created for the first nucleus in the array - int firstNucleusIx = GetNucleusIndex(prefabNuclei, prefabReceptor.nucleiArray[0]); - IReceptor clonedFirstNucleus = clonedNuclei[firstNucleusIx] as IReceptor; - clonedNucleus.nucleiArray = clonedFirstNucleus.nucleiArray; - } - } - - foreach (Nucleus nucleus in this.clusterNuclei) { - if (nucleus is Cluster clonedSubCluster) - RestoreAllExternalReceivers(clonedSubCluster, this.prefab, this); - } - } - - /// - /// Sort the nuclei in a correct evaluation order - /// - /// - /// - /// - private List TopologicalSort(List nodes) { - Dictionary inDegree = new(); - foreach (Nucleus node in nodes) - inDegree[node] = 0; // Initialize in-degree to zero - - // Calculate in-degrees - foreach (Nucleus node in nodes) { - if (node is Cluster cluster) { - foreach (Nucleus receiver in cluster.CollectReceivers()) - inDegree[receiver]++; - } - else if (node is Neuron neuron) { - foreach (Nucleus receiver in neuron.receivers) - inDegree[receiver]++; - } - } - - Queue queue = new(); - foreach (Nucleus node in nodes) { - if (inDegree[node] == 0) // Nodes with no dependencies - queue.Enqueue(node); - } - // The queue basically stores all input nuclei? - - List sortedOrder = new(); - while (queue.Count > 0) { - Nucleus current = queue.Dequeue(); - sortedOrder.Add(current); // Process the node - - if (current is Neuron neuron) { - foreach (Nucleus receiver in neuron.receivers) { - inDegree[receiver]--; - if (inDegree[receiver] == 0) // If all dependencies resolved - queue.Enqueue(receiver); - } - } - else if (current is Cluster cluster) { - foreach (Nucleus receiver in cluster.CollectReceivers()) { - inDegree[receiver]--; - if (inDegree[receiver] == 0) // If all dependencies resolved - queue.Enqueue(receiver); - } - } - } - - // Check for cycles in the graph - if (sortedOrder.Count != nodes.Count) - throw new InvalidOperationException("Graph is not a DAG; a cycle exists."); - - return sortedOrder; - } - - public override Nucleus Clone(ClusterPrefab parent) { - Cluster clone = new(this.prefab, parent); - - foreach (Synapse synapse in this.synapses) { - Synapse clonedSynapse = clone.AddSynapse(synapse.neuron); - clonedSynapse.weight = synapse.weight; - } - - foreach (Neuron output in this.outputs) { - foreach (Nucleus receiver in output.receivers) { - int ix = GetNucleusIndex(this.clusterNuclei.ToArray(), output); - if (ix < 0) - continue; - - if (clone.clusterNuclei[ix] is not Neuron clonedOutput) - continue; - - clonedOutput.AddReceiver(receiver); - } - } - - return clone; - } - - public override Nucleus ShallowCloneTo(Cluster parent) { - Cluster clone = new(this.prefab, parent) { - name = this.name, - clusterPrefab = this.clusterPrefab, - }; - - return clone; - } - - private static void RestoreAllExternalReceivers(Cluster clonedCluster, ClusterPrefab prefabParent, Cluster clonedParent) { - int clonedClusterIx = GetNucleusIndex(clonedParent.clusterNuclei, clonedCluster); - if (prefabParent.nuclei[clonedClusterIx] is not Cluster sourceCluster) - return; - - for (int nucleusIx = 0; nucleusIx < sourceCluster.clusterNuclei.Count; nucleusIx++) { - Nucleus sourceNucleus = sourceCluster.clusterNuclei[nucleusIx]; - if (sourceNucleus is not Neuron sourceNeuron) - continue; - - if (clonedCluster.clusterNuclei[nucleusIx] is not Neuron clonedNeuron) - continue; - - // copy the receivers (and thus synapses) from the source to the clone - foreach (Nucleus receiver in sourceNeuron.receivers) { - int ix = GetNucleusIndex(prefabParent.nuclei, receiver); - if (ix < 0 || ix >= clonedParent.clusterNuclei.Count) - continue; - - Nucleus clonedReceiver = clonedParent.clusterNuclei[ix]; - - // Find the synapse for the weight - float weight = 1; - foreach (Synapse synapse in receiver.synapses) { - // Find the weight for this synapse - if (synapse.neuron == sourceNucleus) { - weight = synapse.weight; - break; - } - } - - clonedNeuron.AddReceiver(clonedReceiver, weight); - // Debug.Log($"external: {clonedReceiver.name} receives from {clonedNeuron.name} {clonedNeuron.GetHashCode()}"); - } - } - } - - protected int GetNucleusIndex(Nucleus[] nuclei, Nucleus nucleus) { - for (int i = 0; i < nuclei.Length; i++) { - if (nucleus == nuclei[i]) - return i; - } - return -1; - } - - public static int GetNucleusIndex(List nuclei, Nucleus nucleus) { - int i = 0; - foreach (Nucleus nucleiElement in nuclei) { - //for (int i = 0; i < nuclei.Length; i++) { - if (nucleus == nucleiElement) - return i; - i++; - } - return -1; - } - - #endregion Init - - public ClusterPrefab prefab; - - - [SerializeReference] - public List clusterNuclei = new(); - // the nuclei sorted using topological sorting - // to ensure that the cluster is computer in the right order - public List sortedNuclei; - //public Dictionary nucleiDict = new(); - - public List _inputs = null; - public virtual List inputs { - get { - if (this._inputs == null) { - this._inputs = new(); + string nucleusName0 = nucleusName + ": 0"; foreach (Nucleus nucleus in this.clusterNuclei) { - // inputs have no synapses - if (nucleus.synapses.Count == 0) - this._inputs.Add(nucleus); - } - ComputeOrders(); - } - return this._inputs; - } - } - - public Dictionary> computeOrders = new(); - private void ComputeOrders() { - foreach (Nucleus input in this._inputs) - computeOrders[input] = TopologicalSort2(input); - } - - private List TopologicalSort2(Nucleus startNode) { - Dictionary inDegree = new(); - HashSet visited = new(); - - // Initialize in-degrees and mark all nodes as unvisited - foreach (Nucleus node in this.clusterNuclei) - inDegree[node] = 0; - - // Calculate in-degrees for all nodes reachable from the start node - Queue queue = new Queue(); - queue.Enqueue(startNode); - visited.Add(startNode); - - while (queue.Count > 0) { - Nucleus current = queue.Dequeue(); - List receivers = null; - if (current is Neuron neuron) - receivers = neuron.receivers; - else if (current is Cluster cluster) - receivers = cluster.CollectReceivers(); - - // if (current is Neuron neuron) { - foreach (Nucleus receiver in receivers) { - if (!visited.Contains(receiver)) { - visited.Add(receiver); - queue.Enqueue(receiver); - } - inDegree[receiver]++; - } - // } - } - - // Perform topological sort on all reachable nodes - queue.Clear(); - foreach (Nucleus node in visited) { - if (inDegree[node] == 0) - queue.Enqueue(node); - } - - List sortedOrder = new List(); - while (queue.Count > 0) { - Nucleus current = queue.Dequeue(); - sortedOrder.Add(current); // Process the node - - List receivers = null; - if (current is Neuron neuron) - receivers = neuron.receivers; - else if (current is Cluster cluster) - receivers = cluster.CollectReceivers(); - - //if (current is Neuron neuron) { - - foreach (Nucleus receiver in receivers) { - if (visited.Contains(receiver)) { - inDegree[receiver]--; - if (inDegree[receiver] == 0) // If all dependencies resolved - queue.Enqueue(receiver); - } - } - //} - } - - // Check for cycles in the graph - if (sortedOrder.Count != visited.Count) - throw new InvalidOperationException("Graph is not a DAG; a cycle exists."); - - return sortedOrder; - } - - public virtual Neuron defaultOutput {//=> this.nuclei[0] as Nucleus; - get { - if (this.clusterNuclei.Count > 0) - return this.clusterNuclei[0] as Neuron; - return null; - } - } - protected List _outputs = null; - public List outputs { - get { - if (this._outputs == null) { - this._outputs = new(); - foreach (Nucleus nucleus in this.clusterNuclei) { - if (nucleus is Neuron neuron) // && neuron.receivers.Count == 0) - this._outputs.Add(neuron); - } - } - return this._outputs; - } - } - - public bool TryGetNucleus(string nucleusName, out Nucleus foundNucleus) { - foreach (Nucleus receptor in this.clusterNuclei) { - if (receptor is Nucleus nucleus) - if (nucleus.name == nucleusName) { - foundNucleus = nucleus; - return true; - } - } - foundNucleus = null; - return false; - } - - public Nucleus GetNucleus(string nucleusName) { - int dotPosition = nucleusName.IndexOf('.'); - if (dotPosition >= 0) { - string clusterName = nucleusName[..dotPosition]; - string clusterName0 = clusterName + ": 0"; - foreach (Nucleus nucleus in this.clusterNuclei) { - if (nucleus is Cluster cluster) { - if (cluster.name == clusterName || cluster.name == clusterName0) { - string subNucleusName = nucleusName[(dotPosition + 1)..]; - return cluster.GetNucleus(subNucleusName); + if (nucleus is IReceptor receptor) { + if (nucleus.name == nucleusName | nucleus.name == nucleusName0) + return nucleus; } - } - } - return null; - } - else { - string nucleusName0 = nucleusName + ": 0"; - foreach (Nucleus nucleus in this.clusterNuclei) { - if (nucleus is IReceptor receptor) { - if (nucleus.name == nucleusName | nucleus.name == nucleusName0) + else if (nucleus.name == nucleusName) return nucleus; } - else if (nucleus.name == nucleusName) - return nucleus; - } - return null; - } - } - - // [Obsolete("Use GetNucleus instead")] - // public IReceptor GetReceptor(string receptorName) { - // return GetNucleus(receptorName) as IReceptor; - // } - - #region Receivers - - public virtual List CollectReceivers() { - List receivers = new(); - foreach (Neuron output in this.outputs) { - foreach (Nucleus receiver in output.receivers) { - // Only add receivers outside this cluster - if (receiver.clusterPrefab != this.prefab) - receivers.Add(receiver); - //receivers.AddRange(output.receivers); + return null; } } - return receivers; - } - #endregion Receivers - - #region Update - - public void UpdateFromNucleus(Nucleus startNucleus) { - // no bias+synapse input state calculation for now... - - if (this.computeOrders.ContainsKey(startNucleus) == false) { - //Debug.LogError($"{this.name} compute orders does not contain an order for {startNucleus.name}"); - return; - } - - List computeOrder = this.computeOrders[startNucleus]; - if (startNucleus.trace) - Debug.Log($"Update from {startNucleus.name}"); - foreach (Nucleus nucleus in computeOrder) { - nucleus.UpdateStateIsolated(); - if (startNucleus.trace && nucleus is Neuron neuron) - Debug.Log($" {nucleus.name}[{nucleus.GetHashCode()}] = {neuron.outputValue}"); - } - - // continue in parent - this.parent?.UpdateFromNucleus(this); - - UpdateNuclei(); - } - - public override void UpdateStateIsolated() { - throw new Exception("Cluster should not be updated!"); - // float3 sum = this.bias; - - // //Applying the weight factors - // foreach (Synapse synapse in this.synapses) { - // if (lengthsq(synapse.neuron.outputValue) > 0) { - // sum += synapse.weight * synapse.neuron.outputValue; - // } + // [Obsolete("Use GetNucleus instead")] + // public IReceptor GetReceptor(string receptorName) { + // return GetNucleus(receptorName) as IReceptor; // } - // foreach (Nucleus nucleus in this.sortedNuclei) - // nucleus.UpdateStateIsolated(); + #region Receivers + + public virtual List CollectReceivers() { + List receivers = new(); + foreach (Neuron output in this.outputs) { + foreach (Nucleus receiver in output.receivers) { + // Only add receivers outside this cluster + if (receiver.clusterPrefab != this.prefab) + receivers.Add(receiver); + //receivers.AddRange(output.receivers); + } + } + return receivers; + } + + #endregion Receivers + + #region Update + + public void UpdateFromNucleus(Nucleus startNucleus) { + // no bias+synapse input state calculation for now... + + if (this.computeOrders.ContainsKey(startNucleus) == false) { + //Debug.LogError($"{this.name} compute orders does not contain an order for {startNucleus.name}"); + return; + } + + List computeOrder = this.computeOrders[startNucleus]; + if (startNucleus.trace) + Debug.Log($"Update from {startNucleus.name}"); + foreach (Nucleus nucleus in computeOrder) { + nucleus.UpdateStateIsolated(); + if (startNucleus.trace && nucleus is Neuron neuron) + Debug.Log($" {nucleus.name}[{nucleus.GetHashCode()}] = {neuron.outputValue}"); + } + + // continue in parent + this.parent?.UpdateFromNucleus(this); + + UpdateNuclei(); + } + + public override void UpdateStateIsolated() { + throw new Exception("Cluster should not be updated!"); + // float3 sum = this.bias; + + // //Applying the weight factors + // foreach (Synapse synapse in this.synapses) { + // if (lengthsq(synapse.neuron.outputValue) > 0) { + // sum += synapse.weight * synapse.neuron.outputValue; + // } + // } + + // foreach (Nucleus nucleus in this.sortedNuclei) + // nucleus.UpdateStateIsolated(); + + // UpdateNuclei(); + } + + public override void UpdateNuclei() { + foreach (Nucleus nucleus in this.clusterNuclei) + nucleus.UpdateNuclei(); + } + + #endregion Update - // UpdateNuclei(); } - public override void UpdateNuclei() { - foreach (Nucleus nucleus in this.clusterNuclei) - nucleus.UpdateNuclei(); - } - - #endregion Update - -} - } \ No newline at end of file diff --git a/Runtime/Scripts/Core/ClusterArray.cs b/Runtime/Scripts/Core/ClusterArray.cs index 490c216..3d0bab3 100644 --- a/Runtime/Scripts/Core/ClusterArray.cs +++ b/Runtime/Scripts/Core/ClusterArray.cs @@ -1,3 +1,4 @@ +/* using System; using System.Collections.Generic; using UnityEngine; @@ -8,6 +9,7 @@ namespace NanoBrain { public class ClusterArray : Nucleus { public ClusterPrefab prefab; + [SerializeReference] public Cluster[] clusters; public Dictionary thingClusters = new(); @@ -19,7 +21,7 @@ namespace NanoBrain { for (int ix = 0; ix < size; ix++) { Cluster cluster = new(prefab, parent); cluster.defaultOutput.AddReceiver(receiver); - cluster.clusterArray = this; + //cluster.clusterArray = this; this.clusters[ix] = cluster; } } @@ -31,7 +33,7 @@ namespace NanoBrain { for (int ix = 0; ix < size; ix++) { Cluster cluster = new(prefab, parent); cluster.defaultOutput.AddReceiver(receiver); - cluster.clusterArray = this; + //cluster.clusterArray = this; this.clusters[ix] = cluster; } } @@ -69,7 +71,7 @@ namespace NanoBrain { Cluster sourceCluster = this.clusters[0]; Cluster newCluster = sourceCluster.Clone(prefab) as Cluster; newCluster.name = $"{baseName}: {newLength - 1}"; - newCluster.clusterArray = this; + //newCluster.clusterArray = this; newClusters[newLength - 1] = newCluster; this.clusters = newClusters; } @@ -139,4 +141,5 @@ namespace NanoBrain { } } -} \ No newline at end of file +} +*/ \ No newline at end of file diff --git a/Runtime/Scripts/Core/Nucleus.cs b/Runtime/Scripts/Core/Nucleus.cs index b343d43..1491957 100644 --- a/Runtime/Scripts/Core/Nucleus.cs +++ b/Runtime/Scripts/Core/Nucleus.cs @@ -55,8 +55,8 @@ public abstract class Nucleus { MemoryCell, Cluster, Receptor, - ClusterReceptor, - ClusterArray, + //ClusterReceptor, + //ClusterArray, } #region Synapses From 19f929606a3132d6beab77d777abc8f343ec6fd4 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Fri, 17 Apr 2026 16:34:21 +0200 Subject: [PATCH 10/34] Simplifications --- Editor/ClusterInspector.cs | 170 +++++++------- Editor/ClusterViewer.cs | 293 ++++++++++++++---------- Runtime/Scripts/Core/Cluster.cs | 63 ++--- Runtime/Scripts/Core/ClusterReceptor.cs | 4 +- Runtime/Scripts/Core/IReceptor.cs | 133 +++++------ Runtime/Scripts/Core/Neuron.cs | 50 ++-- Runtime/Scripts/Core/Nucleus.cs | 2 +- Runtime/Scripts/Core/NucleusArray.cs | 4 +- Runtime/Scripts/Core/Receptor.cs | 62 ++--- 9 files changed, 421 insertions(+), 360 deletions(-) diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterInspector.cs index 2b94851..b9f2cdc 100644 --- a/Editor/ClusterInspector.cs +++ b/Editor/ClusterInspector.cs @@ -174,23 +174,24 @@ namespace NanoBrain { memory.staticMemory = EditorGUILayout.Toggle("Static Memory", memory.staticMemory); } - if (this.currentNucleus is IReceptor receptor1) { - EditorGUILayout.BeginHorizontal(); - EditorGUILayout.IntField("Array size", receptor1.nucleiArray.Count()); - if (GUILayout.Button("Add")) { - Undo.RecordObject(prefabAsset, "Array add " + prefabAsset.name); - receptor1.AddReceptorElement(this.prefab); - anythingChanged = true; - } - if (GUILayout.Button("Del")) { - Undo.RecordObject(prefabAsset, "Array delete " + prefabAsset.name); - receptor1.RemoveReceptorElement(); - anythingChanged = true; - } - EditorGUILayout.EndHorizontal(); + // if (this.currentNucleus is IReceptor receptor1) { + // EditorGUILayout.BeginHorizontal(); + // EditorGUILayout.IntField("Array size", receptor1.nucleiArray.Count()); + // if (GUILayout.Button("Add")) { + // Undo.RecordObject(prefabAsset, "Array add " + prefabAsset.name); + // receptor1.AddReceptorElement(this.prefab); + // anythingChanged = true; + // } + // if (GUILayout.Button("Del")) { + // Undo.RecordObject(prefabAsset, "Array delete " + prefabAsset.name); + // receptor1.RemoveReceptorElement(); + // anythingChanged = true; + // } + // EditorGUILayout.EndHorizontal(); - } - else if (this.currentNucleus is Cluster cluster) { + // } + // else + if (this.currentNucleus is Cluster cluster) { EditorGUILayout.BeginHorizontal(); if (cluster.siblingClusters != null && cluster.siblingClusters.Length > 1) EditorGUILayout.IntField("Array size", cluster.siblingClusters.Count()); @@ -211,7 +212,7 @@ namespace NanoBrain { // Synapses - if (this.currentNucleus is not Receptor && this.currentNucleus is not ClusterReceptor) { + if (this.currentNucleus is not Receptor) { //} && this.currentNucleus is not ClusterReceptor) { showSynapses = EditorGUILayout.BeginFoldoutHeaderGroup(showSynapses, "Synapses"); if (showSynapses) { if (this.currentNucleus is Neuron neuron2) { @@ -248,11 +249,11 @@ namespace NanoBrain { continue; } else { - if (synapse.neuron.parent is IReceptor iReceptor) { - array = iReceptor.nucleiArray; - if (iReceptor is Cluster iCluster) - elementIx = Cluster.GetNucleusIndex(iCluster.clusterNuclei, synapse.neuron); - } + // if (synapse.neuron.parent is IReceptor iReceptor) { + // array = iReceptor.nucleiArray; + // if (iReceptor is Cluster iCluster) + // elementIx = Cluster.GetNucleusIndex(iCluster.clusterNuclei, synapse.neuron); + // } // else if (synapse.nucleus is Receptor receptor2) // && receptor2.array != null && receptor2.array.nuclei.Length > 1) // array = receptor2.nucleiArray; } @@ -299,14 +300,14 @@ namespace NanoBrain { EditorGUI.indentLevel++; float newWeight = EditorGUILayout.FloatField("Weight", synapse.weight); if (newWeight != synapse.weight) { - if (synapse.neuron.parent is IReceptor receptor) { - Nucleus[] receptorArray = receptor.nucleiArray; - foreach (Synapse s in this.currentNucleus.synapses) { - if (s.neuron.parent is IReceptor r && r.nucleiArray == receptorArray) - s.weight = newWeight; - } - } - else + // if (synapse.neuron.parent is IReceptor receptor) { + // Nucleus[] receptorArray = receptor.nucleiArray; + // foreach (Synapse s in this.currentNucleus.synapses) { + // if (s.neuron.parent is IReceptor r && r.nucleiArray == receptorArray) + // s.weight = newWeight; + // } + // } + // else synapse.weight = newWeight; anythingChanged = true; } @@ -340,10 +341,10 @@ namespace NanoBrain { neuron.curvePreset = newPreset; EditorGUILayout.EndHorizontal(); } - if (neuron is Receptor receptor2) { - if (receptor2.nucleiArray == null || receptor2.nucleiArray.Count() == 0) - receptor2.array = new NucleusArray(neuron); - } + // if (neuron is Receptor receptor2) { + // if (receptor2.nucleiArray == null || receptor2.nucleiArray.Count() == 0) + // receptor2.array = new NucleusArray(neuron); + // } } EditorGUILayout.Space(); @@ -377,22 +378,22 @@ namespace NanoBrain { void OnSceneGUI(SceneView sceneView) { if (this.gameObject != null) { - 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 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.color = Color.yellow; Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector); } - } + // } } } @@ -452,15 +453,15 @@ namespace NanoBrain { BuildLayers(); } - protected virtual void AddClusterReceptorInput(Nucleus nucleus) { - ClusterPickerWindow.ShowPicker(prefab => OnClusterReceptorPicked(nucleus, prefab), "Select Cluster"); - } - private void OnClusterReceptorPicked(Nucleus nucleus, ClusterPrefab selectedPrefab) { - ClusterReceptor clusterInstance = new(selectedPrefab, this.prefab, "New " + selectedPrefab.name); - clusterInstance.defaultOutput.AddReceiver(nucleus); - this.currentNucleus = clusterInstance; - BuildLayers(); - } + // protected virtual void AddClusterReceptorInput(Nucleus nucleus) { + // ClusterPickerWindow.ShowPicker(prefab => OnClusterReceptorPicked(nucleus, prefab), "Select Cluster"); + // } + // private void OnClusterReceptorPicked(Nucleus nucleus, ClusterPrefab selectedPrefab) { + // ClusterReceptor clusterInstance = new(selectedPrefab, this.prefab, "New " + selectedPrefab.name); + // clusterInstance.defaultOutput.AddReceiver(nucleus); + // this.currentNucleus = clusterInstance; + // BuildLayers(); + // } private void EditCluster(Cluster subCluster) { // May be used with storedPrefab... @@ -502,9 +503,10 @@ namespace NanoBrain { EditorGUILayout.EndHorizontal(); if (connecting) { Nucleus nucleus = nuclei.ElementAt(selectedConnectNucleus); - if (nucleus is IReceptor receptor) - receptor.AddArrayReceiver(this.currentNucleus); - else if (nucleus is Neuron neuron) + // if (nucleus is IReceptor receptor) + // receptor.AddArrayReceiver(this.currentNucleus); + // else + if (nucleus is Neuron neuron) neuron.AddReceiver(this.currentNucleus); else if (nucleus is Cluster subCluster) subCluster.defaultOutput.AddReceiver(this.currentNucleus); @@ -563,37 +565,37 @@ namespace NanoBrain { protected virtual void ChangeSynapse(Synapse synapse, Neuron newNucleus) { Neuron synapseNeuron = synapse.neuron as Neuron; if (synapse.neuron.parent is Cluster subCluster && subCluster.prefab != this.prefab) { - if (synapse.neuron.parent is ClusterReceptor receptor) { - // the new nucleus is part of a (cluster) receptor, - // so we have to change all synapses to this nucleus array elements - int oldNucleusIx = Cluster.GetNucleusIndex(subCluster.clusterNuclei, synapse.neuron); - int newNucleusIx = Cluster.GetNucleusIndex(subCluster.clusterNuclei, newNucleus); - foreach (Nucleus element in receptor.nucleiArray) { - if (element is not ClusterReceptor clusterReceptor) - continue; - // Get the same neuron as the synapse.nucleus in a different element - // of the ClusterReceptor array - Nucleus oldElementNucleus = clusterReceptor.clusterNuclei[oldNucleusIx]; - if (oldElementNucleus is not Neuron oldElementNeuron) - continue; - // Get the same neuron as newNucleus in a different element - // of the ClusterReceptor array - Nucleus newElementNucleus = clusterReceptor.clusterNuclei[newNucleusIx]; - if (newElementNucleus is not Neuron newElementNeuron) - continue; + // if (synapse.neuron.parent is ClusterReceptor receptor) { + // // the new nucleus is part of a (cluster) receptor, + // // so we have to change all synapses to this nucleus array elements + // int oldNucleusIx = Cluster.GetNucleusIndex(subCluster.clusterNuclei, synapse.neuron); + // int newNucleusIx = Cluster.GetNucleusIndex(subCluster.clusterNuclei, newNucleus); + // foreach (Nucleus element in receptor.nucleiArray) { + // if (element is not ClusterReceptor clusterReceptor) + // continue; + // // Get the same neuron as the synapse.nucleus in a different element + // // of the ClusterReceptor array + // Nucleus oldElementNucleus = clusterReceptor.clusterNuclei[oldNucleusIx]; + // if (oldElementNucleus is not Neuron oldElementNeuron) + // continue; + // // Get the same neuron as newNucleus in a different element + // // of the ClusterReceptor array + // Nucleus newElementNucleus = clusterReceptor.clusterNuclei[newNucleusIx]; + // if (newElementNucleus is not Neuron newElementNeuron) + // continue; - oldElementNeuron.RemoveReceiver(this.currentNucleus); - newElementNeuron.AddReceiver(this.currentNucleus); - // Now find the synapse which pointed to the old Neuron - // Synapse synapseForUpdate = this.currentNucleus.GetSynapse(oldElementNeuron); - // synapseForUpdate.nucleus = newElementNeuron; - } - } - else { + // oldElementNeuron.RemoveReceiver(this.currentNucleus); + // newElementNeuron.AddReceiver(this.currentNucleus); + // // Now find the synapse which pointed to the old Neuron + // // Synapse synapseForUpdate = this.currentNucleus.GetSynapse(oldElementNeuron); + // // synapseForUpdate.nucleus = newElementNeuron; + // } + // } + // else { // it is a neuron in a subcluster synapseNeuron.RemoveReceiver(this.currentNucleus); newNucleus.AddReceiver(this.currentNucleus); - } + // } } else { synapseNeuron.RemoveReceiver(this.currentNucleus); diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index c2f55c8..2cc0233 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -186,67 +186,67 @@ namespace NanoBrain { // Draw selected Nucleus if (expandArray) { - if (this.currentNucleus is IReceptor receptor1) { - float maxValue = 0; - foreach (Nucleus nucleus in receptor1.nucleiArray) { - if (nucleus is Neuron neuron) { - float value = neuron.outputMagnitude; - if (value > maxValue) - maxValue = value; - } - } + // if (this.currentNucleus is IReceptor receptor1) { + // float maxValue = 0; + // foreach (Nucleus nucleus in receptor1.nucleiArray) { + // if (nucleus is Neuron neuron) { + // float value = neuron.outputMagnitude; + // if (value > maxValue) + // maxValue = value; + // } + // } - float spacing = 400f / receptor1.nucleiArray.Count(); - float margin = 10 + spacing / 2; - float xMin = 150 - size; - float xMax = 150 + size; - float yMin = 10 + margin - size / 2; - float yMax = 400 - margin + size; - Vector3[] verts = new Vector3[4] { - new(xMin, yMin, 0), - new(xMax, yMin, 0), - new(xMax, yMax, 0), - new(xMin, yMax, 0) - }; - Handles.color = Color.black; - Handles.DrawAAConvexPolygon(verts); - int row = 0; - foreach (Nucleus nucleus in receptor1.nucleiArray) { - Vector3 pos = new(150, margin + row * spacing, 0.0f); - Handles.color = Color.white; - // The selected nucleus highlight ring - Handles.DrawSolidDisc(pos, Vector3.forward, size + 2); - DrawNucleus(nucleus, pos, maxValue, size); - row++; - } - GUIStyle style = new(EditorStyles.label) { - alignment = TextAnchor.UpperCenter, - normal = { textColor = Color.white }, - fontStyle = FontStyle.Bold, - }; - Vector3 labelPos = new(150, yMax + size + 5, 0); - string receptorName = receptor1.GetName(); - int colonPos = receptorName.IndexOf(":"); - if (colonPos > 0) { - string baseName = receptorName[..colonPos]; - Handles.Label(labelPos, baseName, style); - } - else - Handles.Label(labelPos, receptorName, style); - } - else { - Handles.color = Color.white; - // The selected nucleus highlight ring - Handles.DrawSolidDisc(position, Vector3.forward, size + 2); - float maxValue = 1; - if (this.currentNucleus is Neuron neuron) - maxValue = neuron.outputMagnitude; - else if (this.currentNucleus is Cluster cluster) - maxValue = cluster.defaultOutput.outputMagnitude; + // float spacing = 400f / receptor1.nucleiArray.Count(); + // float margin = 10 + spacing / 2; + // float xMin = 150 - size; + // float xMax = 150 + size; + // float yMin = 10 + margin - size / 2; + // float yMax = 400 - margin + size; + // Vector3[] verts = new Vector3[4] { + // new(xMin, yMin, 0), + // new(xMax, yMin, 0), + // new(xMax, yMax, 0), + // new(xMin, yMax, 0) + // }; + // Handles.color = Color.black; + // Handles.DrawAAConvexPolygon(verts); + // int row = 0; + // foreach (Nucleus nucleus in receptor1.nucleiArray) { + // Vector3 pos = new(150, margin + row * spacing, 0.0f); + // Handles.color = Color.white; + // // The selected nucleus highlight ring + // Handles.DrawSolidDisc(pos, Vector3.forward, size + 2); + // DrawNucleus(nucleus, pos, maxValue, size); + // row++; + // } + // GUIStyle style = new(EditorStyles.label) { + // alignment = TextAnchor.UpperCenter, + // normal = { textColor = Color.white }, + // fontStyle = FontStyle.Bold, + // }; + // Vector3 labelPos = new(150, yMax + size + 5, 0); + // string receptorName = receptor1.GetName(); + // int colonPos = receptorName.IndexOf(":"); + // if (colonPos > 0) { + // string baseName = receptorName[..colonPos]; + // Handles.Label(labelPos, baseName, style); + // } + // else + // Handles.Label(labelPos, receptorName, style); + // } + // else { + Handles.color = Color.white; + // The selected nucleus highlight ring + Handles.DrawSolidDisc(position, Vector3.forward, size + 2); + 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); + DrawNucleus(this.currentNucleus, position, maxValue, 20); - } + // } } else { Handles.color = Color.white; @@ -290,11 +290,11 @@ namespace NanoBrain { int row = 0; List drawnArrays = new(); foreach (Nucleus receiver in receivers) { - if (receiver is Receptor receptor) { - if (drawnArrays.Contains(receptor.nucleiArray)) - continue; - drawnArrays.Add(receptor.nucleiArray); - } + // if (receiver is Receptor receptor) { + // if (drawnArrays.Contains(receptor.nucleiArray)) + // continue; + // drawnArrays.Add(receptor.nucleiArray); + // } Nucleus receiverNucleus = receiver; if (receiverNucleus == null) @@ -321,26 +321,21 @@ namespace NanoBrain { if (synapse.neuron == null) continue; - if (synapse.neuron is Receptor receptor) { - if (drawnArrays.Contains(receptor.nucleiArray)) - continue; - drawnArrays.Add(receptor.nucleiArray); - } - else if (synapse.neuron.parent is ClusterReceptor clusterReceptor) { - if (drawnArrays.Contains(clusterReceptor.nucleiArray)) - continue; - drawnArrays.Add(clusterReceptor.nucleiArray); - } - else if (synapse.neuron.parent is Cluster cluster && cluster.siblingClusters != null) { + // if (synapse.neuron is Receptor receptor) { + // if (drawnArrays.Contains(receptor.nucleiArray)) + // continue; + // drawnArrays.Add(receptor.nucleiArray); + // } + // else if (synapse.neuron.parent is ClusterReceptor clusterReceptor) { + // if (drawnArrays.Contains(clusterReceptor.nucleiArray)) + // continue; + // drawnArrays.Add(clusterReceptor.nucleiArray); + // } + if (synapse.neuron.parent is Cluster cluster && cluster.siblingClusters != null) { if (drawnArrays.Contains(cluster.siblingClusters)) continue; drawnArrays.Add(cluster.siblingClusters); } - // else if (synapse.neuron.parent is Cluster cluster && cluster.clusterArray != null) { - // if (drawnArrays.Contains(cluster.clusterArray.clusters)) - // continue; - // drawnArrays.Add(cluster.clusterArray.clusters); - // } if (synapse.neuron is Neuron synapseNeuron) { float value = synapseNeuron.outputMagnitude * synapse.weight; // Debug.Log($"{synapse.nucleus.name}: {value} {length(synapse.nucleus.outputValue)} {synapse.weight}"); @@ -360,16 +355,16 @@ namespace NanoBrain { if (synapse.neuron is null) continue; - if (synapse.neuron is Receptor neuron) { - if (drawnArrays.Contains(neuron.nucleiArray)) - continue; - drawnArrays.Add(neuron.nucleiArray); - } - else if (synapse.neuron.parent is ClusterReceptor clusterReceptor) { - if (drawnArrays.Contains(clusterReceptor.nucleiArray)) - continue; - drawnArrays.Add(clusterReceptor.nucleiArray); - } + // if (synapse.neuron is Receptor neuron) { + // if (drawnArrays.Contains(neuron.nucleiArray)) + // continue; + // drawnArrays.Add(neuron.nucleiArray); + // } + // else if (synapse.neuron.parent is ClusterReceptor clusterReceptor) { + // if (drawnArrays.Contains(clusterReceptor.nucleiArray)) + // continue; + // drawnArrays.Add(clusterReceptor.nucleiArray); + // } Vector3 pos = new(250, margin + row * spacing, 0.0f); Handles.color = Color.white; Handles.DrawLine(parentPos, pos); @@ -384,11 +379,9 @@ namespace NanoBrain { } if (synapse.neuron.parent != null && synapse.neuron.parent != this.currentNucleus.parent) { // the synapse nucleus is part of a subcluster - DrawNucleus(synapse.neuron.parent, pos, maxValue, size, color); + //DrawNucleus(synapse.neuron.parent, pos, maxValue, size, color); + DrawNucleus(synapse.neuron, pos, maxValue, size, color); } - // else if (synapse.nucleus.cluster != null && synapse.nucleus.cluster != this.currentNucleus.cluster) { - // DrawNucleus(synapse.nucleus.parent, pos, maxValue, size, color); - // } else { DrawNucleus(synapse.neuron, pos, maxValue, size, color); } @@ -428,7 +421,29 @@ namespace NanoBrain { fontStyle = FontStyle.Bold, }; - if (nucleus is IReceptor receptor1) { + // if (nucleus is IReceptor receptor1) { + // if (expandArray) { + // // Put array indices above elements + // style.alignment = TextAnchor.LowerCenter; + // Vector3 labelPos1 = position + Vector3.down * (size + 5); // below disc + // int colonPos1 = nucleus.name.IndexOf(":"); + // if (colonPos1 > 0) { + // string extName = nucleus.name[(colonPos1 + 2)..]; + // Handles.Label(labelPos1, extName, style); + // } + // } + // else { + // // draw the array size label + // if (color.grayscale > 0.5f) + // style.normal.textColor = Color.black; + // else + // style.normal.textColor = Color.white; + // Handles.Label(labelPosition, receptor1.nucleiArray.Length.ToString(), style); + // style.normal.textColor = Color.white; + // } + // } + // else + if (nucleus.parent != null && nucleus.parent is Cluster parentCluster) { if (expandArray) { // Put array indices above elements style.alignment = TextAnchor.LowerCenter; @@ -440,13 +455,15 @@ namespace NanoBrain { } } else { - // draw the array size label - if (color.grayscale > 0.5f) - style.normal.textColor = Color.black; - else + if (parentCluster.siblingClusters != null && parentCluster.siblingClusters.Length > 1) { + // draw the array size label + if (color.grayscale > 0.5f) + style.normal.textColor = Color.black; + else + style.normal.textColor = Color.white; + Handles.Label(labelPosition, parentCluster.siblingClusters.Length.ToString(), style); style.normal.textColor = Color.white; - Handles.Label(labelPosition, receptor1.nucleiArray.Length.ToString(), style); - style.normal.textColor = Color.white; + } } } else if (nucleus is Cluster cluster) { @@ -478,25 +495,46 @@ namespace NanoBrain { Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron style.alignment = TextAnchor.UpperCenter; - 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); + if (nucleus.parent != null && nucleus.parent is Cluster parentCluster1) { + parentCluster1.name ??= ""; + string baseName = ""; + if (parentCluster1 != currentNucleus.parent) { + int colonPos = parentCluster1.name.IndexOf(":"); + if (colonPos > 0 && colonPos < parentCluster1.name.Length - 2) + baseName = parentCluster1.name[..colonPos] + "."; + else + baseName = parentCluster1.name + "."; + } + // if (colonPos > 0 && colonPos < parentCluster1.name.Length - 2) { + // // if it is an array, we should not show the :0 of the first element + // //baseName = baseName[..colonPos]; + // Handles.Label(labelPos, baseName + nucleus.name, style); + // } + // else + 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); } - else - Handles.Label(labelPos, nucleus.name, style); } // Draw Cluster ring - if (nucleus is Cluster) { + if (nucleus.parent != currentNucleus.parent || nucleus is Cluster) { Handles.color = Color.white; Handles.DrawWireDisc(position, Vector3.forward, size + 5); } // 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); @@ -507,7 +545,10 @@ namespace NanoBrain { if (e.type == EventType.MouseDown && e.button == 0) { // Consume the event so the scene doesn't also handle it e.Use(); - HandleClicked(nucleus); + if (nucleus.parent != null && nucleus.parent is Cluster parentCluster2) + HandleClicked(parentCluster2); + else + HandleClicked(nucleus); } } } @@ -533,7 +574,7 @@ namespace NanoBrain { private void HandleClicked(Nucleus nucleus) { if (nucleus == this.currentNucleus) { - if (nucleus is Receptor || nucleus is ClusterReceptor) + if (nucleus is Receptor) // || nucleus is ClusterReceptor) expandArray = !expandArray; else expandArray = false; @@ -547,22 +588,22 @@ namespace NanoBrain { void OnSceneGUI(SceneView sceneView) { if (this.gameObject != null) { - 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.color = Color.yellow; - Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector); - } + // 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.color = Color.yellow; + Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector); } + // } } } diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index c96ea65..12d105a 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -117,6 +117,7 @@ namespace NanoBrain { } } + /* // Copy nucleus arrays for receptors for (int nucleusIx = 0; nucleusIx < prefabNuclei.Length; nucleusIx++) { Nucleus prefabNucleus = prefabNuclei[nucleusIx]; @@ -152,6 +153,7 @@ namespace NanoBrain { clonedNucleus.nucleiArray = clonedFirstNucleus.nucleiArray; } } + */ foreach (Nucleus nucleus in this.clusterNuclei) { if (nucleus is Cluster clonedSubCluster) @@ -225,16 +227,19 @@ namespace NanoBrain { clonedSynapse.weight = synapse.weight; } - foreach (Neuron output in this.outputs) { - foreach (Nucleus receiver in output.receivers) { - int ix = GetNucleusIndex(this.clusterNuclei.ToArray(), output); - if (ix < 0) - continue; + foreach (Nucleus nucleus in this.clusterNuclei) { + if (nucleus is Neuron output) { + foreach (Nucleus receiver in output.receivers) { + int ix = GetNucleusIndex(this.clusterNuclei, output); + Debug.Log($"{output.name} -> {receiver.name}: {ix}"); + if (ix < 0) + continue; - if (clone.clusterNuclei[ix] is not Neuron clonedOutput) - continue; + if (clone.clusterNuclei[ix] is not Neuron clonedOutput) + continue; - clonedOutput.AddReceiver(receiver); + clonedOutput.AddReceiver(receiver); + } } } @@ -313,43 +318,41 @@ namespace NanoBrain { public void AddInstance(ClusterPrefab prefab) { - // if (this.siblingClusters.Length == 0) { - // Debug.LogError("Empty perceptoid array, cannot add"); - // return; - // } + // Ensure siblingClusters exists this.siblingClusters ??= new Cluster[1] { this }; + // Prepare the new array int newLength = this.siblingClusters.Length + 1; Cluster[] newSiblings = new Cluster[newLength]; + for (int i = 0; i < newSiblings.Length - 1; i++) + newSiblings[i] = this.siblingClusters[i]; + + Cluster newCluster = this.Clone(prefab) as Cluster; string baseName = this.name; int colonPos = baseName.IndexOf(":"); if (colonPos > 0) baseName = baseName[..colonPos]; - - for (int i = 0; i < newSiblings.Length - 1; i++) - newSiblings[i] = this.siblingClusters[i]; - // Cluster sourceCluster = this.siblingClusters[0]; - Cluster newCluster = this.Clone(prefab) as Cluster; newCluster.name = $"{baseName}: {newLength - 1}"; - //newCluster.clusterArray = this; newSiblings[newLength - 1] = newCluster; - this.siblingClusters = newSiblings; - newCluster.siblingClusters = newSiblings; + // All siblingClusters need to user this array! + foreach (Cluster sibling in this.siblingClusters) + sibling.siblingClusters = newSiblings; } public void RemoveInstance() { - int newLength = this.siblingClusters.Length - 1; - if (newLength == 0) { - Debug.LogWarning("Perceptoid array cannot be empty"); + if (this.siblingClusters == null || this.siblingClusters.Length <= 1) return; - } + + // Prepare the new array + int newLength = this.siblingClusters.Length - 1; Cluster[] newClusters = new Cluster[newLength]; + for (int i = 0; i < newLength; i++) newClusters[i] = this.siblingClusters[i]; - // Delete the last perception - //Cluster.Delete(nucleus); + + Neuron.Delete(this.siblingClusters[^1]); this.siblingClusters = newClusters; } @@ -601,9 +604,11 @@ namespace NanoBrain { if (startNucleus.trace) Debug.Log($"Update from {startNucleus.name}"); foreach (Nucleus nucleus in computeOrder) { - nucleus.UpdateStateIsolated(); - if (startNucleus.trace && nucleus is Neuron neuron) - Debug.Log($" {nucleus.name}[{nucleus.GetHashCode()}] = {neuron.outputValue}"); + if (nucleus is not Cluster) { + nucleus.UpdateStateIsolated(); + if (startNucleus.trace && nucleus is Neuron neuron) + Debug.Log($" {nucleus.name}[{nucleus.GetHashCode()}]"); // = {neuron.outputValue}"); + } } // continue in parent diff --git a/Runtime/Scripts/Core/ClusterReceptor.cs b/Runtime/Scripts/Core/ClusterReceptor.cs index a6c7e52..ac99b2d 100644 --- a/Runtime/Scripts/Core/ClusterReceptor.cs +++ b/Runtime/Scripts/Core/ClusterReceptor.cs @@ -1,3 +1,4 @@ +/* using System; using System.Collections.Generic; using UnityEngine; @@ -275,4 +276,5 @@ namespace NanoBrain { } -} \ No newline at end of file +} +*/ \ No newline at end of file diff --git a/Runtime/Scripts/Core/IReceptor.cs b/Runtime/Scripts/Core/IReceptor.cs index 4c4b373..1f8cbcd 100644 --- a/Runtime/Scripts/Core/IReceptor.cs +++ b/Runtime/Scripts/Core/IReceptor.cs @@ -17,17 +17,17 @@ namespace NanoBrain { /// The array of nuclei used to track multiple things sending stimuli /// /// The size of the array determines the maximum number of things which can be distinguished - public Nucleus[] nucleiArray { get; set; } + // public Nucleus[] nucleiArray { get; set; } /// /// Extends the nucleiArray with an additional element /// /// A prefab of the nucleus to add? - public void AddReceptorElement(ClusterPrefab prefab); + // public void AddReceptorElement(ClusterPrefab prefab); /// /// Removes the last element from the nucleiArray /// - public void RemoveReceptorElement(); + // public void RemoveReceptorElement(); /// /// Add a receiver for this receptor array @@ -35,7 +35,7 @@ namespace NanoBrain { /// The receiving Nucleus /// The initial weight to use for the synapses /// This function will add a synapse to the receiver for each element in the nucleiArray. - public void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1); + // public void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1); /// /// Process an external stimulus @@ -47,77 +47,78 @@ namespace NanoBrain { } public static class IReceptorHelpers { + /* + /// + /// Implementation for the NanoBrain::IReceptor::AddReceptorElement which can be used for all implementations of IReceptor + /// + /// The IReceptor which needs to extend its nucleiArray + /// A prefab of the nucleus to add? + public static void AddReceptorElement(IReceptor receptor, ClusterPrefab prefab) { + if (receptor.nucleiArray.Length == 0) { + Debug.LogError("Empty perceptoid array, cannot add"); + } + int newLength = receptor.nucleiArray.Length + 1; + Nucleus[] newArray = new Nucleus[newLength]; - /// - /// Implementation for the NanoBrain::IReceptor::AddReceptorElement which can be used for all implementations of IReceptor - /// - /// The IReceptor which needs to extend its nucleiArray - /// A prefab of the nucleus to add? - public static void AddReceptorElement(IReceptor receptor, ClusterPrefab prefab) { - if (receptor.nucleiArray.Length == 0) { - Debug.LogError("Empty perceptoid array, cannot add"); - } - int newLength = receptor.nucleiArray.Length + 1; - Nucleus[] newArray = new Nucleus[newLength]; + string baseName = receptor.GetName(); + int colonPos = baseName.IndexOf(":"); + if (colonPos > 0) + baseName = baseName[..colonPos]; - string baseName = receptor.GetName(); - int colonPos = baseName.IndexOf(":"); - if (colonPos > 0) - baseName = baseName[..colonPos]; + for (int i = 0; i < receptor.nucleiArray.Length; i++) + newArray[i] = receptor.nucleiArray[i]; + if (receptor.nucleiArray[0] is Nucleus nucleus) { + newArray[newLength - 1] = nucleus.Clone(prefab); + newArray[newLength - 1].name = $"{baseName}: {newLength - 1}"; + } - for (int i = 0; i < receptor.nucleiArray.Length; i++) - newArray[i] = receptor.nucleiArray[i]; - if (receptor.nucleiArray[0] is Nucleus nucleus) { - newArray[newLength - 1] = nucleus.Clone(prefab); - newArray[newLength - 1].name = $"{baseName}: {newLength - 1}"; - } - - foreach (Nucleus element in receptor.nucleiArray) { - if (element is IReceptor receptorElement) { - receptorElement.nucleiArray = newArray; + foreach (Nucleus element in receptor.nucleiArray) { + if (element is IReceptor receptorElement) { + receptorElement.nucleiArray = newArray; + } + } } - } - } - /// - /// Implementation for the NanoBrain::IReceptor::RemoteReceptorElement which can be used for all implementations of IReceptor - /// - /// The IReceptor which needs to shorten its nucleiArray - public static void RemoveReceptorElement(IReceptor receptor) { - int newLength = receptor.nucleiArray.Length - 1; - if (newLength == 0) { - Debug.LogWarning("Perceptoid array cannot be empty"); - } - Nucleus[] newArray = new Nucleus[newLength]; - for (int i = 0; i < newLength; i++) - newArray[i] = receptor.nucleiArray[i]; - // Delete the last perception - if (receptor.nucleiArray[newLength] is Nucleus nucleus) - Neuron.Delete(nucleus); + /// + /// Implementation for the NanoBrain::IReceptor::RemoteReceptorElement which can be used for all implementations of IReceptor + /// + /// The IReceptor which needs to shorten its nucleiArray + public static void RemoveReceptorElement(IReceptor receptor) { + int newLength = receptor.nucleiArray.Length - 1; + if (newLength == 0) { + Debug.LogWarning("Perceptoid array cannot be empty"); + } + Nucleus[] newArray = new Nucleus[newLength]; + for (int i = 0; i < newLength; i++) + newArray[i] = receptor.nucleiArray[i]; + // Delete the last perception + if (receptor.nucleiArray[newLength] is Nucleus nucleus) + Neuron.Delete(nucleus); + + foreach (Nucleus element in receptor.nucleiArray) { + if (element is IReceptor receptorElement) { + receptorElement.nucleiArray = newArray; + } + } - foreach (Nucleus element in receptor.nucleiArray) { - if (element is IReceptor receptorElement) { - receptorElement.nucleiArray = newArray; } - } - } + /// + /// Implementation for the NanoBreain::IRceptor::AddArrayReceiver which can be used for all implementations of IReceptor + /// + /// The IReceptor for which a receiving nuclues needs to be added + /// The nucleus to receive input from the receptor + /// The initial weight for the synapses + public static void AddArrayReceiver(IReceptor receptor, Nucleus receiverToAdd, float weight = 1) { + foreach (Nucleus element in receptor.nucleiArray) { + if (element is Cluster cluster) + cluster.defaultOutput.AddReceiver(receiverToAdd, weight); + if (element is Neuron neuron) + neuron.AddReceiver(receiverToAdd, weight); + } - /// - /// Implementation for the NanoBreain::IRceptor::AddArrayReceiver which can be used for all implementations of IReceptor - /// - /// The IReceptor for which a receiving nuclues needs to be added - /// The nucleus to receive input from the receptor - /// The initial weight for the synapses - public static void AddArrayReceiver(IReceptor receptor, Nucleus receiverToAdd, float weight = 1) { - foreach (Nucleus element in receptor.nucleiArray) { - if (element is Cluster cluster) - cluster.defaultOutput.AddReceiver(receiverToAdd, weight); - if (element is Neuron neuron) - neuron.AddReceiver(receiverToAdd, weight); - } + } - } + */ } - } \ No newline at end of file diff --git a/Runtime/Scripts/Core/Neuron.cs b/Runtime/Scripts/Core/Neuron.cs index fa48b56..16d4a1a 100644 --- a/Runtime/Scripts/Core/Neuron.cs +++ b/Runtime/Scripts/Core/Neuron.cs @@ -171,7 +171,7 @@ namespace NanoBrain { AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.Linear); } return curve; - + } public static AnimationCurve Binary() { return AnimationCurve.Linear(0, 0, 1, 1); @@ -270,9 +270,11 @@ namespace NanoBrain { } else if (nucleus is Cluster cluster) { // remove all receivers for this cluster - foreach (Neuron output in cluster.outputs) { - foreach (Nucleus receiver in output.receivers) { - receiver.synapses.RemoveAll(s => s.neuron == output); + foreach (Nucleus clusterNucleus in cluster.clusterNuclei) { + if (clusterNucleus is Neuron output) { + foreach (Nucleus receiver in output.receivers) { + receiver.synapses.RemoveAll(s => s.neuron == output); + } } } } @@ -426,9 +428,9 @@ namespace NanoBrain { return float3(value, value, value); } - protected float3 ActivatorNormalized(float3 input) { + protected float3 ActivatorNormalized(float3 input) { if (lengthsq(input) == 0) - return input; + return input; float3 result = normalize(input); return result; } @@ -497,31 +499,31 @@ namespace NanoBrain { } public virtual void RemoveReceiver(Nucleus receiverToRemove) { - if (this is IReceptor receptor) { - foreach (Nucleus element in receptor.nucleiArray) { - if (element is Neuron neuron) { - neuron._receivers.RemoveAll(receiver => receiver == receiverToRemove); - receiverToRemove.synapses.RemoveAll(synapse => synapse.neuron == neuron); - } - } - } - else { - this._receivers.RemoveAll(receiver => receiver == receiverToRemove); - receiverToRemove.synapses.RemoveAll(synapse => synapse.neuron == this); - } + // if (this is IReceptor receptor) { + // foreach (Nucleus element in receptor.nucleiArray) { + // if (element is Neuron neuron) { + // neuron._receivers.RemoveAll(receiver => receiver == receiverToRemove); + // receiverToRemove.synapses.RemoveAll(synapse => synapse.neuron == neuron); + // } + // } + // } + // else { + this._receivers.RemoveAll(receiver => receiver == receiverToRemove); + receiverToRemove.synapses.RemoveAll(synapse => synapse.neuron == this); + // } } #endregion Receivers - public override void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) { - if (this.parent is ClusterReceptor clusterReceptor) - clusterReceptor.ProcessStimulus(this, inputValue, thingId, thingName); - else - ProcessStimulusDirect(inputValue, thingId, thingName); + public override void ProcessStimulus(Vector3 inputValue) { //, int thingId = 0, string thingName = null) { + // if (this.parent is ClusterReceptor clusterReceptor) + // clusterReceptor.ProcessStimulus(this, inputValue, thingId, thingName); + // else + ProcessStimulusDirect(inputValue); //, thingId, thingName); } - public void ProcessStimulusDirect(Vector3 inputValue, int thingId = 0, string thingName = null) { + public void ProcessStimulusDirect(Vector3 inputValue) { //}, int thingId = 0, string thingName = null) { this.stale = 0; this.bias = inputValue; this.parent.UpdateFromNucleus(this); diff --git a/Runtime/Scripts/Core/Nucleus.cs b/Runtime/Scripts/Core/Nucleus.cs index 1491957..25f958f 100644 --- a/Runtime/Scripts/Core/Nucleus.cs +++ b/Runtime/Scripts/Core/Nucleus.cs @@ -138,7 +138,7 @@ public abstract class Nucleus { /// The value of the stimulus /// The id of the thing causing the stimulus /// The name of the thing causing the stimulus - public virtual void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = "") { + public virtual void ProcessStimulus(Vector3 inputValue) { //, int thingId = 0, string thingName = "") { } #endregion Update diff --git a/Runtime/Scripts/Core/NucleusArray.cs b/Runtime/Scripts/Core/NucleusArray.cs index 60a4a21..672f523 100644 --- a/Runtime/Scripts/Core/NucleusArray.cs +++ b/Runtime/Scripts/Core/NucleusArray.cs @@ -1,3 +1,4 @@ +/* using System.Linq; using System.Collections.Generic; using UnityEngine; @@ -194,4 +195,5 @@ namespace NanoBrain { } } -} \ No newline at end of file +} +*/ \ No newline at end of file diff --git a/Runtime/Scripts/Core/Receptor.cs b/Runtime/Scripts/Core/Receptor.cs index ef2e800..6e9efaf 100644 --- a/Runtime/Scripts/Core/Receptor.cs +++ b/Runtime/Scripts/Core/Receptor.cs @@ -10,16 +10,16 @@ namespace NanoBrain { /// Basic IReceptor to receive external input /// [System.Serializable] - public class Receptor : Neuron, IReceptor { + public class Receptor : Neuron { //}, IReceptor { /// /// Create a new Receptor in a Cluster instance /// /// The Cluster in which the Receptor is created /// The name of the new Receptor public Receptor(Cluster parent, string name) : base(parent, name) { - this.array = new NucleusArray(this); - if (this.name.IndexOf(":") < 0) - this.name += ": 0"; + //this.array = new NucleusArray(this); + // if (this.name.IndexOf(":") < 0) + // this.name += ": 0"; } /// /// Create a new Receptor in a Cluster Prefab @@ -27,7 +27,7 @@ namespace NanoBrain { /// The Cluster Prefab in which the Receptor is created /// The name of the new Receptor public Receptor(ClusterPrefab prefab, string name) : base(prefab, name) { - this.array = new NucleusArray(this); + //this.array = new NucleusArray(this); } public string GetName() { @@ -45,7 +45,7 @@ namespace NanoBrain { /// \copydoc NanoBrain::Neuron::Clone public override Nucleus Clone(ClusterPrefab prefab) { Receptor clone = new(prefab, name) { - array = this._array + //array = this._array }; CloneFields(clone); // Adding receivers will also add synapses to the receivers @@ -55,28 +55,30 @@ namespace NanoBrain { return clone; } - [SerializeReference] - private NucleusArray _array; - public NucleusArray array { - set { _array = value; } - } + /* + [SerializeReference] + private NucleusArray _array; + public NucleusArray array { + set { _array = value; } + } - public Nucleus[] nucleiArray { - get { return _array.nuclei; } - set { _array.nuclei = value; } - } + public Nucleus[] nucleiArray { + get { return _array.nuclei; } + set { _array.nuclei = value; } + } - public void AddReceptorElement(ClusterPrefab prefab) { - IReceptorHelpers.AddReceptorElement(this, prefab); - } + public void AddReceptorElement(ClusterPrefab prefab) { + IReceptorHelpers.AddReceptorElement(this, prefab); + } - public void RemoveReceptorElement() { - IReceptorHelpers.RemoveReceptorElement(this); - } + public void RemoveReceptorElement() { + IReceptorHelpers.RemoveReceptorElement(this); + } - public virtual void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1) { - IReceptorHelpers.AddArrayReceiver(this, receiverToAdd, weight); - } + public virtual void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1) { + IReceptorHelpers.AddArrayReceiver(this, receiverToAdd, weight); + } + */ public override void UpdateStateIsolated() { this.outputValue = this.bias; @@ -104,10 +106,14 @@ namespace NanoBrain { #endif - public override void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) { - this._array ??= new NucleusArray(this.parent); - this._array.ProcessStimulus(thingId, inputValue, thingName); + public override void ProcessStimulus(Vector3 inputValue) { //}, int thingId = 0, string thingName = null) { + // this._array ??= new NucleusArray(this.parent); + // this._array.ProcessStimulus(thingId, inputValue, thingName); + + this.stale = 0; + this.bias = inputValue; + this.parent.UpdateFromNucleus(this); } - + } } \ No newline at end of file From 619ced65052bda431241726833280c4c836d16a8 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Fri, 17 Apr 2026 16:52:37 +0200 Subject: [PATCH 11/34] Removed the use of Receptors --- Editor/ClusterInspector.cs | 22 +++++++++++----------- Editor/ClusterViewer.cs | 4 ++-- Runtime/Scripts/Core/Cluster.cs | 2 +- Runtime/Scripts/Core/IReceptor.cs | 8 +++++--- Runtime/Scripts/Core/Nucleus.cs | 2 +- Runtime/Scripts/Core/Receptor.cs | 8 +++++--- 6 files changed, 25 insertions(+), 21 deletions(-) diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterInspector.cs index b9f2cdc..d4fe1c6 100644 --- a/Editor/ClusterInspector.cs +++ b/Editor/ClusterInspector.cs @@ -212,7 +212,7 @@ namespace NanoBrain { // Synapses - if (this.currentNucleus is not Receptor) { //} && this.currentNucleus is not ClusterReceptor) { + //if (this.currentNucleus is not Receptor) { //} && this.currentNucleus is not ClusterReceptor) { showSynapses = EditorGUILayout.BeginFoldoutHeaderGroup(showSynapses, "Synapses"); if (showSynapses) { if (this.currentNucleus is Neuron neuron2) { @@ -320,7 +320,7 @@ namespace NanoBrain { anythingChanged |= AddSynapse(this.prefab, this.currentNucleus); } EditorGUILayout.EndFoldoutHeaderGroup(); - } + //} // Activation @@ -410,9 +410,9 @@ namespace NanoBrain { case Nucleus.Type.Cluster: AddClusterInput(nucleus); break; - case Nucleus.Type.Receptor: - AddReceptorInput(nucleus); - break; + // case Nucleus.Type.Receptor: + // AddReceptorInput(nucleus); + // break; // case Nucleus.Type.ClusterReceptor: // AddClusterReceptorInput(nucleus); // break; @@ -446,12 +446,12 @@ namespace NanoBrain { subclusterInstance.defaultOutput.AddReceiver(nucleus); } - protected virtual void AddReceptorInput(Nucleus nucleus) { - Receptor newReceptor = new(this.prefab, "New Receptor"); - newReceptor.AddReceiver(nucleus); - this.currentNucleus = newReceptor; - BuildLayers(); - } + // protected virtual void AddReceptorInput(Nucleus nucleus) { + // Receptor newReceptor = new(this.prefab, "New Receptor"); + // newReceptor.AddReceiver(nucleus); + // this.currentNucleus = newReceptor; + // BuildLayers(); + // } // protected virtual void AddClusterReceptorInput(Nucleus nucleus) { // ClusterPickerWindow.ShowPicker(prefab => OnClusterReceptorPicked(nucleus, prefab), "Select Cluster"); diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index 2cc0233..fe94305 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -490,7 +490,7 @@ namespace NanoBrain { } } - if (expandArray == false || nucleus is not IReceptor) { + if (expandArray == false) {// || nucleus is not IReceptor) { // put name below nucleus Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron style.alignment = TextAnchor.UpperCenter; @@ -574,7 +574,7 @@ namespace NanoBrain { private void HandleClicked(Nucleus nucleus) { if (nucleus == this.currentNucleus) { - if (nucleus is Receptor) // || nucleus is ClusterReceptor) + if (nucleus is Cluster) //is Receptor) // || nucleus is ClusterReceptor) expandArray = !expandArray; else expandArray = false; diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index 12d105a..3264d8a 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -557,7 +557,7 @@ namespace NanoBrain { else { string nucleusName0 = nucleusName + ": 0"; foreach (Nucleus nucleus in this.clusterNuclei) { - if (nucleus is IReceptor receptor) { + if (nucleus is Cluster) { //IReceptor receptor) { if (nucleus.name == nucleusName | nucleus.name == nucleusName0) return nucleus; } diff --git a/Runtime/Scripts/Core/IReceptor.cs b/Runtime/Scripts/Core/IReceptor.cs index 1f8cbcd..f6380f5 100644 --- a/Runtime/Scripts/Core/IReceptor.cs +++ b/Runtime/Scripts/Core/IReceptor.cs @@ -1,3 +1,4 @@ +/* using UnityEngine; namespace NanoBrain { @@ -47,7 +48,7 @@ namespace NanoBrain { } public static class IReceptorHelpers { - /* + /// /// Implementation for the NanoBrain::IReceptor::AddReceptorElement which can be used for all implementations of IReceptor /// @@ -119,6 +120,7 @@ namespace NanoBrain { } - */ + } -} \ No newline at end of file +} +*/ \ No newline at end of file diff --git a/Runtime/Scripts/Core/Nucleus.cs b/Runtime/Scripts/Core/Nucleus.cs index 25f958f..60352b2 100644 --- a/Runtime/Scripts/Core/Nucleus.cs +++ b/Runtime/Scripts/Core/Nucleus.cs @@ -54,7 +54,7 @@ public abstract class Nucleus { Neuron, MemoryCell, Cluster, - Receptor, + //Receptor, //ClusterReceptor, //ClusterArray, } diff --git a/Runtime/Scripts/Core/Receptor.cs b/Runtime/Scripts/Core/Receptor.cs index 6e9efaf..a72da67 100644 --- a/Runtime/Scripts/Core/Receptor.cs +++ b/Runtime/Scripts/Core/Receptor.cs @@ -1,3 +1,4 @@ +/* using UnityEngine; #if UNITY_MATHEMATICS using Unity.Mathematics; @@ -55,7 +56,7 @@ namespace NanoBrain { return clone; } - /* + [SerializeReference] private NucleusArray _array; public NucleusArray array { @@ -78,7 +79,7 @@ namespace NanoBrain { public virtual void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1) { IReceptorHelpers.AddArrayReceiver(this, receiverToAdd, weight); } - */ + public override void UpdateStateIsolated() { this.outputValue = this.bias; @@ -116,4 +117,5 @@ namespace NanoBrain { } } -} \ No newline at end of file +} +*/ \ No newline at end of file From e2e169ca609e286cf0ec708ef9baccb684e25edb Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Fri, 17 Apr 2026 17:05:25 +0200 Subject: [PATCH 12/34] small fixes --- Editor/ClusterViewer.cs | 2 +- Runtime/Scripts/Brain.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index fe94305..6a386b1 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -545,7 +545,7 @@ namespace NanoBrain { if (e.type == EventType.MouseDown && e.button == 0) { // Consume the event so the scene doesn't also handle it e.Use(); - if (nucleus.parent != null && nucleus.parent is Cluster parentCluster2) + if (nucleus is Cluster parentCluster2) HandleClicked(parentCluster2); else HandleClicked(nucleus); diff --git a/Runtime/Scripts/Brain.cs b/Runtime/Scripts/Brain.cs index 93b05a7..81f2b4c 100644 --- a/Runtime/Scripts/Brain.cs +++ b/Runtime/Scripts/Brain.cs @@ -22,7 +22,7 @@ namespace NanoBrain { get { if (brainInstance == null && brainPrefab != null) { brainInstance = new Cluster(brainPrefab) { - name = brainPrefab.name + " (Instance)" + name = brainPrefab.name }; } else if (brainInstance != null && brainPrefab == null) { brainInstance = null; From c8f0f0c9da474a4b2b05830419c999ba8a95cb9c Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Mon, 20 Apr 2026 09:26:36 +0200 Subject: [PATCH 13/34] Fix aging of neurons --- Runtime/Scripts/Core/Cluster.cs | 21 ++++++++++++++------- Runtime/Scripts/Core/Neuron.cs | 31 +++++++++++++++++++++++++++---- Runtime/Scripts/Core/Synapse.cs | 6 ++++++ 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index 3264d8a..50ab0a0 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -382,14 +382,21 @@ namespace NanoBrain { } // Otherwise find the stalest cluster? - Cluster stalestCluster = this.siblingClusters[0]; - for (int ix = 1; ix < this.siblingClusters.Length; ix++) { - if (this.siblingClusters[ix].defaultOutput.stale > stalestCluster.defaultOutput.stale) - stalestCluster = this.siblingClusters[ix]; - } + // Cluster stalestCluster = this.siblingClusters[0]; + // for (int ix = 1; ix < this.siblingClusters.Length; ix++) { + // if (this.siblingClusters[ix].defaultOutput.stale > stalestCluster.defaultOutput.stale) + // stalestCluster = this.siblingClusters[ix]; + // } - RemoveThingCluster(stalestCluster); - return stalestCluster; + // Otherwise find longest unused cluster + Cluster unusedCluster = this.siblingClusters[0]; + for (int ix = 1; ix < this.siblingClusters.Length; ix++) { + if (this.siblingClusters[ix].defaultOutput.lastUpdate < unusedCluster.defaultOutput.lastUpdate) + unusedCluster = this.siblingClusters[ix]; + } + + RemoveThingCluster(unusedCluster); + return unusedCluster; } private void RemoveThingCluster(Cluster cluster) { diff --git a/Runtime/Scripts/Core/Neuron.cs b/Runtime/Scripts/Core/Neuron.cs index 16d4a1a..c622bcf 100644 --- a/Runtime/Scripts/Core/Neuron.cs +++ b/Runtime/Scripts/Core/Neuron.cs @@ -213,11 +213,23 @@ namespace NanoBrain { public Action WhenFiring; - public virtual bool isSleeping => this.outputMagnitude == 0; + public virtual bool isSleeping => Time.time - this.lastUpdate > this.timeToSleep; //this.outputMagnitude == 0; + public void SleepCheck() { + if (this.isSleeping) { +#if UNITY_MATHEMATICS + this.bias = new float3(0, 0, 0); +#else + this.bias = new Vector3(0,0,0); +#endif + } + } + // [NonSerialized] + // public int stale = 1000; [NonSerialized] - public int stale = 1000; - public readonly int staleValueForSleep = 20; + public float lastUpdate = 0; + // public readonly int staleValueForSleep = 20; + public readonly float timeToSleep = 1f; /// \copydoc NanoBrain::Nucleus::ShallowCloneTo public override Nucleus ShallowCloneTo(Cluster newParent) { @@ -288,8 +300,18 @@ namespace NanoBrain { } public override void UpdateStateIsolated() { + CheckSleepingSynapses(); var result = Combinator(); this.outputValue = Activator(result); + this.lastUpdate = Time.time; + } + + protected void CheckSleepingSynapses() { + foreach (Synapse synapse in this.synapses) { + if (synapse.isSleeping) { + synapse.neuron.outputValue = Vector3.zero; + } + } } #region Combinator @@ -524,7 +546,8 @@ namespace NanoBrain { } public void ProcessStimulusDirect(Vector3 inputValue) { //}, int thingId = 0, string thingName = null) { - this.stale = 0; + // this.stale = 0; + this.lastUpdate = Time.time; this.bias = inputValue; this.parent.UpdateFromNucleus(this); } diff --git a/Runtime/Scripts/Core/Synapse.cs b/Runtime/Scripts/Core/Synapse.cs index 63bacf7..e71130d 100644 --- a/Runtime/Scripts/Core/Synapse.cs +++ b/Runtime/Scripts/Core/Synapse.cs @@ -28,6 +28,12 @@ namespace NanoBrain { this.neuron = nucleus; this.weight = weight; } + + public bool isSleeping { + get { + return this.neuron.isSleeping; + } + } } } \ No newline at end of file From 75d9d1cd5c5cc8d224db58fc484c2c6e992403df Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Mon, 20 Apr 2026 09:39:36 +0200 Subject: [PATCH 14/34] Cleanup --- Runtime/Scripts/Core/Cluster.cs | 64 ----- Runtime/Scripts/Core/ClusterArray.cs | 145 ---------- Runtime/Scripts/Core/ClusterArray.cs.meta | 2 - Runtime/Scripts/Core/ClusterReceptor.cs | 280 ------------------- Runtime/Scripts/Core/ClusterReceptor.cs.meta | 2 - Runtime/Scripts/Core/IReceptor.cs | 126 --------- Runtime/Scripts/Core/IReceptor.cs.meta | 2 - Runtime/Scripts/Core/Neuron.cs | 20 +- Runtime/Scripts/Core/NucleusArray.cs | 199 ------------- Runtime/Scripts/Core/NucleusArray.cs.meta | 2 - Runtime/Scripts/Core/Receptor.cs | 121 -------- Runtime/Scripts/Core/Receptor.cs.meta | 2 - 12 files changed, 1 insertion(+), 964 deletions(-) delete mode 100644 Runtime/Scripts/Core/ClusterArray.cs delete mode 100644 Runtime/Scripts/Core/ClusterArray.cs.meta delete mode 100644 Runtime/Scripts/Core/ClusterReceptor.cs delete mode 100644 Runtime/Scripts/Core/ClusterReceptor.cs.meta delete mode 100644 Runtime/Scripts/Core/IReceptor.cs delete mode 100644 Runtime/Scripts/Core/IReceptor.cs.meta delete mode 100644 Runtime/Scripts/Core/NucleusArray.cs delete mode 100644 Runtime/Scripts/Core/NucleusArray.cs.meta delete mode 100644 Runtime/Scripts/Core/Receptor.cs delete mode 100644 Runtime/Scripts/Core/Receptor.cs.meta diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index 50ab0a0..731e435 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -117,44 +117,6 @@ namespace NanoBrain { } } - /* - // Copy nucleus arrays for receptors - for (int nucleusIx = 0; nucleusIx < prefabNuclei.Length; nucleusIx++) { - Nucleus prefabNucleus = prefabNuclei[nucleusIx]; - if (prefabNucleus is not IReceptor prefabReceptor) - continue; - - if (prefabReceptor.nucleiArray == null || prefabReceptor.nucleiArray.Length == 0) - continue; - - IReceptor clonedNucleus = clonedNuclei[nucleusIx] as IReceptor; - if (prefabReceptor == prefabReceptor.nucleiArray[0]) { - // We clone the array only for the first entry - NucleusArray clonedArray = new(prefabReceptor.nucleiArray.Length); - int arrayIx = 0; - foreach (Nucleus prefabArrayNucleus in prefabReceptor.nucleiArray) { - int arrayNucleusIx = GetNucleusIndex(prefabNuclei, prefabArrayNucleus); - if (arrayNucleusIx >= 0) { - Nucleus clonedArrayNucleus = clonedNuclei[arrayNucleusIx]; - clonedArray.nuclei[arrayIx] = clonedArrayNucleus; - } - else { - Debug.LogError($" Could not find prefab nucleus {prefabNucleus.name} in the clones"); - } - arrayIx++; - } - //clonedNucleus.array = clonedArray; - clonedNucleus.nucleiArray = clonedArray.nuclei; - } - else { - // The others will refer to the array created for the first nucleus in the array - int firstNucleusIx = GetNucleusIndex(prefabNuclei, prefabReceptor.nucleiArray[0]); - IReceptor clonedFirstNucleus = clonedNuclei[firstNucleusIx] as IReceptor; - clonedNucleus.nucleiArray = clonedFirstNucleus.nucleiArray; - } - } - */ - foreach (Nucleus nucleus in this.clusterNuclei) { if (nucleus is Cluster clonedSubCluster) RestoreAllExternalReceivers(clonedSubCluster, this.prefab, this); @@ -381,13 +343,6 @@ namespace NanoBrain { } } - // Otherwise find the stalest cluster? - // Cluster stalestCluster = this.siblingClusters[0]; - // for (int ix = 1; ix < this.siblingClusters.Length; ix++) { - // if (this.siblingClusters[ix].defaultOutput.stale > stalestCluster.defaultOutput.stale) - // stalestCluster = this.siblingClusters[ix]; - // } - // Otherwise find longest unused cluster Cluster unusedCluster = this.siblingClusters[0]; for (int ix = 1; ix < this.siblingClusters.Length; ix++) { @@ -575,11 +530,6 @@ namespace NanoBrain { } } - // [Obsolete("Use GetNucleus instead")] - // public IReceptor GetReceptor(string receptorName) { - // return GetNucleus(receptorName) as IReceptor; - // } - #region Receivers public virtual List CollectReceivers() { @@ -589,7 +539,6 @@ namespace NanoBrain { // Only add receivers outside this cluster if (receiver.clusterPrefab != this.prefab) receivers.Add(receiver); - //receivers.AddRange(output.receivers); } } return receivers; @@ -626,19 +575,6 @@ namespace NanoBrain { public override void UpdateStateIsolated() { throw new Exception("Cluster should not be updated!"); - // float3 sum = this.bias; - - // //Applying the weight factors - // foreach (Synapse synapse in this.synapses) { - // if (lengthsq(synapse.neuron.outputValue) > 0) { - // sum += synapse.weight * synapse.neuron.outputValue; - // } - // } - - // foreach (Nucleus nucleus in this.sortedNuclei) - // nucleus.UpdateStateIsolated(); - - // UpdateNuclei(); } public override void UpdateNuclei() { diff --git a/Runtime/Scripts/Core/ClusterArray.cs b/Runtime/Scripts/Core/ClusterArray.cs deleted file mode 100644 index 3d0bab3..0000000 --- a/Runtime/Scripts/Core/ClusterArray.cs +++ /dev/null @@ -1,145 +0,0 @@ -/* -using System; -using System.Collections.Generic; -using UnityEngine; - -namespace NanoBrain { - - [Serializable] - public class ClusterArray : Nucleus { - - public ClusterPrefab prefab; - [SerializeReference] - public Cluster[] clusters; - - public Dictionary thingClusters = new(); - - public ClusterArray(ClusterPrefab prefab, Cluster parent, int size, Nucleus receiver = null) { - this.prefab = prefab; - this.name = prefab.name; - this.clusters = new Cluster[size]; - for (int ix = 0; ix < size; ix++) { - Cluster cluster = new(prefab, parent); - cluster.defaultOutput.AddReceiver(receiver); - //cluster.clusterArray = this; - this.clusters[ix] = cluster; - } - } - - public ClusterArray(ClusterPrefab prefab, ClusterPrefab parent, int size, Nucleus receiver = null) { - this.prefab = prefab; - this.name = prefab.name; - this.clusters = new Cluster[size]; - for (int ix = 0; ix < size; ix++) { - Cluster cluster = new(prefab, parent); - cluster.defaultOutput.AddReceiver(receiver); - //cluster.clusterArray = this; - this.clusters[ix] = cluster; - } - } - - public override Nucleus ShallowCloneTo(Cluster parent) { - ClusterArray clone = new(this.prefab, parent, this.clusters.Length) { - clusterPrefab = this.clusterPrefab, - }; - - return clone; - } - - public override Nucleus Clone(ClusterPrefab parent) { - ClusterArray clone = new(this.prefab, parent, this.clusters.Length) { - }; - - return clone; - } - - public void Add(ClusterPrefab prefab) { - if (this.clusters.Length == 0) { - Debug.LogError("Empty perceptoid array, cannot add"); - return; - } - int newLength = this.clusters.Length + 1; - Cluster[] newClusters = new Cluster[newLength]; - - string baseName = this.name; - int colonPos = baseName.IndexOf(":"); - if (colonPos > 0) - baseName = baseName[..colonPos]; - - for (int i = 0; i < this.clusters.Length; i++) - newClusters[i] = this.clusters[i]; - Cluster sourceCluster = this.clusters[0]; - Cluster newCluster = sourceCluster.Clone(prefab) as Cluster; - newCluster.name = $"{baseName}: {newLength - 1}"; - //newCluster.clusterArray = this; - newClusters[newLength - 1] = newCluster; - this.clusters = newClusters; - } - - public void Remove() { - int newLength = this.clusters.Length - 1; - if (newLength == 0) { - Debug.LogWarning("Perceptoid array cannot be empty"); - return; - } - Cluster[] newClusters = new Cluster[newLength]; - for (int i = 0; i < newLength; i++) - newClusters[i] = this.clusters[i]; - // Delete the last perception - //Cluster.Delete(nucleus); - this.clusters = newClusters; - } - - public override void UpdateStateIsolated() { - // Clusters don't do anything, - // The nuclei in them do the work - // and should be called directly, not from the cluster - } - - public virtual Cluster GetThingCluster() { - Cluster selectedCluster = SelectCluster(); - return selectedCluster; - } - public virtual Cluster GetThingCluster(int thingId, string thingName = null) { - if (thingClusters.TryGetValue(thingId, out Cluster cluster)) - return cluster; - - Cluster selectedCluster = SelectCluster(); - thingClusters[thingId] = selectedCluster; - return selectedCluster; - } - - private Cluster SelectCluster() { - // Find a sleeping cluster - foreach (Cluster cluster in clusters) { - if (cluster.defaultOutput.isSleeping) { - RemoveThingCluster(cluster); - return cluster; - } - } - - // Otherwise find the stalest cluster? - Cluster stalestCluster = clusters[0]; - for (int ix = 1; ix < clusters.Length; ix++) { - if (clusters[ix].defaultOutput.stale > stalestCluster.defaultOutput.stale) - stalestCluster = clusters[ix]; - } - - RemoveThingCluster(stalestCluster); - return stalestCluster; - } - - private void RemoveThingCluster(Cluster cluster) { - List keysToRemove = new(); - foreach (KeyValuePair kvp in thingClusters) { - if (kvp.Value == cluster) - keysToRemove.Add(kvp.Key); - } - - foreach (int thingId in keysToRemove) - thingClusters.Remove(thingId); - } - } - -} -*/ \ No newline at end of file diff --git a/Runtime/Scripts/Core/ClusterArray.cs.meta b/Runtime/Scripts/Core/ClusterArray.cs.meta deleted file mode 100644 index 8125246..0000000 --- a/Runtime/Scripts/Core/ClusterArray.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 837dfcffeb99804479a0887cbfc33372 \ No newline at end of file diff --git a/Runtime/Scripts/Core/ClusterReceptor.cs b/Runtime/Scripts/Core/ClusterReceptor.cs deleted file mode 100644 index ac99b2d..0000000 --- a/Runtime/Scripts/Core/ClusterReceptor.cs +++ /dev/null @@ -1,280 +0,0 @@ -/* -using System; -using System.Collections.Generic; -using UnityEngine; -#if UNITY_MATHEMATICS -using Unity.Mathematics; -using static Unity.Mathematics.math; -#endif -using System.Linq; - -namespace NanoBrain { - - [Serializable] - public class ClusterReceptor : Cluster, IReceptor { - public ClusterReceptor(ClusterPrefab prefab, Cluster parent, string name) : base(prefab, parent) { - this.name = name; - this.array = new NucleusArray(this); - if (this.name.IndexOf(":") < 0) - this.name += ": 0"; - - } - public ClusterReceptor(ClusterPrefab prefab, ClusterPrefab parent, string name) : base(prefab, parent) { - this.name = name; - this.array = new NucleusArray(this); - } - - public string GetName() { - return this.name; - } - - public override Nucleus ShallowCloneTo(Cluster parent) { - ClusterReceptor clone = new(this.prefab, parent, this.name) { - clusterPrefab = this.clusterPrefab, - }; - - return clone; - } - - public override Nucleus Clone(ClusterPrefab parent) { - ClusterReceptor clone = new(prefab, parent, this.name) { - array = this._array - }; - - foreach (Synapse synapse in this.synapses) { - Synapse clonedSynapse = clone.AddSynapse(synapse.neuron); - clonedSynapse.weight = synapse.weight; - } - - this._outputs = null; // Make sure the output are regenerated - foreach (Neuron output in this.outputs) { - int ix = GetNucleusIndex(this.clusterNuclei, output); - if (ix < 0 || clone.clusterNuclei[ix] is not Neuron clonedOutput) - continue; - - foreach (Nucleus receiver in output.receivers) - clonedOutput.AddReceiver(receiver); - } - return clone; - } - - public override List CollectReceivers() { - List receivers = new(); - foreach (Nucleus element in this.nucleiArray) { - if (element is not Cluster clusterElement) - continue; - - foreach (Nucleus outputNucleus in clusterElement.clusterNuclei) { - if (outputNucleus is not Neuron output) - continue; - - // this should be clusterElement.outputs, - // but outputs is not updated when correctly and may contain old data... - foreach (Nucleus receiver in output.receivers) { - // Only add receivers outside clusterElement cluster - if (receiver.clusterPrefab != clusterElement.prefab && - receivers.Contains(receiver) == false) - receivers.Add(receiver); - } - } - } - return receivers; - } - - [SerializeReference] - private NucleusArray _array; - public NucleusArray array { - set { _array = value; } - } - - public Nucleus[] nucleiArray { - get { return _array.nuclei; } - set { _array.nuclei = value; } - } - - public void AddReceptorElement(ClusterPrefab prefab) { - IReceptorHelpers.AddReceptorElement(this, prefab); - } - - public void RemoveReceptorElement() { - IReceptorHelpers.RemoveReceptorElement(this); - } - - public void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1) { - IReceptorHelpers.AddArrayReceiver(this, receiverToAdd, weight); - } - - public override void UpdateStateIsolated() { - // Clusters don't do anything, - // The nuclei in them do the work - // and should be called directly, not from the cluster - } - - public override void UpdateNuclei() { - foreach (Nucleus nucleus in this.clusterNuclei) - nucleus.UpdateNuclei(); - } - - public override void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null) { - Debug.LogError("Process Stimulus was called on clusterreceptor without a neuron specified"); - } - - private readonly Dictionary thingReceivers = new(); - - public virtual void ProcessStimulus(Neuron input, Vector3 inputValue, int thingId = 0, string thingName = null) { - CleanupReceivers(); - - if (!thingReceivers.TryGetValue(thingId, out ClusterReceptor selectedReceiver)) - selectedReceiver = FindReceiver2(thingId, inputValue, input); - if (selectedReceiver == null) - return; - - if (thingName != null) { - string baseName = selectedReceiver.name; - int colonPos = selectedReceiver.name.IndexOf(":"); - if (colonPos > 0) - baseName = selectedReceiver.name[..colonPos]; - selectedReceiver.name = baseName + ": " + thingName; - } - - int inputIx = GetNucleusIndex(this.clusterNuclei, input); - if (inputIx < 0) - return; - - if (selectedReceiver.clusterNuclei[inputIx] is Neuron selectedNeuron) - selectedNeuron.ProcessStimulusDirect(inputValue); - } - -#if UNITY_MATHEMATICS - - private ClusterReceptor FindReceiver2(int thingId, float3 inputValue, Neuron input) { - // No existing nucleus for this thing - ClusterReceptor selectedReceiver = null; - float selectedMagnitude = 0; - foreach (ClusterReceptor receiver in this.nucleiArray.Cast()) { - if (thingReceivers.ContainsValue(receiver) == false) { - // We found an unusued receiver - thingReceivers.Add(thingId, receiver); - return receiver; - } - else if (receiver.defaultOutput.isSleeping) { - // A sleeping receiver is not active and can therefore always be used - thingReceivers.Add(thingId, receiver); - receiver.bias = float3(0, 0, 0); - return receiver; - } - else if (selectedReceiver == null) { - // If we haven't found a receiver yet, just start by taking the first - selectedReceiver = receiver; - selectedMagnitude = length(selectedReceiver.defaultOutput.outputValue); - } - // Look for the receiver with the lowest output magnitude - else { - float magnitude = length(receiver.defaultOutput.outputValue); - - if (length(receiver.defaultOutput.outputValue) < selectedMagnitude) { - selectedReceiver = receiver; - selectedMagnitude = length(selectedReceiver.defaultOutput.outputValue); - } - } - } - if (selectedReceiver != null) { - // To re-initialize the cluster (esp. memory cells) - // we update the cluster neuron twice. - // Bit of a hack..... - int inputIx = GetNucleusIndex(this.clusterNuclei, input); - if (inputIx >= 0) { - if (selectedReceiver.clusterNuclei[inputIx] is Neuron selectedNeuron) - selectedNeuron.ProcessStimulusDirect(inputValue); - } - - // Replace the receiver - // Find the thingId current associated with the receiver - int keyToRemove = thingReceivers.FirstOrDefault(r => r.Value.Equals(selectedReceiver)).Key; - if (keyToRemove != 0 || thingReceivers.ContainsKey(keyToRemove)) - thingReceivers.Remove(keyToRemove); - // And add the new association - thingReceivers.Add(thingId, selectedReceiver); - } - return selectedReceiver; - } - -#else - - private ClusterReceptor FindReceiver2(int thingId, Vector3 inputValue, Neuron input) { - // No existing nucleus for this thing - ClusterReceptor selectedReceiver = null; - float selectedMagnitude = 0; - foreach (ClusterReceptor receiver in this.nucleiArray.Cast()) { - if (thingReceivers.ContainsValue(receiver) == false) { - // We found an unusued receiver - thingReceivers.Add(thingId, receiver); - return receiver; - } - else if (receiver.defaultOutput.isSleeping) { - // A sleeping receiver is not active and can therefore always be used - thingReceivers.Add(thingId, receiver); - receiver.bias = new Vector3(0, 0, 0); - return receiver; - } - else if (selectedReceiver == null) { - // If we haven't found a receiver yet, just start by taking the first - selectedReceiver = receiver; - selectedMagnitude = selectedReceiver.defaultOutput.outputValue.magnitude; - } - // Look for the receiver with the lowest output magnitude - else { - float magnitude = receiver.defaultOutput.outputValue.magnitude; - - if (receiver.defaultOutput.outputValue.magnitude < selectedMagnitude) { - selectedReceiver = receiver; - selectedMagnitude = selectedReceiver.defaultOutput.outputValue.magnitude; - } - } - } - if (selectedReceiver != null) { - // To re-initialize the cluster (esp. memory cells) - // we update the cluster neuron twice. - // Bit of a hack..... - int inputIx = GetNucleusIndex(this.clusterNuclei, input); - if (inputIx >= 0) { - if (selectedReceiver.clusterNuclei[inputIx] is Neuron selectedNeuron) - selectedNeuron.ProcessStimulusDirect(inputValue); - } - - // Replace the receiver - // Find the thingId current associated with the receiver - int keyToRemove = thingReceivers.FirstOrDefault(r => r.Value.Equals(selectedReceiver)).Key; - if (keyToRemove != 0 || thingReceivers.ContainsKey(keyToRemove)) - thingReceivers.Remove(keyToRemove); - // And add the new association - thingReceivers.Add(thingId, selectedReceiver); - } - return selectedReceiver; - } - -#endif - - private void CleanupReceivers() { - // Remove a thing-receiver connection when the nucleus is inactive - List receiversToRemove = new(); - foreach (KeyValuePair item in thingReceivers) { - if (item.Value != null && item.Value.defaultOutput.isSleeping) - receiversToRemove.Add(item.Key); - } - foreach (int thingId in receiversToRemove) { - Nucleus selectedReceiver = thingReceivers[thingId]; - - thingReceivers.Remove(thingId); - - int colonPos = selectedReceiver.name.IndexOf(":"); - if (colonPos > 0) - selectedReceiver.name = selectedReceiver.name[..colonPos]; - - } - } - - } - -} -*/ \ No newline at end of file diff --git a/Runtime/Scripts/Core/ClusterReceptor.cs.meta b/Runtime/Scripts/Core/ClusterReceptor.cs.meta deleted file mode 100644 index 027f164..0000000 --- a/Runtime/Scripts/Core/ClusterReceptor.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 4f64f5d72a422a7c8bb9ace598432aad \ No newline at end of file diff --git a/Runtime/Scripts/Core/IReceptor.cs b/Runtime/Scripts/Core/IReceptor.cs deleted file mode 100644 index f6380f5..0000000 --- a/Runtime/Scripts/Core/IReceptor.cs +++ /dev/null @@ -1,126 +0,0 @@ -/* -using UnityEngine; - -namespace NanoBrain { - - /// - /// A Receptor is a Nucleus which can receive input (called Stimulus) from outside the the cluster/brain - /// - /// It has the ability to distinguish stimuli from different things using an array of Nuclei - public interface IReceptor { - /// - /// Get the name of the receptor - /// - /// The name of the receptor - public string GetName(); - - /// - /// The array of nuclei used to track multiple things sending stimuli - /// - /// The size of the array determines the maximum number of things which can be distinguished - // public Nucleus[] nucleiArray { get; set; } - - /// - /// Extends the nucleiArray with an additional element - /// - /// A prefab of the nucleus to add? - // public void AddReceptorElement(ClusterPrefab prefab); - /// - /// Removes the last element from the nucleiArray - /// - // public void RemoveReceptorElement(); - - /// - /// Add a receiver for this receptor array - /// - /// The receiving Nucleus - /// The initial weight to use for the synapses - /// This function will add a synapse to the receiver for each element in the nucleiArray. - // public void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1); - - /// - /// Process an external stimulus - /// - /// The value of the stimulus - /// The id of the thing causing the stimulus - /// The name of the thing causing the stimulus - public void ProcessStimulus(Vector3 inputValue, int thingId = 0, string thingName = null); - } - - public static class IReceptorHelpers { - - /// - /// Implementation for the NanoBrain::IReceptor::AddReceptorElement which can be used for all implementations of IReceptor - /// - /// The IReceptor which needs to extend its nucleiArray - /// A prefab of the nucleus to add? - public static void AddReceptorElement(IReceptor receptor, ClusterPrefab prefab) { - if (receptor.nucleiArray.Length == 0) { - Debug.LogError("Empty perceptoid array, cannot add"); - } - int newLength = receptor.nucleiArray.Length + 1; - Nucleus[] newArray = new Nucleus[newLength]; - - string baseName = receptor.GetName(); - int colonPos = baseName.IndexOf(":"); - if (colonPos > 0) - baseName = baseName[..colonPos]; - - for (int i = 0; i < receptor.nucleiArray.Length; i++) - newArray[i] = receptor.nucleiArray[i]; - if (receptor.nucleiArray[0] is Nucleus nucleus) { - newArray[newLength - 1] = nucleus.Clone(prefab); - newArray[newLength - 1].name = $"{baseName}: {newLength - 1}"; - } - - foreach (Nucleus element in receptor.nucleiArray) { - if (element is IReceptor receptorElement) { - receptorElement.nucleiArray = newArray; - } - } - } - - /// - /// Implementation for the NanoBrain::IReceptor::RemoteReceptorElement which can be used for all implementations of IReceptor - /// - /// The IReceptor which needs to shorten its nucleiArray - public static void RemoveReceptorElement(IReceptor receptor) { - int newLength = receptor.nucleiArray.Length - 1; - if (newLength == 0) { - Debug.LogWarning("Perceptoid array cannot be empty"); - } - Nucleus[] newArray = new Nucleus[newLength]; - for (int i = 0; i < newLength; i++) - newArray[i] = receptor.nucleiArray[i]; - // Delete the last perception - if (receptor.nucleiArray[newLength] is Nucleus nucleus) - Neuron.Delete(nucleus); - - foreach (Nucleus element in receptor.nucleiArray) { - if (element is IReceptor receptorElement) { - receptorElement.nucleiArray = newArray; - } - } - - } - - /// - /// Implementation for the NanoBreain::IRceptor::AddArrayReceiver which can be used for all implementations of IReceptor - /// - /// The IReceptor for which a receiving nuclues needs to be added - /// The nucleus to receive input from the receptor - /// The initial weight for the synapses - public static void AddArrayReceiver(IReceptor receptor, Nucleus receiverToAdd, float weight = 1) { - foreach (Nucleus element in receptor.nucleiArray) { - if (element is Cluster cluster) - cluster.defaultOutput.AddReceiver(receiverToAdd, weight); - if (element is Neuron neuron) - neuron.AddReceiver(receiverToAdd, weight); - } - - } - - - } -} -*/ \ No newline at end of file diff --git a/Runtime/Scripts/Core/IReceptor.cs.meta b/Runtime/Scripts/Core/IReceptor.cs.meta deleted file mode 100644 index 0c0ee6f..0000000 --- a/Runtime/Scripts/Core/IReceptor.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 73f052292ad16bb53a3c07aa1694c705 \ No newline at end of file diff --git a/Runtime/Scripts/Core/Neuron.cs b/Runtime/Scripts/Core/Neuron.cs index c622bcf..f4b7338 100644 --- a/Runtime/Scripts/Core/Neuron.cs +++ b/Runtime/Scripts/Core/Neuron.cs @@ -521,32 +521,14 @@ namespace NanoBrain { } public virtual void RemoveReceiver(Nucleus receiverToRemove) { - // if (this is IReceptor receptor) { - // foreach (Nucleus element in receptor.nucleiArray) { - // if (element is Neuron neuron) { - // neuron._receivers.RemoveAll(receiver => receiver == receiverToRemove); - // receiverToRemove.synapses.RemoveAll(synapse => synapse.neuron == neuron); - // } - // } - // } - // else { this._receivers.RemoveAll(receiver => receiver == receiverToRemove); receiverToRemove.synapses.RemoveAll(synapse => synapse.neuron == this); - // } } #endregion Receivers - public override void ProcessStimulus(Vector3 inputValue) { //, int thingId = 0, string thingName = null) { - // if (this.parent is ClusterReceptor clusterReceptor) - // clusterReceptor.ProcessStimulus(this, inputValue, thingId, thingName); - // else - ProcessStimulusDirect(inputValue); //, thingId, thingName); - } - - public void ProcessStimulusDirect(Vector3 inputValue) { //}, int thingId = 0, string thingName = null) { - // this.stale = 0; + public override void ProcessStimulus(Vector3 inputValue) {; this.lastUpdate = Time.time; this.bias = inputValue; this.parent.UpdateFromNucleus(this); diff --git a/Runtime/Scripts/Core/NucleusArray.cs b/Runtime/Scripts/Core/NucleusArray.cs deleted file mode 100644 index 672f523..0000000 --- a/Runtime/Scripts/Core/NucleusArray.cs +++ /dev/null @@ -1,199 +0,0 @@ -/* -using System.Linq; -using System.Collections.Generic; -using UnityEngine; -#if UNITY_MATHEMATICS -using Unity.Mathematics; -using static Unity.Mathematics.math; -#endif - -namespace NanoBrain { - - /// - /// Class to manage an array of nuclei for an IReceptor - /// - /// Would love to get rid of this class. - [System.Serializable] - public class NucleusArray { - /// - /// The nuclei in this array - /// - [SerializeReference] - private Nucleus[] _nuclei; - public Nucleus[] nuclei { - get { - return _nuclei; - } - set { - _nuclei = value; - } - } - - /// - /// Create a new NucleusArray with the given nucleus - /// - /// The Nucleus to put in the NucleusArray - /// This results in an nucleus array of size 1 - public NucleusArray(Nucleus nucleus) { - this._nuclei = new Nucleus[1]; - this._nuclei[0] = nucleus; - } - /// - /// Create a new NucleusArray of the given size - /// - /// The size of the nucluesArray - public NucleusArray(int size) { - this._nuclei = new Nucleus[size]; - } - - - // public void AddNucleus(ClusterPrefab prefab) { - // if (this._nuclei.Length == 0) { - // Debug.LogError("Empty perceptoid array, cannot add"); - // return; - // } - // int newLength = this._nuclei.Length + 1; - // Nucleus[] newArray = new Nucleus[newLength]; - - // for (int i = 0; i < this._nuclei.Length; i++) - // newArray[i] = this._nuclei[i]; - // if (this._nuclei[0] is Nucleus nucleus) { - // newArray[newLength - 1] = nucleus.Clone(prefab); - // newArray[newLength - 1].name += $": {newLength - 1}"; - // } - - // this._nuclei = newArray; - // } - - // public void RemoveNucleus() { - // int newLength = this._nuclei.Length - 1; - // if (newLength == 0) { - // Debug.LogWarning("Perceptoid array cannot be empty"); - // return; - // } - // Nucleus[] newPerceptei = new Nucleus[newLength]; - // for (int i = 0; i < newLength; i++) - // newPerceptei[i] = this._nuclei[i]; - // // Delete the last perception - // if (this._nuclei[newLength] is Nucleus nucleus) - // Neuron.Delete(nucleus); //this._nuclei[newLength]); - - // this._nuclei = newPerceptei; - // } - - public Dictionary thingReceivers = new(); - -#if UNITY_MATHEMATICS - - private Nucleus FindReceiver(int thingId, float3 inputValue) { - float inputMagnitude = length(inputValue); - return FindReceiverMagnitude(thingId, inputMagnitude); - } - -#else - - private Nucleus FindReceiver(int thingId, Vector3 inputValue) { - float inputMagnitude = inputValue.magnitude; - return FindReceiverMagnitude(thingId, inputMagnitude); - } - -#endif - - private Nucleus FindReceiverMagnitude(int thingId, float inputMagnitude) { - Neuron selectedReceiver = null; - float selectedMagnitude = 0; - foreach (Nucleus nucleusReceiver in this._nuclei) { - if (nucleusReceiver is not Neuron receiver) - continue; - if (thingReceivers.ContainsValue(receiver) == false) { - // We found an unusued receiver - thingReceivers.Add(thingId, receiver); - return receiver; - } - else if (receiver.isSleeping) { - // A sleeping receiver is not active and can therefore always be used - thingReceivers.Add(thingId, receiver); - return receiver; - } - else if (selectedReceiver == null) { - // If we haven't found a receiver yet, just start by taking the first - selectedReceiver = receiver; - selectedMagnitude = selectedReceiver.outputMagnitude; - } - // Look for the receiver with the lowest magnitude - else { - float magnitude = receiver.outputMagnitude; - - if (magnitude < inputMagnitude && receiver.outputMagnitude < selectedMagnitude) { - selectedReceiver = receiver; - selectedMagnitude = selectedReceiver.outputMagnitude; - } - } - } - if (selectedReceiver != null) { - // Replace the receiver - // Find the thingId current associated with the receiver - int keyToRemove = thingReceivers.FirstOrDefault(r => r.Value.Equals(selectedReceiver)).Key; - if (keyToRemove != 0 || thingReceivers.ContainsKey(keyToRemove)) - thingReceivers.Remove(keyToRemove); - // And add the new association - thingReceivers.Add(thingId, selectedReceiver); - } - return selectedReceiver; - } - - /// - /// Process an external stimulus - /// - /// The value of the stimulus - /// The id of the thing causing the stimulus - /// The name of the thing causing the stimulus - public virtual void ProcessStimulus(int thingId, Vector3 inputValue, string thingName = null) { - CleanupReceivers(); - - if (this._nuclei[0] is Neuron neuron) - inputValue = neuron.Activator(inputValue); - - if (!thingReceivers.TryGetValue(thingId, out Nucleus selectedReceiver)) { - // No existing nucleus for this thing - selectedReceiver = FindReceiver(thingId, inputValue); - } - if (selectedReceiver == null) - return; - - if (thingName != null) { - string baseName = selectedReceiver.name; - int colonPos = selectedReceiver.name.IndexOf(":"); - if (colonPos > 0) - baseName = selectedReceiver.name[..colonPos]; - selectedReceiver.name = baseName + ": " + thingName; - } - - if (selectedReceiver is Neuron selectedNucleus) - selectedNucleus.ProcessStimulusDirect(inputValue); - } - - /// - /// Remove a thing-receiver connection when the nucleus is inactive - /// - private void CleanupReceivers() { - List receiversToRemove = new(); - foreach (KeyValuePair item in thingReceivers) { - if (item.Value != null && item.Value is Neuron neuron && neuron.isSleeping) - receiversToRemove.Add(item.Key); - } - foreach (int thingId in receiversToRemove) { - Nucleus selectedReceiver = thingReceivers[thingId]; - - thingReceivers.Remove(thingId); - - int colonPos = selectedReceiver.name.IndexOf(":"); - if (colonPos > 0) - selectedReceiver.name = selectedReceiver.name[..colonPos]; - - } - } - } - -} -*/ \ No newline at end of file diff --git a/Runtime/Scripts/Core/NucleusArray.cs.meta b/Runtime/Scripts/Core/NucleusArray.cs.meta deleted file mode 100644 index 61e26b7..0000000 --- a/Runtime/Scripts/Core/NucleusArray.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: f8cac60bd79854595a8571c042f77998 \ No newline at end of file diff --git a/Runtime/Scripts/Core/Receptor.cs b/Runtime/Scripts/Core/Receptor.cs deleted file mode 100644 index a72da67..0000000 --- a/Runtime/Scripts/Core/Receptor.cs +++ /dev/null @@ -1,121 +0,0 @@ -/* -using UnityEngine; -#if UNITY_MATHEMATICS -using Unity.Mathematics; -using static Unity.Mathematics.math; -#endif - -namespace NanoBrain { - - /// - /// Basic IReceptor to receive external input - /// - [System.Serializable] - public class Receptor : Neuron { //}, IReceptor { - /// - /// Create a new Receptor in a Cluster instance - /// - /// The Cluster in which the Receptor is created - /// The name of the new Receptor - public Receptor(Cluster parent, string name) : base(parent, name) { - //this.array = new NucleusArray(this); - // if (this.name.IndexOf(":") < 0) - // this.name += ": 0"; - } - /// - /// Create a new Receptor in a Cluster Prefab - /// - /// The Cluster Prefab in which the Receptor is created - /// The name of the new Receptor - public Receptor(ClusterPrefab prefab, string name) : base(prefab, name) { - //this.array = new NucleusArray(this); - } - - public string GetName() { - return this.name; - } - - /// \copydoc NanoBrain::Neuron::ShallowCloneTo - public override Nucleus ShallowCloneTo(Cluster parent) { - Receptor clone = new(parent, name) { - - }; - CloneFields(clone); - return clone; - } - /// \copydoc NanoBrain::Neuron::Clone - public override Nucleus Clone(ClusterPrefab prefab) { - Receptor clone = new(prefab, name) { - //array = this._array - }; - CloneFields(clone); - // Adding receivers will also add synapses to the receivers - foreach (Nucleus receiver in this.receivers.ToArray()) - clone.AddReceiver(receiver); - - return clone; - } - - - [SerializeReference] - private NucleusArray _array; - public NucleusArray array { - set { _array = value; } - } - - public Nucleus[] nucleiArray { - get { return _array.nuclei; } - set { _array.nuclei = value; } - } - - public void AddReceptorElement(ClusterPrefab prefab) { - IReceptorHelpers.AddReceptorElement(this, prefab); - } - - public void RemoveReceptorElement() { - IReceptorHelpers.RemoveReceptorElement(this); - } - - public virtual void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1) { - IReceptorHelpers.AddArrayReceiver(this, receiverToAdd, weight); - } - - - public override void UpdateStateIsolated() { - this.outputValue = this.bias; - } - -#if UNITY_MATHEMATICS - - public override void UpdateNuclei() { - this.stale++; - if (this.stale > staleValueForSleep && lengthsq(this.bias) > 0) { - this.bias = new float3(0, 0, 0); - this.parent.UpdateFromNucleus(this); - } - } - -#else - - public override void UpdateNuclei() { - this.stale++; - if (this.stale > staleValueForSleep && this.bias.sqrMagnitude > 0) { - this.bias = new Vector3(0, 0, 0); - this.parent.UpdateFromNucleus(this); - } - } - - -#endif - public override void ProcessStimulus(Vector3 inputValue) { //}, int thingId = 0, string thingName = null) { - // this._array ??= new NucleusArray(this.parent); - // this._array.ProcessStimulus(thingId, inputValue, thingName); - - this.stale = 0; - this.bias = inputValue; - this.parent.UpdateFromNucleus(this); - } - - } -} -*/ \ No newline at end of file diff --git a/Runtime/Scripts/Core/Receptor.cs.meta b/Runtime/Scripts/Core/Receptor.cs.meta deleted file mode 100644 index 56793ae..0000000 --- a/Runtime/Scripts/Core/Receptor.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: cfb9734aebc3ab85aacf87d26fb92e55 \ No newline at end of file From 308a6a1ee7893c6dd02208045b02fce5483add50 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Mon, 20 Apr 2026 12:53:58 +0200 Subject: [PATCH 15/34] The Entities are battling --- Editor/ClusterInspector.cs | 89 +++++++++++---------------------- Editor/ClusterViewer.cs | 43 ++++------------ Runtime/Scripts/Core/Cluster.cs | 26 ++++++++++ Runtime/Scripts/Core/Neuron.cs | 18 +++++-- 4 files changed, 78 insertions(+), 98 deletions(-) diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterInspector.cs index d4fe1c6..60ff610 100644 --- a/Editor/ClusterInspector.cs +++ b/Editor/ClusterInspector.cs @@ -174,23 +174,6 @@ namespace NanoBrain { memory.staticMemory = EditorGUILayout.Toggle("Static Memory", memory.staticMemory); } - // if (this.currentNucleus is IReceptor receptor1) { - // EditorGUILayout.BeginHorizontal(); - // EditorGUILayout.IntField("Array size", receptor1.nucleiArray.Count()); - // if (GUILayout.Button("Add")) { - // Undo.RecordObject(prefabAsset, "Array add " + prefabAsset.name); - // receptor1.AddReceptorElement(this.prefab); - // anythingChanged = true; - // } - // if (GUILayout.Button("Del")) { - // Undo.RecordObject(prefabAsset, "Array delete " + prefabAsset.name); - // receptor1.RemoveReceptorElement(); - // anythingChanged = true; - // } - // EditorGUILayout.EndHorizontal(); - - // } - // else if (this.currentNucleus is Cluster cluster) { EditorGUILayout.BeginHorizontal(); if (cluster.siblingClusters != null && cluster.siblingClusters.Length > 1) @@ -356,6 +339,8 @@ namespace NanoBrain { DeleteNucleus(this.currentNucleus); if (this.currentNucleus is Cluster subCluster) { + if (GUILayout.Button("Reimport Cluster")) + ReimportCluster(subCluster); if (GUILayout.Button("Edit Cluster")) EditCluster(subCluster); } @@ -376,26 +361,26 @@ namespace NanoBrain { } } - void OnSceneGUI(SceneView sceneView) { - if (this.gameObject != null) { - // 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.color = Color.yellow; - Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector); - } - // } - } - } + // void OnSceneGUI(SceneView sceneView) { + // if (this.gameObject != null) { + // // 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.color = Color.yellow; + // Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector); + // } + // // } + // } + // } #region Synapses @@ -446,36 +431,18 @@ namespace NanoBrain { subclusterInstance.defaultOutput.AddReceiver(nucleus); } - // protected virtual void AddReceptorInput(Nucleus nucleus) { - // Receptor newReceptor = new(this.prefab, "New Receptor"); - // newReceptor.AddReceiver(nucleus); - // this.currentNucleus = newReceptor; - // BuildLayers(); - // } - - // protected virtual void AddClusterReceptorInput(Nucleus nucleus) { - // ClusterPickerWindow.ShowPicker(prefab => OnClusterReceptorPicked(nucleus, prefab), "Select Cluster"); - // } - // private void OnClusterReceptorPicked(Nucleus nucleus, ClusterPrefab selectedPrefab) { - // ClusterReceptor clusterInstance = new(selectedPrefab, this.prefab, "New " + selectedPrefab.name); - // clusterInstance.defaultOutput.AddReceiver(nucleus); - // this.currentNucleus = clusterInstance; - // BuildLayers(); - // } - private void EditCluster(Cluster subCluster) { // May be used with storedPrefab... Selection.activeObject = subCluster.prefab; EditorGUIUtility.PingObject(subCluster.prefab); - var editor = Editor.CreateEditor(subCluster.prefab); + _ = CreateEditor(subCluster.prefab); } - // protected virtual void AddClusterArrayInput(Nucleus nucleus) { - // ClusterPickerWindow.ShowPicker(prefab => OnPickedClusterArray(nucleus, prefab), "Select Cluster"); - // } - // private void OnPickedClusterArray(Nucleus nucleus, ClusterPrefab selectedPrefab) { - // _ = new ClusterArray(selectedPrefab, this.prefab, 1, nucleus); - // } + private void ReimportCluster(Cluster subCluster) { + Cluster reimportedCluster = new(subCluster.prefab, this.prefab); + subCluster.MoveReceivers(reimportedCluster); + // subcluster should be garbage now... + } int selectedConnectNucleus = -1; // Connect to another nucleus in the same cluster diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index 6a386b1..8e55da5 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -321,17 +321,9 @@ namespace NanoBrain { if (synapse.neuron == null) continue; - // if (synapse.neuron is Receptor receptor) { - // if (drawnArrays.Contains(receptor.nucleiArray)) - // continue; - // drawnArrays.Add(receptor.nucleiArray); - // } - // else if (synapse.neuron.parent is ClusterReceptor clusterReceptor) { - // if (drawnArrays.Contains(clusterReceptor.nucleiArray)) - // continue; - // drawnArrays.Add(clusterReceptor.nucleiArray); - // } - if (synapse.neuron.parent is Cluster cluster && cluster.siblingClusters != null) { + if (synapse.neuron.parent is Cluster cluster && + cluster.siblingClusters != null && + synapse.neuron.parent != nucleus.parent) { if (drawnArrays.Contains(cluster.siblingClusters)) continue; drawnArrays.Add(cluster.siblingClusters); @@ -421,28 +413,6 @@ namespace NanoBrain { fontStyle = FontStyle.Bold, }; - // if (nucleus is IReceptor receptor1) { - // if (expandArray) { - // // Put array indices above elements - // style.alignment = TextAnchor.LowerCenter; - // Vector3 labelPos1 = position + Vector3.down * (size + 5); // below disc - // int colonPos1 = nucleus.name.IndexOf(":"); - // if (colonPos1 > 0) { - // string extName = nucleus.name[(colonPos1 + 2)..]; - // Handles.Label(labelPos1, extName, style); - // } - // } - // else { - // // draw the array size label - // if (color.grayscale > 0.5f) - // style.normal.textColor = Color.black; - // else - // style.normal.textColor = Color.white; - // Handles.Label(labelPosition, receptor1.nucleiArray.Length.ToString(), style); - // style.normal.textColor = Color.white; - // } - // } - // else if (nucleus.parent != null && nucleus.parent is Cluster parentCluster) { if (expandArray) { // Put array indices above elements @@ -579,6 +549,13 @@ namespace NanoBrain { else expandArray = false; } + else if (nucleus.parent != this.currentNucleus.parent) { + // We go to a different cluster + // select the cluster, not the neuron in the cluster + this.currentNucleus = nucleus.parent; + expandArray = false; + BuildLayers(); + } else { this.currentNucleus = nucleus; expandArray = false; diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index 731e435..2fb58b2 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -544,6 +544,32 @@ namespace NanoBrain { return receivers; } + public void MoveReceivers(Cluster newCluster) { + foreach (Nucleus outputNucleus in this.clusterNuclei) { + if (outputNucleus is not Neuron output) + continue; + + // Find the existing output in the new cluster + if (newCluster.GetNucleus(output.name) is not Neuron newOutput) { + Debug.LogWarning("Could not find output {output.name} in {newCluster.name}"); + continue; + } + Debug.Log($"Check {output.name} receivers"); + Nucleus[] receivers = output.receivers.ToArray(); + foreach (Nucleus receiver in receivers) { + Debug.Log("."); + if (receiver.clusterPrefab != this.prefab) { + // Replace synapse with new synapse + // to the new cluster + Debug.Log($"move {receiver.name} from {output.name} to {newOutput.name}"); + Synapse synapse = receiver.GetSynapse(output); + newOutput.AddReceiver(receiver, synapse.weight); + output.RemoveReceiver(receiver); + } + } + } + } + #endregion Receivers #region Update diff --git a/Runtime/Scripts/Core/Neuron.cs b/Runtime/Scripts/Core/Neuron.cs index f4b7338..530f44f 100644 --- a/Runtime/Scripts/Core/Neuron.cs +++ b/Runtime/Scripts/Core/Neuron.cs @@ -209,7 +209,12 @@ namespace NanoBrain { public float outputSqrMagnitude => _outputValue.sqrMagnitude; #endif - public bool isFiring => this.outputMagnitude > 0.5f; + public bool isFiring { + get { + SleepCheck(); + return this.outputMagnitude > 0.5f; + } + } public Action WhenFiring; @@ -221,14 +226,12 @@ namespace NanoBrain { #else this.bias = new Vector3(0,0,0); #endif + this._outputValue = this.bias; } } - // [NonSerialized] - // public int stale = 1000; [NonSerialized] public float lastUpdate = 0; - // public readonly int staleValueForSleep = 20; public readonly float timeToSleep = 1f; /// \copydoc NanoBrain::Nucleus::ShallowCloneTo @@ -521,8 +524,15 @@ namespace NanoBrain { } public virtual void RemoveReceiver(Nucleus receiverToRemove) { + int n1 = _receivers.Count; this._receivers.RemoveAll(receiver => receiver == receiverToRemove); + int n2 = _receivers.Count; + Debug.Log($" Removed {n1} - {n2} receivers"); + + n1 = receiverToRemove.synapses.Count; receiverToRemove.synapses.RemoveAll(synapse => synapse.neuron == this); + n2 = receiverToRemove.synapses.Count; + Debug.Log($" Removed {n1} - {n2} synapses"); } From 249e88850a2f24f52ac7c2cf3a1a455ab0edabe7 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Mon, 20 Apr 2026 16:06:33 +0200 Subject: [PATCH 16/34] Improve full graph view --- Editor/BrainEditorWindow.cs | 242 ++---- Editor/ClusterInspector.cs | 1372 ++++---------------------------- Editor/ClusterViewer.cs | 17 +- Editor/DAGWindow.cs | 356 --------- Editor/DAGWindow.cs.meta | 2 - Runtime/Scripts/Core/Neuron.cs | 9 +- 6 files changed, 243 insertions(+), 1755 deletions(-) delete mode 100644 Editor/DAGWindow.cs delete mode 100644 Editor/DAGWindow.cs.meta diff --git a/Editor/BrainEditorWindow.cs b/Editor/BrainEditorWindow.cs index 195ac6a..2b16ffe 100644 --- a/Editor/BrainEditorWindow.cs +++ b/Editor/BrainEditorWindow.cs @@ -29,10 +29,6 @@ namespace NanoBrain { const float minZoom = 0.5f; const float maxZoom = 2.0f; - // Vector2 dragStart; - // bool draggingNode = false; - // int draggingNodeId = -1; - private readonly System.Type acceptedType = typeof(ClusterPrefab); [MenuItem("Window/Brain Viewer")] @@ -42,14 +38,10 @@ namespace NanoBrain { } void OnEnable() { - // if (nodes.Count == 0) - // CreateSampleGraph(); - - // Register callback so window updates when selection changes Selection.selectionChanged += OnSelectionChanged; RefreshSelection(); - ComputeLeftToRightLayout(); + ComputeLayout(); } private void OnDisable() { @@ -58,7 +50,7 @@ namespace NanoBrain { private void OnSelectionChanged() { RefreshSelection(); - ComputeLeftToRightLayout(); + ComputeLayout(); Repaint(); } @@ -86,27 +78,6 @@ namespace NanoBrain { } } - - // void CreateSampleGraph() { - // nodes.Clear(); - // edges.Clear(); - - // nodes.Add(new DagNode() { id = 0, title = "In1" }); - // nodes.Add(new DagNode() { id = 1, title = "In2" }); - // nodes.Add(new DagNode() { id = 2, title = "A" }); - // nodes.Add(new DagNode() { id = 3, title = "B" }); - // nodes.Add(new DagNode() { id = 4, title = "C" }); - // nodes.Add(new DagNode() { id = 5, title = "Out1" }); - // nodes.Add(new DagNode() { id = 6, title = "Out2" }); - - // edges.Add(new DagEdge() { fromId = 0, toId = 2 }); - // edges.Add(new DagEdge() { fromId = 1, toId = 2 }); - // edges.Add(new DagEdge() { fromId = 2, toId = 3 }); - // edges.Add(new DagEdge() { fromId = 2, toId = 4 }); - // edges.Add(new DagEdge() { fromId = 3, toId = 5 }); - // edges.Add(new DagEdge() { fromId = 4, toId = 6 }); - // } - void OnGUI() { HandleInput(); @@ -132,7 +103,8 @@ namespace NanoBrain { foreach (DagEdge e in edges) { DagNode from = GetNodeById(e.fromId); DagNode to = GetNodeById(e.toId); - if (from == null || to == null) continue; + if (from == null || to == null) + continue; DrawEdgeCircleNodes(from, to); } @@ -141,13 +113,6 @@ namespace NanoBrain { DrawNucleus(n); GUI.matrix = oldMatrix; - - // Footer toolbar - GUILayout.FlexibleSpace(); - EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); - if (GUILayout.Button("Fit", EditorStyles.toolbarButton)) FitToView(); - if (GUILayout.Button("Layout LR", EditorStyles.toolbarButton)) ComputeLeftToRightLayout(); - EditorGUILayout.EndHorizontal(); } void HandleInput() { @@ -171,22 +136,6 @@ namespace NanoBrain { } DagNode GetNodeById(int id) => nodes.FirstOrDefault(x => x.id == id); - List GetIncomingEdges(DagNode node) { - List incoming = new(); - foreach (DagEdge e in edges) { - if (e.toId == node.id) - incoming.Add(e); - } - return incoming; - } - List GetOutgoingEdges(DagNode node) { - List outgoing = new(); - foreach (DagEdge e in edges) { - if (e.fromId == node.id) - outgoing.Add(e); - } - return outgoing; - } void DrawNucleus(DagNode n) { Vector3 position = n.position; @@ -194,11 +143,6 @@ namespace NanoBrain { Handles.color = Color.white * 0.9f; Handles.DrawSolidDisc(n.position, Vector3.forward, n.radius); - if (GetIncomingEdges(n).Count == 0) - DrawArrowHead(n.position - new Vector2(n.radius + 10, 0), n.position - new Vector2(n.radius + 5, 0), 10f / zoom, 12f / zoom, Color.white); - if (GetOutgoingEdges(n).Count == 0) - DrawArrowHead(n.position + new Vector2(n.radius + 10, 0), n.position + new Vector2(n.radius + 15, 0), 10f / zoom, 12f / zoom, Color.white); - Handles.color = Color.white; GUIStyle style = new(EditorStyles.label) { alignment = TextAnchor.UpperCenter, @@ -216,109 +160,114 @@ namespace NanoBrain { Handles.color = Color.white * 0.9f; Handles.DrawLine(from.position, to.position); - - // Vector2 dir = (b - a).normalized; - // Vector2 start = a + dir * from.radius; - // Vector2 end = b - dir * to.radius; - - //DrawArrowHead(end - dir * 2f, end, 10f / zoom, 12f / zoom, Color.white); - } - void DrawArrowHead(Vector2 from, Vector2 to, float headWidth, float headLength, Color color) { - Vector2 dir = (to - from).normalized; - if (dir == Vector2.zero) return; - Vector2 right = new Vector2(-dir.y, dir.x); - - Vector3 p1 = to; - Vector3 p2 = to - dir * headLength + right * headWidth * 0.5f; - Vector3 p3 = to - dir * headLength - right * headWidth * 0.5f; - - Handles.color = color; - Handles.DrawAAConvexPolygon(p1, p2, p3); - } - - // Left-to-right layered layout (sources on the left, sinks on the right) - void ComputeLeftToRightLayout() { + // Right-to-left layered layout (sources on the right, sinks on the left) + void ComputeLayout() { // build adjacency and indegree - var adj = nodes.ToDictionary(n => n.id, n => new List()); - var indeg = nodes.ToDictionary(n => n.id, n => 0); - foreach (var e in edges) { - if (!adj.ContainsKey(e.fromId) || !adj.ContainsKey(e.toId)) continue; - adj[e.fromId].Add(e.toId); - indeg[e.toId]++; + Dictionary> adjacency = nodes.ToDictionary(n => n.id, n => new List()); + Dictionary indegree = nodes.ToDictionary(n => n.id, n => 0); + foreach (DagEdge edge in edges) { + if (!adjacency.ContainsKey(edge.fromId) || !adjacency.ContainsKey(edge.toId)) + continue; + adjacency[edge.fromId].Add(edge.toId); + indegree[edge.toId]++; + } + Dictionary outdegree = nodes.ToDictionary(node => node.id, n => 0); + foreach (DagEdge edge in edges) { + if (!adjacency.ContainsKey(edge.fromId) || !adjacency.ContainsKey(edge.toId)) + continue; + adjacency[edge.fromId].Add(edge.toId); + outdegree[edge.fromId]++; } // Kahn's algorithm to compute topological layers (horizontal layers) - Dictionary layer = new(); - Queue q = new(indeg.Where(kv => kv.Value == 0).Select(kv => kv.Key)); - foreach (var id in q) layer[id] = 0; + // build parent list (reverse adjacency) and parentIndegree = number of children each parent has + Dictionary> parents = nodes.ToDictionary(n => n.id, _ => new List()); + Dictionary childCount = nodes.ToDictionary(n => n.id, _ => 0); + foreach (DagEdge edge in edges) { + if (!adjacency.ContainsKey(edge.fromId) || !adjacency.ContainsKey(edge.toId)) continue; + adjacency[edge.fromId].Add(edge.toId); + parents[edge.toId].Add(edge.fromId); // parent of 'to' is 'from' + childCount[edge.fromId]++; // outdegree + } - while (q.Count > 0) { - int u = q.Dequeue(); + Dictionary layer = new(); + Queue queue = new(outdegree.Where(kv => kv.Value == 0).Select(kv => kv.Key)); + foreach (int id in queue) + layer[id] = 0; + + // process parents (reverse traversal) + while (queue.Count > 0) { + int u = queue.Dequeue(); int l = layer[u]; - foreach (var v in adj[u]) { - // prefer placing v at least one layer after u - if (!layer.ContainsKey(v) || layer[v] < l + 1) layer[v] = l + 1; - indeg[v]--; - if (indeg[v] == 0) q.Enqueue(v); + foreach (int p in parents[u]) { + if (!layer.ContainsKey(p) || layer[p] < l + 1) + layer[p] = l + 1; + childCount[p]--; // decrement remaining unprocessed children + if (childCount[p] == 0) + queue.Enqueue(p); } } + // Any unreachable nodes -> assign next layers int maxLayer = layer.Count > 0 ? layer.Values.Max() : 0; - foreach (var n in nodes) { - if (!layer.ContainsKey(n.id)) { + foreach (DagNode node in nodes) { + if (!layer.ContainsKey(node.id)) { maxLayer++; - layer[n.id] = maxLayer; + layer[node.id] = maxLayer; } } // Group nodes by layer (left to right) - var layers = layer.GroupBy(kv => kv.Value).OrderBy(g => g.Key).Select(g => g.Select(x => x.Key).ToList()).ToList(); + List> layers = layer.GroupBy(kv => kv.Value).OrderBy(g => g.Key).Select(g => g.Select(x => x.Key).ToList()).ToList(); - // Layout parameters (horizontal spacing drives left->right) - float hSpacing = 150f; + // Same code without using Linq + // Build layers dictionary: layerIndex -> List nodeIds + // Dictionary> layersDict = new(); + // foreach (KeyValuePair kv in layer) { + // int nodeId = kv.Key; + // int layerIndex = kv.Value; + // if (!layersDict.TryGetValue(layerIndex, out List list)) { + // list = new List(); + // layersDict[layerIndex] = list; + // } + // list.Add(nodeId); + // } + + // // Determine sorted layer indices + // List layerIndices = new(layersDict.Keys); + // layerIndices.Sort(); // ascending order + + // // Build final List> in sorted order + // List> layers = new(); + // foreach (int idx in layerIndices) { + // layers.Add(layersDict[idx]); + // } + + float hSpacing = 100f; float vSpacing = 100f; // Place nodes: x increases with layer index, y spaced within layer - for (int li = 0; li < layers.Count; li++) { - var lst = layers[li]; - float totalHeight = (lst.Count - 1) * vSpacing; - for (int i = 0; i < lst.Count; i++) { - int id = lst[i]; - var n = GetNodeById(id); - if (n == null) continue; - float x = hSpacing + li * hSpacing; + for (int layerIx = 0; layerIx < layers.Count; layerIx++) { + List nodeList = layers[layerIx]; + float totalHeight = (nodeList.Count - 1) * vSpacing; + for (int i = 0; i < nodeList.Count; i++) { + int index = nodeList[i]; + DagNode node = GetNodeById(index); + if (node == null) + continue; + float x = hSpacing + layerIx * hSpacing; float y = 400 - totalHeight / 2f + i * vSpacing; // Debug.Log($"({li}, {i}) -> {x}, {y}"); - n.position = new Vector2(x, y); + node.position = new Vector2(x, y); } } Repaint(); } - void FitToView() { - if (nodes.Count == 0) return; - // compute bounds including radii - Rect bounds = new Rect(nodes[0].position - Vector2.one * nodes[0].radius, Vector2.one * nodes[0].radius * 2f); - foreach (var n in nodes) - bounds = RectUnion(bounds, new Rect(n.position - Vector2.one * n.radius, Vector2.one * n.radius * 2f)); - - // center graph at origin (0,0) then set pan so it appears centered in window - Vector2 graphCenter = bounds.center; - // move nodes so center is at origin - for (int i = 0; i < nodes.Count; i++) - nodes[i].position -= graphCenter; - - // reset pan/zoom so centered - pan = Vector2.zero; - zoom = 1.0f; - Repaint(); - } - - static Rect RectUnion(Rect a, Rect b) { float xMin = Mathf.Min(a.xMin, b.xMin); float xMax = Mathf.Max(a.xMax, b.xMax); @@ -327,21 +276,6 @@ namespace NanoBrain { return Rect.MinMaxRect(xMin, yMin, xMax, yMax); } - Vector2 ScreenToGraph_old(Vector2 screenPos) { - Vector2 origin = new Vector2(position.width / 2, position.height / 2); - // invert the GUI.matrix transform (approx for current simple transforms) - return (screenPos - (origin + pan)) / zoom + origin * (1 - 1 / zoom); - } - Vector2 ScreenToGraph(Vector2 screenPos) { - Vector2 windowCenter = new Vector2(position.width / 2f, position.height / 2f); - Rect bounds = GetGraphBounds(); - Vector2 graphCenter = bounds.center; - Vector2 autoPan = -graphCenter; - // inverse of: screen -> translate by -(windowCenter+autoPan+pan), scale by 1/zoom, translate by windowCenter - return (screenPos - (windowCenter + autoPan + pan)) / zoom + windowCenter; - } - - Rect GetGraphBounds() { if (nodes == null || nodes.Count == 0) return new Rect(Vector2.zero, Vector2.one); Rect bounds = new( @@ -352,18 +286,6 @@ namespace NanoBrain { new Rect(n.position - Vector2.one * n.radius, 2f * n.radius * Vector2.one)); return bounds; } - - - - int HitTestNode(Vector2 graphPos) { - // returns node id under point or -1 - for (int i = nodes.Count - 1; i >= 0; i--) { - var n = nodes[i]; - if ((graphPos - n.position).sqrMagnitude <= n.radius * n.radius) return n.id; - } - return -1; - } - } } \ No newline at end of file diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterInspector.cs index 60ff610..c93e228 100644 --- a/Editor/ClusterInspector.cs +++ b/Editor/ClusterInspector.cs @@ -33,29 +33,31 @@ namespace NanoBrain { VisualElement mainContainer = new() { style = { - flexDirection = FlexDirection.Row + flexDirection = FlexDirection.Row, } }; - GraphEditor graph = new(cluster); - graph.style.flexGrow = 1; + GraphEditor graphContainer = new(cluster); + graphContainer.style.flexShrink = 0; + graphContainer.style.width = 300; + graphContainer.style.overflow = Overflow.Hidden; VisualElement inspectorContainer = new() { name = "inspector", style = { - alignSelf = Align.Stretch, minHeight = 450, width = 300, - flexGrow = 0 + flexGrow = 0, + flexDirection = FlexDirection.Row, } }; - mainContainer.Add(graph); + mainContainer.Add(graphContainer); mainContainer.Add(inspectorContainer); root.Add(mainContainer); - graph.SetGraph(gameObject, output, inspectorContainer); + graphContainer.SetGraph(gameObject, output, inspectorContainer); - return graph; + return graphContainer; } public class GraphEditor : GraphView { @@ -106,17 +108,7 @@ namespace NanoBrain { this.prefabAsset = CreateInstance(); //Debug.LogError("Cluster Prefab is not found on disk"); } - DrawInspector(inspectorContainer); - } - - #region Inspector - - private VisualElement inspectorIMGUIContainer; - private bool showSynapses = true; - private bool showActivation = true; - protected bool breakOnWake = false; - protected bool trace = false; - void DrawInspector(VisualElement inspectorContainer) { + // DrawInspector(inspectorContainer); if (inspectorContainer == null) return; @@ -131,6 +123,14 @@ namespace NanoBrain { inspectorContainer.Add(inspectorIMGUIContainer); } + #region Inspector + + private VisualElement inspectorIMGUIContainer; + private bool showSynapses = true; + private bool showActivation = true; + protected bool breakOnWake = false; + protected bool trace = false; + void InspectorHandler(SerializedObject serializedObject) { bool anythingChanged = false; @@ -150,13 +150,34 @@ namespace NanoBrain { fontStyle = FontStyle.Bold }; - GUILayout.Label(this.currentNucleus.GetType().ToString(), headerStyle); - string newName = EditorGUILayout.TextField(this.currentNucleus.name, boldTextFieldStyle); - if (newName != this.currentNucleus.name) { - this.currentNucleus.name = newName; - this.prefab.RefreshOutputs(); - outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); - anythingChanged = true; + string nucleusType = this.currentNucleus.GetType().Name; + // if (this.currentNucleus.parent != null) { + // string clusterName = this.currentNucleus.parent.name; + // GUILayout.Label(clusterName + ": " + nucleusType, headerStyle); + // } + // else + GUILayout.Label(nucleusType, headerStyle); + + + if (this.currentNucleus.parent is Cluster parentCluster) { + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button(this.currentNucleus.parent.name)) + EditCluster(parentCluster); + EditorGUI.BeginDisabledGroup(true); + EditorGUILayout.TextField(this.currentNucleus.name, boldTextFieldStyle); + EditorGUI.EndDisabledGroup(); + if (GUILayout.Button("Reimport")) + ReimportCluster(parentCluster); + EditorGUILayout.EndHorizontal(); + } + else { + string newName = EditorGUILayout.TextField(this.currentNucleus.name, boldTextFieldStyle); + if (newName != this.currentNucleus.name) { + this.currentNucleus.name = newName; + this.prefab.RefreshOutputs(); + outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); + anythingChanged = true; + } } if (Application.isPlaying) { @@ -174,7 +195,7 @@ namespace NanoBrain { memory.staticMemory = EditorGUILayout.Toggle("Static Memory", memory.staticMemory); } - if (this.currentNucleus is Cluster cluster) { + if (this.currentNucleus is Cluster cluster) { EditorGUILayout.BeginHorizontal(); if (cluster.siblingClusters != null && cluster.siblingClusters.Length > 1) EditorGUILayout.IntField("Array size", cluster.siblingClusters.Count()); @@ -195,115 +216,113 @@ namespace NanoBrain { // Synapses - //if (this.currentNucleus is not Receptor) { //} && this.currentNucleus is not ClusterReceptor) { - showSynapses = EditorGUILayout.BeginFoldoutHeaderGroup(showSynapses, "Synapses"); - if (showSynapses) { - if (this.currentNucleus is Neuron neuron2) { - Neuron.CombinatorType newCombinator = (Neuron.CombinatorType)EditorGUILayout.EnumPopup("Combinator", neuron2.combinator); - anythingChanged |= newCombinator != neuron2.combinator; - neuron2.combinator = newCombinator; - } + showSynapses = EditorGUILayout.BeginFoldoutHeaderGroup(showSynapses, "Synapses"); + if (showSynapses) { + if (this.currentNucleus is Neuron neuron2) { + Neuron.CombinatorType newCombinator = (Neuron.CombinatorType)EditorGUILayout.EnumPopup("Combinator", neuron2.combinator); + anythingChanged |= newCombinator != neuron2.combinator; + neuron2.combinator = newCombinator; + } - EditorGUIUtility.wideMode = true; - EditorGUIUtility.labelWidth = 100; - Vector3 newBias = EditorGUILayout.Vector3Field("Bias", this.currentNucleus.bias); - anythingChanged |= newBias != this.currentNucleus.bias; - this.currentNucleus.bias = newBias; + EditorGUIUtility.wideMode = true; + EditorGUIUtility.labelWidth = 100; + Vector3 newBias = EditorGUILayout.Vector3Field("Bias", this.currentNucleus.bias); + anythingChanged |= newBias != this.currentNucleus.bias; + this.currentNucleus.bias = newBias; - Nucleus[] array = null; - int elementIx = -1; - if (this.currentNucleus.synapses.Count > 0) { - Synapse[] synapses = this.currentNucleus.synapses.ToArray(); - foreach (Synapse synapse in synapses) { - if (synapse.neuron == null) - continue; + Nucleus[] array = null; + int elementIx = -1; + if (this.currentNucleus.synapses.Count > 0) { + Synapse[] synapses = this.currentNucleus.synapses.ToArray(); + foreach (Synapse synapse in synapses) { + if (synapse.neuron == null) + continue; - if (array != null) { - if (synapse.neuron.parent is Cluster iCluster && elementIx > 0) { - int thisElementIx = Cluster.GetNucleusIndex(iCluster.clusterNuclei, synapse.neuron); - if (thisElementIx == elementIx) - continue; - else - elementIx = thisElementIx; - } - // if (array.Contains(synapse.nucleus)) - // continue; - else if (array.Contains(synapse.neuron.parent)) + if (array != null) { + if (synapse.neuron.parent is Cluster iCluster && elementIx > 0) { + int thisElementIx = Cluster.GetNucleusIndex(iCluster.clusterNuclei, synapse.neuron); + if (thisElementIx == elementIx) continue; - } - else { - // if (synapse.neuron.parent is IReceptor iReceptor) { - // array = iReceptor.nucleiArray; - // if (iReceptor is Cluster iCluster) - // elementIx = Cluster.GetNucleusIndex(iCluster.clusterNuclei, synapse.neuron); - // } - // else if (synapse.nucleus is Receptor receptor2) // && receptor2.array != null && receptor2.array.nuclei.Length > 1) - // array = receptor2.nucleiArray; - } - - EditorGUILayout.Space(); - - if (Application.isPlaying) { - if (synapse.neuron is Neuron synapseNeuron) { - Vector3 value = synapseNeuron.outputValue * synapse.weight; - GUIContent synapseValueLabel = new(synapse.neuron.name, synapseNeuron.outputValue.ToString()); - EditorGUILayout.FloatField(synapseValueLabel, synapseNeuron.outputMagnitude); - } - } - else { - EditorGUILayout.BeginHorizontal(); - - if (synapse.neuron.parent != null && synapse.neuron.parent != this.currentNucleus) { - // If it is a cluster - GUIStyle labelStyle = new(GUI.skin.label); - float labelWidth = 200; - if (synapse.neuron.clusterPrefab != null) { - labelWidth = labelStyle.CalcSize(new GUIContent($"{synapse.neuron.parent.baseName}.")).x; - GUILayout.Label($"{synapse.neuron.parent.baseName}", GUILayout.Width(labelWidth)); - } - string[] options = synapse.neuron.parent.clusterNuclei.Select(n => n.name).ToArray(); - int selectedIndex = System.Array.IndexOf(options, synapse.neuron.name); - int newIndex = EditorGUILayout.Popup(selectedIndex, options); - if (newIndex != selectedIndex && synapse.neuron.parent.clusterNuclei[newIndex] is Neuron newNeuron) - ChangeSynapse(synapse, newNeuron); - } else - GUILayout.Label(synapse.neuron.name); - - bool disconnecting = GUILayout.Button("Disconnect", GUILayout.Width(80)); - if (disconnecting && synapse.neuron is Neuron synapseNeuron) { - synapseNeuron.RemoveReceiver(this.currentNucleus); - this.prefab.GarbageCollection(); - anythingChanged = true; - } - EditorGUILayout.EndHorizontal(); - + elementIx = thisElementIx; } + // if (array.Contains(synapse.nucleus)) + // continue; + else if (array.Contains(synapse.neuron.parent)) + continue; + } + else { + // if (synapse.neuron.parent is IReceptor iReceptor) { + // array = iReceptor.nucleiArray; + // if (iReceptor is Cluster iCluster) + // elementIx = Cluster.GetNucleusIndex(iCluster.clusterNuclei, synapse.neuron); + // } + // else if (synapse.nucleus is Receptor receptor2) // && receptor2.array != null && receptor2.array.nuclei.Length > 1) + // array = receptor2.nucleiArray; + } - EditorGUI.indentLevel++; - float newWeight = EditorGUILayout.FloatField("Weight", synapse.weight); - if (newWeight != synapse.weight) { - // if (synapse.neuron.parent is IReceptor receptor) { - // Nucleus[] receptorArray = receptor.nucleiArray; - // foreach (Synapse s in this.currentNucleus.synapses) { - // if (s.neuron.parent is IReceptor r && r.nucleiArray == receptorArray) - // s.weight = newWeight; - // } - // } - // else - synapse.weight = newWeight; + EditorGUILayout.Space(); + + if (Application.isPlaying) { + if (synapse.neuron is Neuron synapseNeuron) { + Vector3 value = synapseNeuron.outputValue * synapse.weight; + GUIContent synapseValueLabel = new(synapse.neuron.name, synapseNeuron.outputValue.ToString()); + EditorGUILayout.FloatField(synapseValueLabel, synapseNeuron.outputMagnitude); + } + } + else { + EditorGUILayout.BeginHorizontal(); + + if (synapse.neuron.parent != null && synapse.neuron.parent != this.currentNucleus) { + // If it is a cluster + GUIStyle labelStyle = new(GUI.skin.label); + float labelWidth = 200; + if (synapse.neuron.clusterPrefab != null) { + labelWidth = labelStyle.CalcSize(new GUIContent($"{synapse.neuron.parent.baseName}.")).x; + GUILayout.Label($"{synapse.neuron.parent.baseName}", GUILayout.Width(labelWidth)); + } + string[] options = synapse.neuron.parent.clusterNuclei.Select(n => n.name).ToArray(); + int selectedIndex = System.Array.IndexOf(options, synapse.neuron.name); + int newIndex = EditorGUILayout.Popup(selectedIndex, options); + if (newIndex != selectedIndex && synapse.neuron.parent.clusterNuclei[newIndex] is Neuron newNeuron) + ChangeSynapse(synapse, newNeuron); + } + else + GUILayout.Label(synapse.neuron.name); + + bool disconnecting = GUILayout.Button("Disconnect", GUILayout.Width(80)); + if (disconnecting && synapse.neuron is Neuron synapseNeuron) { + synapseNeuron.RemoveReceiver(this.currentNucleus); + this.prefab.GarbageCollection(); anythingChanged = true; } - EditorGUI.indentLevel--; - } - } + EditorGUILayout.EndHorizontal(); - EditorGUILayout.Space(); - anythingChanged |= ConnectNucleus(this.prefab, this.currentNucleus); - anythingChanged |= AddSynapse(this.prefab, this.currentNucleus); + } + + EditorGUI.indentLevel++; + float newWeight = EditorGUILayout.FloatField("Weight", synapse.weight); + if (newWeight != synapse.weight) { + // if (synapse.neuron.parent is IReceptor receptor) { + // Nucleus[] receptorArray = receptor.nucleiArray; + // foreach (Synapse s in this.currentNucleus.synapses) { + // if (s.neuron.parent is IReceptor r && r.nucleiArray == receptorArray) + // s.weight = newWeight; + // } + // } + // else + synapse.weight = newWeight; + anythingChanged = true; + } + EditorGUI.indentLevel--; + } } - EditorGUILayout.EndFoldoutHeaderGroup(); - //} + + EditorGUILayout.Space(); + anythingChanged |= ConnectNucleus(this.prefab, this.currentNucleus); + anythingChanged |= AddSynapse(this.prefab, this.currentNucleus); + } + EditorGUILayout.EndFoldoutHeaderGroup(); // Activation @@ -314,12 +333,12 @@ namespace NanoBrain { if (this.currentNucleus is Neuron neuron) { if (this.currentNucleus is not MemoryCell) { EditorGUILayout.BeginHorizontal(); - EditorGUILayout.LabelField("Activation Curve", GUILayout.Width(150)); + EditorGUILayout.LabelField("Activation Curve", GUILayout.MinWidth(60)); if (neuron.curveMax > 0) - EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, 0, 1, neuron.curveMax)); + 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)); - Neuron.ActivationType newPreset = (Neuron.ActivationType)EditorGUILayout.EnumPopup(neuron.curvePreset, GUILayout.Width(100)); + 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.curvePreset, GUILayout.MinWidth(50)); anythingChanged |= newPreset != neuron.curvePreset; neuron.curvePreset = newPreset; EditorGUILayout.EndHorizontal(); @@ -361,27 +380,6 @@ namespace NanoBrain { } } - // void OnSceneGUI(SceneView sceneView) { - // if (this.gameObject != null) { - // // 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.color = Color.yellow; - // Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector); - // } - // // } - // } - // } - #region Synapses protected virtual void AddInput(Nucleus.Type selectedType, Nucleus nucleus) { @@ -473,7 +471,7 @@ namespace NanoBrain { // if (nucleus is IReceptor receptor) // receptor.AddArrayReceiver(this.currentNucleus); // else - if (nucleus is Neuron neuron) + if (nucleus is Neuron neuron) neuron.AddReceiver(this.currentNucleus); else if (nucleus is Cluster subCluster) subCluster.defaultOutput.AddReceiver(this.currentNucleus); @@ -559,9 +557,9 @@ namespace NanoBrain { // } // } // else { - // it is a neuron in a subcluster - synapseNeuron.RemoveReceiver(this.currentNucleus); - newNucleus.AddReceiver(this.currentNucleus); + // it is a neuron in a subcluster + synapseNeuron.RemoveReceiver(this.currentNucleus); + newNucleus.AddReceiver(this.currentNucleus); // } } else { @@ -588,1071 +586,5 @@ namespace NanoBrain { #endregion Inspector } } - /* - [CustomEditor(typeof(ClusterPrefab))] - public class ClusterInspector : Editor { - public override VisualElement CreateInspectorGUI() { - ClusterPrefab prefab = target as ClusterPrefab; - if (prefab != null) - prefab.EnsureInitialization(); - - serializedObject.Update(); - - VisualElement root = new(); - CreateInspector(root, prefab, prefab.output, null); - - serializedObject.ApplyModifiedProperties(); - return root; - } - - public static GraphView CreateInspector(VisualElement root, ClusterPrefab cluster, Nucleus output, GameObject gameObject) { - root.style.paddingLeft = 0; - root.style.paddingRight = 0; - root.style.paddingTop = 0; - root.style.paddingBottom = 0; - - root.styleSheets.Add(Resources.Load("GraphStyles")); - - VisualElement mainContainer = new() { - style = { - flexDirection = FlexDirection.Row - } - }; - GraphView graph = new(cluster); - graph.style.flexGrow = 1; - - VisualElement inspectorContainer = new VisualElement { - name = "inspector", - style = { - alignSelf = Align.Stretch, - minHeight = 450, - width = 300, - flexGrow = 0 - } - }; - - mainContainer.Add(graph); - mainContainer.Add(inspectorContainer); - root.Add(mainContainer); - - graph.SetGraph(gameObject, output, inspectorContainer); - - return graph; - } - - public class GraphView : VisualElement { - readonly ClusterPrefab prefab; - SerializedObject serializedBrain; - Nucleus currentNucleus; - GameObject gameObject; - private List layers = new(); - private readonly Dictionary neuroidPositions = new(); - private bool expandArray = false; - - ClusterPrefab prefabAsset; - readonly PopupField outputsField; - - public GraphView(ClusterPrefab prefab) { - this.prefab = prefab; - - name = "content"; - style.flexGrow = 1; - - IMGUIContainer graphContainer = new(OnIMGUI); - graphContainer.style.position = Position.Absolute; - graphContainer.style.left = 0; graphContainer.style.top = 0; - graphContainer.style.right = 0; graphContainer.style.bottom = 0; - graphContainer.pickingMode = PickingMode.Position; - graphContainer.focusable = true; - Add(graphContainer); - - VisualElement outputContainer = new() { - style = { - flexDirection = FlexDirection.Row, - alignItems = Align.Center, - } - }; - - List names = this.prefab.outputs.Select(output => output.name).ToList(); - if (names.Count > 0 && names.First() != null) { - outputsField = new(names, names.First()) { - style = { flexGrow = 1 } - }; - outputsField.RegisterValueChangedCallback(evt => OnOutputChanged(evt.newValue)); - outputContainer.Add(outputsField); - } - - Button addButton = new(() => OnAddClusterOutput()) { - text = "Add" - }; - outputContainer.Add(addButton); - - Add(outputContainer); - - // Subscribe when added to panel (editor UI ready) - RegisterCallback(evt => Subscribe()); - RegisterCallback(evt => Unsubscribe()); - } - - void OnOutputChanged(string outputName) { - if (this.currentNucleus.parent != null) - // Get nucleus in the parent instance - this.currentNucleus = this.currentNucleus.parent.GetNucleus(outputName); - else - // Get nucleus in the prefab - this.currentNucleus = this.prefab.GetNucleus(outputName); - } - - void OnAddClusterOutput() { - Nucleus newOutput = new Neuron(this.prefab, "New Output"); - this.prefab.RefreshOutputs(); - outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); - outputsField.value = newOutput.name; - - this.currentNucleus = newOutput; - } - - 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, Nucleus nucleus, VisualElement inspectorContainer) { - this.gameObject = gameObject; - //this.cluster = brain; - if (Application.isPlaying == false) - this.serializedBrain = new SerializedObject(this.prefab); - this.currentNucleus = nucleus; - Rebuild(inspectorContainer); - } - - void Rebuild(VisualElement inspectorContainer) { - BuildLayers(); - - if (this.currentNucleus == null) { - inspectorContainer.Clear(); - return; - } - - string path = AssetDatabase.GetAssetPath(this.prefab); // or known path - this.prefabAsset = AssetDatabase.LoadAssetAtPath(path); - if (this.prefabAsset == null) { - // create in memory save if it doesn't exist - this.prefabAsset = CreateInstance(); - //Debug.LogError("Cluster Prefab is not found on disk"); - } - DrawInspector(inspectorContainer); - } - - private void BuildLayers() { - // A temporary list to track what's been added to layers - this.layers = new(); - int layerIx = 0; - - Nucleus selectedNucleus = this.currentNucleus; - if (selectedNucleus == null) - return; - NeuroidLayer currentLayer = new() { ix = layerIx }; - - if (selectedNucleus is Neuron selectedNeuron && selectedNeuron.receivers != null) { - foreach (Nucleus receiver in selectedNeuron.receivers) { - Nucleus outputNeuroid = receiver; - if (outputNeuroid != null) { - AddToLayer(currentLayer, outputNeuroid); - // Debug.Log($"layer {layerIx} nucleus {outputNeuroid.name}"); - } - } - } - if (currentLayer.neuroids.Count > 0) { - this.layers.Add(currentLayer); - layerIx++; - currentLayer = new() { ix = layerIx }; - } - - AddToLayer(currentLayer, selectedNucleus); - this.layers.Add(currentLayer); - // Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}"); - - layerIx++; - currentLayer = new() { ix = layerIx }; - - if (selectedNucleus.synapses != null) { - foreach (Synapse synapse in selectedNucleus.synapses) { - Nucleus input = synapse.neuron; - AddToLayer(currentLayer, input); - // Debug.Log($"layer {layerIx} nucleus {input.name}"); - } - } - if (currentLayer.neuroids.Count > 0) { - this.layers.Add(currentLayer); - } - } - - private void AddToLayer(NeuroidLayer layer, Nucleus nucleus) { - if (nucleus == null) - return; - layer.neuroids.Add(nucleus); - //nucleus.layerIx = layer.ix; - // Store its position - Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1); - neuroidPositions[nucleus] = neuroidPosition; - - } - - public void OnIMGUI() { - if (currentNucleus == null) - return; - - if (Application.isPlaying == false) - serializedBrain.Update(); - - Handles.BeginGUI(); - DrawGraph(); - Handles.EndGUI(); - - } - - private void DrawGraph() { - float size = 20; - Vector3 position = new(150, 210, 0); - - DrawReceivers(this.currentNucleus, position, size); - DrawSynapses(this.currentNucleus, position, size); - - // Draw selected Nucleus - if (expandArray) { - if (this.currentNucleus is IReceptor receptor1) { - float maxValue = 0; - foreach (Nucleus nucleus in receptor1.nucleiArray) { - if (nucleus is Neuron neuron) { - float value = neuron.outputMagnitude; - if (value > maxValue) - maxValue = value; - } - } - - float spacing = 400f / receptor1.nucleiArray.Count(); - float margin = 10 + spacing / 2; - float xMin = 150 - size; - float xMax = 150 + size; - float yMin = 10 + margin - size / 2; - float yMax = 400 - margin + size; - Vector3[] verts = new Vector3[4] { - new(xMin, yMin, 0), - new(xMax, yMin, 0), - new(xMax, yMax, 0), - new(xMin, yMax, 0) - }; - Handles.color = Color.black; - Handles.DrawAAConvexPolygon(verts); - int row = 0; - foreach (Nucleus nucleus in receptor1.nucleiArray) { - Vector3 pos = new(150, margin + row * spacing, 0.0f); - Handles.color = Color.white; - // The selected nucleus highlight ring - Handles.DrawSolidDisc(pos, Vector3.forward, size + 2); - DrawNucleus(nucleus, pos, maxValue, size); - row++; - } - GUIStyle style = new(EditorStyles.label) { - alignment = TextAnchor.UpperCenter, - normal = { textColor = Color.white }, - fontStyle = FontStyle.Bold, - }; - Vector3 labelPos = new(150, yMax + size + 5, 0); - string receptorName = receptor1.GetName(); - int colonPos = receptorName.IndexOf(":"); - if (colonPos > 0) { - string baseName = receptorName[..colonPos]; - Handles.Label(labelPos, baseName, style); - } - else - Handles.Label(labelPos, receptorName, style); - } - else { - Handles.color = Color.white; - // The selected nucleus highlight ring - Handles.DrawSolidDisc(position, Vector3.forward, size + 2); - 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 { - Handles.color = Color.white; - // The selected nucleus highlight ring - Handles.DrawSolidDisc(position, Vector3.forward, size + 2); - 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); - } - } - - private void DrawReceivers(Nucleus nucleus, Vector3 parentPos, float size) { - List receivers; - if (nucleus is Neuron neuron) - receivers = neuron.receivers; - else if (nucleus is Cluster cluster) - receivers = cluster.CollectReceivers(); - else - return; - - int nodeCount = receivers.Count(); //neuron != null ? neuron.receivers.Count() : 1; - - // 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; - List drawnArrays = new(); - foreach (Nucleus receiver in receivers) { - if (receiver is Receptor receptor) { - if (drawnArrays.Contains(receptor.nucleiArray)) - continue; - drawnArrays.Add(receptor.nucleiArray); - } - - Nucleus receiverNucleus = receiver; - if (receiverNucleus == null) - continue; - - Vector3 pos = new(50, margin + row * spacing, 0.0f); - Handles.color = Color.white; - Handles.DrawLine(parentPos, pos); - - DrawNucleus(receiverNucleus, pos, maxValue, size); - row++; - } - } - - private void DrawSynapses(Nucleus nucleus, Vector3 parentPos, float size) { - int nodeCount = nucleus.synapses.Count; - - // Determine the maximum value in this layer - // This is used to 'scale' the output value colors of the nuclei - float maxValue = 0; - int neuronCount = 0; - List drawnArrays = new(); - foreach (Synapse synapse in nucleus.synapses) { - if (synapse.neuron == null) - continue; - - if (synapse.neuron is Receptor receptor) { - if (drawnArrays.Contains(receptor.nucleiArray)) - continue; - drawnArrays.Add(receptor.nucleiArray); - } - else if (synapse.neuron.parent is ClusterReceptor clusterReceptor) { - if (drawnArrays.Contains(clusterReceptor.nucleiArray)) - continue; - drawnArrays.Add(clusterReceptor.nucleiArray); - } - if (synapse.neuron is Neuron synapseNeuron) { - float value = synapseNeuron.outputMagnitude * synapse.weight; - // Debug.Log($"{synapse.nucleus.name}: {value} {length(synapse.nucleus.outputValue)} {synapse.weight}"); - if (value > maxValue) - maxValue = value; - } - neuronCount++; - } - - // Determine the spacing of the nuclei in the layer - float spacing = 400f / neuronCount; - float margin = 10 + spacing / 2; - - int row = 0; - drawnArrays = new(); - foreach (Synapse synapse in nucleus.synapses) { - if (synapse.neuron is null) - continue; - - if (synapse.neuron is Receptor neuron) { - if (drawnArrays.Contains(neuron.nucleiArray)) - continue; - drawnArrays.Add(neuron.nucleiArray); - } - else if (synapse.neuron.parent is ClusterReceptor clusterReceptor) { - if (drawnArrays.Contains(clusterReceptor.nucleiArray)) - continue; - drawnArrays.Add(clusterReceptor.nucleiArray); - } - Vector3 pos = new(250, margin + row * spacing, 0.0f); - 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 = 0; - if (synapse.neuron is Neuron synapseNeuron) - brightness = synapseNeuron.outputMagnitude * synapse.weight / maxValue; - color = new Color(brightness, brightness, brightness, 1f); - } - if (synapse.neuron.parent != null && synapse.neuron.parent != this.currentNucleus.parent) { - // the synapse nucleus is part of a subcluster - DrawNucleus(synapse.neuron.parent, pos, maxValue, size, color); - } - // else if (synapse.nucleus.cluster != null && synapse.nucleus.cluster != this.currentNucleus.cluster) { - // DrawNucleus(synapse.nucleus.parent, pos, maxValue, size, color); - // } - else { - DrawNucleus(synapse.neuron, pos, maxValue, size, color); - } - row++; - } - } - - private 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, maxValue, size, color); - } - - private void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size, Color color) { - 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 is IReceptor receptor1) { - if (expandArray) { - // Put array indices above elements - style.alignment = TextAnchor.LowerCenter; - Vector3 labelPos1 = position + Vector3.down * (size + 5); // below disc - int colonPos1 = nucleus.name.IndexOf(":"); - if (colonPos1 > 0) { - string extName = nucleus.name[(colonPos1 + 2)..]; - Handles.Label(labelPos1, extName, style); - } - } - else { - // draw the array size label - if (color.grayscale > 0.5f) - style.normal.textColor = Color.black; - else - style.normal.textColor = Color.white; - Handles.Label(labelPosition, receptor1.nucleiArray.Length.ToString(), style); - style.normal.textColor = Color.white; - } - } - - if (expandArray == false || nucleus is not IReceptor) { - // put name below nucleus - Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron - style.alignment = TextAnchor.UpperCenter; - - 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); - - } - - // Draw Cluster ring - if (nucleus is Cluster) { - Handles.color = Color.white; - Handles.DrawWireDisc(position, Vector3.forward, size + 5); - } - - // 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(); - HandleClicked(nucleus); - } - } - } - - private 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); - } - - private void HandleClicked(Nucleus nucleus) { - if (nucleus == this.currentNucleus) { - if (nucleus is Receptor || nucleus is ClusterReceptor) - expandArray = !expandArray; - else - expandArray = false; - } - // else if (nucleus is ReceptorInstance receptor) { - // this.currentNucleus = receptor.receptor; - // expandArray = false; - // BuildLayers(); - // } - else { - this.currentNucleus = nucleus; - expandArray = false; - BuildLayers(); - } - } - - private VisualElement inspectorIMGUIContainer; - private bool showSynapses = true; - private bool showActivation = true; - protected bool breakOnWake = false; - protected bool trace = false; - void DrawInspector(VisualElement inspectorContainer) { - if (inspectorContainer == null) - return; - - inspectorContainer.Clear(); - if (this.currentNucleus == null) - return; - - // create a SerializedObject wrapper so Unity inspector controls work (and Undo) - SerializedObject so = new(prefabAsset); - this.inspectorIMGUIContainer = new IMGUIContainer(() => InspectorHandler(so)); - - inspectorContainer.Add(inspectorIMGUIContainer); - } - - void InspectorHandler(SerializedObject serializedObject) { - bool anythingChanged = false; - - if (serializedObject == null || serializedObject.targetObject == null) - return; - - if (this.currentNucleus == null) - return; - - serializedObject.Update(); - - GUIStyle headerStyle = new(EditorStyles.boldLabel) { - alignment = TextAnchor.MiddleLeft, - margin = new RectOffset(10, 0, 4, 4) - }; - GUIStyle boldTextFieldStyle = new(EditorStyles.textField) { - fontStyle = FontStyle.Bold - }; - - GUILayout.Label(this.currentNucleus.GetType().ToString(), headerStyle); - string newName = EditorGUILayout.TextField(this.currentNucleus.name, boldTextFieldStyle); - if (newName != this.currentNucleus.name) { - this.currentNucleus.name = newName; - this.prefab.RefreshOutputs(); - outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); - anythingChanged = true; - } - - if (Application.isPlaying) { - if (currentNucleus is Neuron currentNeuron1) { - GUIContent nameLabel = new("Output", currentNeuron1.outputValue.ToString()); - EditorGUILayout.FloatField(nameLabel, currentNeuron1.outputMagnitude); - } - else - EditorGUILayout.LabelField(" "); - } - else - EditorGUILayout.LabelField(" "); - - if (this.currentNucleus is MemoryCell memory) { - memory.staticMemory = EditorGUILayout.Toggle("Static Memory", memory.staticMemory); - } - - if (this.currentNucleus is IReceptor receptor1) { - EditorGUILayout.BeginHorizontal(); - EditorGUILayout.IntField("Array size", receptor1.nucleiArray.Count()); - if (GUILayout.Button("Add")) { - Undo.RecordObject(prefabAsset, "Array add " + prefabAsset.name); - receptor1.AddReceptorElement(this.prefab); - anythingChanged = true; - } - if (GUILayout.Button("Del")) { - Undo.RecordObject(prefabAsset, "Array delete " + prefabAsset.name); - receptor1.RemoveReceptorElement(); - anythingChanged = true; - } - EditorGUILayout.EndHorizontal(); - } - - // Synapses - - if (this.currentNucleus is not Receptor && this.currentNucleus is not ClusterReceptor) { - showSynapses = EditorGUILayout.BeginFoldoutHeaderGroup(showSynapses, "Synapses"); - if (showSynapses) { - if (this.currentNucleus is Neuron neuron2) { - Neuron.CombinatorType newCombinator = (Neuron.CombinatorType)EditorGUILayout.EnumPopup("Combinator", neuron2.combinator); - anythingChanged |= newCombinator != neuron2.combinator; - neuron2.combinator = newCombinator; - } - - EditorGUIUtility.wideMode = true; - EditorGUIUtility.labelWidth = 100; - Vector3 newBias = EditorGUILayout.Vector3Field("Bias", this.currentNucleus.bias); - anythingChanged |= newBias != this.currentNucleus.bias; - this.currentNucleus.bias = newBias; - - Nucleus[] array = null; - int elementIx = -1; - if (this.currentNucleus.synapses.Count > 0) { - Synapse[] synapses = this.currentNucleus.synapses.ToArray(); - foreach (Synapse synapse in synapses) { - if (synapse.neuron == null) - continue; - - if (array != null) { - if (synapse.neuron.parent is Cluster iCluster && elementIx > 0) { - int thisElementIx = Cluster.GetNucleusIndex(iCluster.clusterNuclei, synapse.neuron); - if (thisElementIx == elementIx) - continue; - else - elementIx = thisElementIx; - } - // if (array.Contains(synapse.nucleus)) - // continue; - else if (array.Contains(synapse.neuron.parent)) - continue; - } - else { - if (synapse.neuron.parent is IReceptor iReceptor) { - array = iReceptor.nucleiArray; - if (iReceptor is Cluster iCluster) - elementIx = Cluster.GetNucleusIndex(iCluster.clusterNuclei, synapse.neuron); - } - // else if (synapse.nucleus is Receptor receptor2) // && receptor2.array != null && receptor2.array.nuclei.Length > 1) - // array = receptor2.nucleiArray; - } - - EditorGUILayout.Space(); - - if (Application.isPlaying) { - if (synapse.neuron is Neuron synapseNeuron) { - Vector3 value = synapseNeuron.outputValue * synapse.weight; - GUIContent synapseValueLabel = new(synapse.neuron.name, synapseNeuron.outputValue.ToString()); - EditorGUILayout.FloatField(synapseValueLabel, synapseNeuron.outputMagnitude); - } - } - else { - EditorGUILayout.BeginHorizontal(); - - if (synapse.neuron.parent != null && synapse.neuron.parent != this.currentNucleus) { - // If it is a cluster - GUIStyle labelStyle = new(GUI.skin.label); - float labelWidth = 200; - if (synapse.neuron.clusterPrefab != null) { - labelWidth = labelStyle.CalcSize(new GUIContent($"{synapse.neuron.parent.baseName}.")).x; - GUILayout.Label($"{synapse.neuron.parent.baseName}", GUILayout.Width(labelWidth)); - } - string[] options = synapse.neuron.parent.clusterNuclei.Select(n => n.name).ToArray(); - int selectedIndex = System.Array.IndexOf(options, synapse.neuron.name); - int newIndex = EditorGUILayout.Popup(selectedIndex, options); - if (newIndex != selectedIndex && synapse.neuron.parent.clusterNuclei[newIndex] is Neuron newNeuron) - ChangeSynapse(synapse, newNeuron); - } - else - GUILayout.Label(synapse.neuron.name); - - bool disconnecting = GUILayout.Button("Disconnect", GUILayout.Width(80)); - if (disconnecting && synapse.neuron is Neuron synapseNeuron) { - synapseNeuron.RemoveReceiver(this.currentNucleus); - this.prefab.GarbageCollection(); - anythingChanged = true; - } - EditorGUILayout.EndHorizontal(); - - } - - EditorGUI.indentLevel++; - float newWeight = EditorGUILayout.FloatField("Weight", synapse.weight); - if (newWeight != synapse.weight) { - if (synapse.neuron.parent is IReceptor receptor) { - Nucleus[] receptorArray = receptor.nucleiArray; - foreach (Synapse s in this.currentNucleus.synapses) { - if (s.neuron.parent is IReceptor r && r.nucleiArray == receptorArray) - s.weight = newWeight; - } - } - else - synapse.weight = newWeight; - anythingChanged = true; - } - EditorGUI.indentLevel--; - } - } - - EditorGUILayout.Space(); - anythingChanged |= ConnectNucleus(this.prefab, this.currentNucleus); - anythingChanged |= AddSynapse(this.prefab, this.currentNucleus); - } - EditorGUILayout.EndFoldoutHeaderGroup(); - } - - // Activation - - if (this.currentNucleus is not Cluster) { - EditorGUILayout.Space(); - showActivation = EditorGUILayout.BeginFoldoutHeaderGroup(showActivation, "Activation"); - if (showActivation) { - if (this.currentNucleus is Neuron neuron) { - if (this.currentNucleus is not MemoryCell) { - EditorGUILayout.BeginHorizontal(); - EditorGUILayout.LabelField("Activation Curve", GUILayout.Width(150)); - if (neuron.curveMax > 0) - EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, 0, 1, neuron.curveMax)); - else - EditorGUILayout.CurveField(neuron.curve, Color.cyan, new Rect(0, neuron.curveMax, 1, -neuron.curveMax)); - Neuron.CurvePresets newPreset = (Neuron.CurvePresets)EditorGUILayout.EnumPopup(neuron.curvePreset, GUILayout.Width(100)); - anythingChanged |= newPreset != neuron.curvePreset; - neuron.curvePreset = newPreset; - EditorGUILayout.EndHorizontal(); - } - if (neuron is Receptor receptor2) { - if (receptor2.nucleiArray == null || receptor2.nucleiArray.Count() == 0) - receptor2.array = new NucleusArray(neuron); - } - } - - EditorGUILayout.Space(); - } - EditorGUILayout.EndFoldoutHeaderGroup(); - } - - if (GUILayout.Button("Delete this neuron")) - DeleteNucleus(this.currentNucleus); - - if (this.currentNucleus is Cluster subCluster) { - if (GUILayout.Button("Edit Cluster")) - EditCluster(subCluster); - } - - EditorGUILayout.Space(); - breakOnWake = EditorGUILayout.Toggle("Break on wake", breakOnWake); - if (breakOnWake && this.currentNucleus is Neuron currentNeuron) { - if (currentNeuron.isSleeping == false) - Debug.Break(); - } - trace = EditorGUILayout.Toggle("Trace", trace); - this.currentNucleus.trace = trace; - - serializedObject.ApplyModifiedProperties(); - if (anythingChanged) { - EditorUtility.SetDirty(prefabAsset); - AssetDatabase.SaveAssets(); - } - } - - void OnSceneGUI(SceneView sceneView) { - if (this.gameObject != null) { - 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.color = Color.yellow; - Handles.DrawLine(this.gameObject.transform.position, this.gameObject.transform.position + worldVector); - } - } - } - } - - #region Synapses - - protected virtual void AddInput(Nucleus.Type selectedType, Nucleus nucleus) { - switch (selectedType) { - case Nucleus.Type.Neuron: - AddNeuronInput(nucleus); - break; - case Nucleus.Type.MemoryCell: - AddMemoryCellInput(nucleus); - break; - // case Nucleus.Type.Selector: - // AddSelectorInput(nucleus); - // break; - case Nucleus.Type.Cluster: - AddClusterInput(nucleus); - break; - // case Nucleus.Type.Pulsar: - // AddPulsarInput(nucleus); - // break; - case Nucleus.Type.Receptor: - AddReceptorInput(nucleus); - break; - // case Nucleus.Type.ReceptorArray: - // AddReceptorArrayInput(nucleus); - // break; - case Nucleus.Type.ClusterReceptor: - AddClusterReceptorInput(nucleus); - break; - default: - break; - } - } - - protected virtual void AddNeuronInput(Nucleus nucleus) { - Neuron newNeuroid = new(this.prefab, "New neuron"); - newNeuroid.AddReceiver(nucleus); - this.currentNucleus = newNeuroid; - BuildLayers(); - } - - protected virtual void AddMemoryCellInput(Nucleus nucleus) { - MemoryCell newMemory = new(this.prefab, "New memory cell"); - newMemory.AddReceiver(nucleus); - this.currentNucleus = newMemory; - BuildLayers(); - } - - protected virtual void AddClusterInput(Nucleus nucleus) { - ClusterPickerWindow.ShowPicker(brain => OnClusterPicked(nucleus, brain), "Select Cluster"); - } - private void OnClusterPicked(Nucleus nucleus, ClusterPrefab prefab) { - Cluster subclusterInstance = new(prefab, this.prefab); - subclusterInstance.defaultOutput.AddReceiver(nucleus); - } - - protected virtual void AddReceptorInput(Nucleus nucleus) { - Receptor newReceptor = new(this.prefab, "New Receptor"); - newReceptor.AddReceiver(nucleus); - this.currentNucleus = newReceptor; - BuildLayers(); - } - - protected virtual void AddClusterReceptorInput(Nucleus nucleus) { - ClusterPickerWindow.ShowPicker(prefab => OnClusterReceptorPicked(nucleus, prefab), "Select Cluster"); - } - private void OnClusterReceptorPicked(Nucleus nucleus, ClusterPrefab selectedPrefab) { - ClusterReceptor clusterInstance = new(selectedPrefab, this.prefab, "New " + selectedPrefab.name); - clusterInstance.defaultOutput.AddReceiver(nucleus); - this.currentNucleus = clusterInstance; - BuildLayers(); - } - - private void EditCluster(Cluster subCluster) { - // May be used with storedPrefab... - Selection.activeObject = subCluster.prefab; - EditorGUIUtility.PingObject(subCluster.prefab); - var editor = Editor.CreateEditor(subCluster.prefab); - } - - int selectedConnectNucleus = -1; - // Connect to another nucleus in the same cluster - protected virtual bool ConnectNucleus(ClusterPrefab cluster, Nucleus nucleusToConnect) { - if (cluster == null) - return false; - - IEnumerable synapseNuclei = this.currentNucleus.synapses - .Where(synapse => synapse.neuron != null) - .Select(synapse => synapse.neuron); - - IEnumerable nuclei = cluster.nuclei - .Except(synapseNuclei); - IEnumerable nucleiNames = nuclei - .Select(n => { - int idx = n.name.IndexOf(':'); - return idx < 0 ? n.name : n.name[..idx]; - }) - .Distinct(); - - string[] names = nucleiNames.ToArray(); - EditorGUILayout.BeginHorizontal(); - selectedConnectNucleus = EditorGUILayout.Popup(selectedConnectNucleus, names); - bool connecting = GUILayout.Button("Connect", GUILayout.Width(80)); - EditorGUILayout.EndHorizontal(); - if (connecting) { - Nucleus nucleus = nuclei.ElementAt(selectedConnectNucleus); - if (nucleus is IReceptor receptor) - receptor.AddArrayReceiver(this.currentNucleus); - else if (nucleus is Neuron neuron) - neuron.AddReceiver(this.currentNucleus); - else if (nucleus is Cluster subCluster) - subCluster.defaultOutput.AddReceiver(this.currentNucleus); - - } - return connecting; - } - - protected virtual void DeleteNucleus(Nucleus nucleus) { - if (nucleus == null) - return; - - if (nucleus is Neuron neuron) { - foreach (Nucleus receiver in neuron.receivers) { - if (receiver != null) { - this.currentNucleus = receiver; - break; - } - } - } - this.prefab.nuclei.Remove(nucleus); - - if (outputsField.value == nucleus.name) { - this.prefab.RefreshOutputs(); - outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); - outputsField.index = 0; - } - - Neuron.Delete(nucleus); - - this.currentNucleus = this.prefab.output; - BuildLayers(); - } - - Nucleus.Type selectedType = Nucleus.Type.None; - protected virtual bool AddSynapse(ClusterPrefab cluster, Nucleus nucleus) { - if (cluster == null) - return false; - - EditorGUILayout.BeginHorizontal(); - selectedType = (Nucleus.Type)EditorGUILayout.EnumPopup(selectedType); - bool connecting = GUILayout.Button("Add", GUILayout.Width(80)); - EditorGUILayout.EndHorizontal(); - - if (connecting) { - AddInput(selectedType, this.currentNucleus); - } - return connecting; - // if (selectedType == Nucleus.Type.None) - // return false; - - // AddInput(selectedType, this.currentNucleus); - // return true; - } - - protected virtual void ChangeSynapse(Synapse synapse, Neuron newNucleus) { - Neuron synapseNeuron = synapse.neuron as Neuron; - if (synapse.neuron.parent is Cluster subCluster && subCluster.prefab != this.prefab) { - if (synapse.neuron.parent is ClusterReceptor receptor) { - // the new nucleus is part of a (cluster) receptor, - // so we have to change all synapses to this nucleus array elements - int oldNucleusIx = Cluster.GetNucleusIndex(subCluster.clusterNuclei, synapse.neuron); - int newNucleusIx = Cluster.GetNucleusIndex(subCluster.clusterNuclei, newNucleus); - foreach (Nucleus element in receptor.nucleiArray) { - if (element is not ClusterReceptor clusterReceptor) - continue; - // Get the same neuron as the synapse.nucleus in a different element - // of the ClusterReceptor array - Nucleus oldElementNucleus = clusterReceptor.clusterNuclei[oldNucleusIx]; - if (oldElementNucleus is not Neuron oldElementNeuron) - continue; - // Get the same neuron as newNucleus in a different element - // of the ClusterReceptor array - Nucleus newElementNucleus = clusterReceptor.clusterNuclei[newNucleusIx]; - if (newElementNucleus is not Neuron newElementNeuron) - continue; - - oldElementNeuron.RemoveReceiver(this.currentNucleus); - newElementNeuron.AddReceiver(this.currentNucleus); - // Now find the synapse which pointed to the old Neuron - // Synapse synapseForUpdate = this.currentNucleus.GetSynapse(oldElementNeuron); - // synapseForUpdate.nucleus = newElementNeuron; - } - } - else { - // it is a neuron in a subcluster - synapseNeuron.RemoveReceiver(this.currentNucleus); - newNucleus.AddReceiver(this.currentNucleus); - } - } - else { - synapseNeuron.RemoveReceiver(this.currentNucleus); - newNucleus.AddReceiver(this.currentNucleus); - } - } - - protected virtual void DisconnectNucleus(Neuron nucleus) { - if (this.currentNucleus.clusterPrefab == null) - return; - string[] names = this.currentNucleus.synapses.Select(synapse => synapse.neuron.name).ToArray(); - int selectedIndex = -1; - selectedIndex = EditorGUILayout.Popup("Disconnect from", selectedIndex, names); - if (selectedIndex >= 0 && selectedIndex < this.currentNucleus.clusterPrefab.nuclei.Count) { - Synapse synapse = this.currentNucleus.synapses[selectedIndex]; - Neuron synapseNeuron = synapse.neuron as Neuron; - synapseNeuron.RemoveReceiver(this.currentNucleus); - } - } - - #endregion Synapses - } - - } - - public class NeuroidLayer { - public int ix = 0; - public List neuroids = new(); - } - */ } \ No newline at end of file diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index 8e55da5..00d6132 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -466,15 +466,14 @@ namespace NanoBrain { style.alignment = TextAnchor.UpperCenter; if (nucleus.parent != null && nucleus.parent is Cluster parentCluster1) { + // This neuron is part of another cluster parentCluster1.name ??= ""; string baseName = ""; - if (parentCluster1 != currentNucleus.parent) { int colonPos = parentCluster1.name.IndexOf(":"); if (colonPos > 0 && colonPos < parentCluster1.name.Length - 2) baseName = parentCluster1.name[..colonPos] + "."; else baseName = parentCluster1.name + "."; - } // if (colonPos > 0 && colonPos < parentCluster1.name.Length - 2) { // // if it is an array, we should not show the :0 of the first element // //baseName = baseName[..colonPos]; @@ -549,13 +548,13 @@ namespace NanoBrain { else expandArray = false; } - else if (nucleus.parent != this.currentNucleus.parent) { - // We go to a different cluster - // select the cluster, not the neuron in the cluster - this.currentNucleus = nucleus.parent; - expandArray = false; - BuildLayers(); - } + // else if (nucleus.parent != this.currentNucleus.parent) { + // // We go to a different cluster + // // select the cluster, not the neuron in the cluster + // this.currentNucleus = nucleus.parent; + // expandArray = false; + // BuildLayers(); + // } else { this.currentNucleus = nucleus; expandArray = false; diff --git a/Editor/DAGWindow.cs b/Editor/DAGWindow.cs deleted file mode 100644 index 1cb590e..0000000 --- a/Editor/DAGWindow.cs +++ /dev/null @@ -1,356 +0,0 @@ - -using UnityEngine; -using UnityEditor; -using System.Collections.Generic; -using System.Linq; - -namespace NanoBrain { - - // Simple DAG data model - // [System.Serializable] - // public class DagNode - // { - // public int id; - // public string title; - // public Vector2 position; - // public float radius = 36f; // circle radius - // } - - // [System.Serializable] - // public class DagEdge - // { - // public int fromId; - // public int toId; - // } - - public class DAGEditorWindow : EditorWindow { - List nodes = new List(); - List edges = new List(); - - Vector2 pan = Vector2.zero; - float zoom = 1.0f; - const float minZoom = 0.5f; - const float maxZoom = 2.0f; - - GUIStyle labelStyle; - int selectedNodeId = -1; - - Vector2 dragStart; - bool draggingNode = false; - int draggingNodeId = -1; - - [MenuItem("Window/DAG Viewer (LR, Circles)")] - public static void ShowWindow() { - var w = GetWindow("DAG Viewer (LR)"); - w.minSize = new Vector2(500, 300); - } - - void OnEnable() { - labelStyle = new GUIStyle(EditorStyles.label); - labelStyle.alignment = TextAnchor.MiddleCenter; - labelStyle.normal.textColor = Color.white; - labelStyle.fontStyle = FontStyle.Bold; - - if (nodes.Count == 0) - CreateSampleGraph(); - - ComputeLeftToRightLayout(); - } - - void CreateSampleGraph() { - nodes.Clear(); - edges.Clear(); - - nodes.Add(new DagNode() { id = 0, title = "In1" }); - nodes.Add(new DagNode() { id = 1, title = "In2" }); - nodes.Add(new DagNode() { id = 2, title = "A" }); - nodes.Add(new DagNode() { id = 3, title = "B" }); - nodes.Add(new DagNode() { id = 4, title = "C" }); - nodes.Add(new DagNode() { id = 5, title = "Out1" }); - nodes.Add(new DagNode() { id = 6, title = "Out2" }); - - edges.Add(new DagEdge() { fromId = 0, toId = 2 }); - edges.Add(new DagEdge() { fromId = 1, toId = 2 }); - edges.Add(new DagEdge() { fromId = 2, toId = 3 }); - edges.Add(new DagEdge() { fromId = 2, toId = 4 }); - edges.Add(new DagEdge() { fromId = 3, toId = 5 }); - edges.Add(new DagEdge() { fromId = 4, toId = 6 }); - } - - void OnGUI() { - HandleInput(); - - Rect rect = new Rect(0, 0, position.width, position.height); - EditorGUI.DrawRect(rect, new Color(0.11f, 0.11f, 0.11f)); - - Matrix4x4 oldMatrix = GUI.matrix; - Vector2 origin = new Vector2(position.width / 2, position.height / 2); - GUI.matrix = Matrix4x4.TRS(origin + pan, Quaternion.identity, Vector3.one * zoom) * - Matrix4x4.TRS(-origin, Quaternion.identity, Vector3.one); - - // Draw edges first - foreach (var e in edges) { - var from = GetNodeById(e.fromId); - var to = GetNodeById(e.toId); - if (from == null || to == null) continue; - DrawEdgeCircleNodes(from, to); - } - - // Draw nodes (circles) - foreach (var n in nodes) { - DrawNodeCircle(n); - } - - GUI.matrix = oldMatrix; - - // Footer toolbar - GUILayout.FlexibleSpace(); - EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); - if (GUILayout.Button("Fit", EditorStyles.toolbarButton)) FitToView(); - if (GUILayout.Button("Layout LR", EditorStyles.toolbarButton)) ComputeLeftToRightLayout(); - if (GUILayout.Button("Add Node", EditorStyles.toolbarButton)) { - AddNode("N" + nodes.Count); - ComputeLeftToRightLayout(); - } - if (GUILayout.Button("Add Edge (selected->new)", EditorStyles.toolbarButton)) { - if (selectedNodeId != -1) { - var newNode = AddNode("N" + nodes.Count); - edges.Add(new DagEdge() { fromId = selectedNodeId, toId = newNode.id }); - ComputeLeftToRightLayout(); - } - } - EditorGUILayout.EndHorizontal(); - } - - void HandleInput() { - Event e = Event.current; - - // Zoom with scroll - if (e.type == EventType.ScrollWheel) { - float oldZoom = zoom; - float delta = -e.delta.y * 0.01f; - zoom = Mathf.Clamp(zoom + delta, minZoom, maxZoom); - Vector2 mouse = e.mousePosition; - pan += (mouse - new Vector2(position.width / 2, position.height / 2)) * (1 - zoom / oldZoom); - e.Use(); - } - - // Pan with middle or right+ctrl drag - if (e.type == EventType.MouseDrag && (e.button == 2 || (e.button == 1 && e.control))) { - pan += e.delta; - e.Use(); - } - - // Node dragging & selection (convert mouse to graph space) - Vector2 graphMouse = ScreenToGraph(e.mousePosition); - if (e.type == EventType.MouseDown && e.button == 0) { - int hit = HitTestNode(graphMouse); - if (hit != -1) { - selectedNodeId = hit; - draggingNode = true; - draggingNodeId = hit; - dragStart = graphMouse; - e.Use(); - } - else { - selectedNodeId = -1; - } - } - - if (draggingNode && draggingNodeId != -1) { - if (e.type == EventType.MouseDrag && e.button == 0) { - Vector2 graphDelta = e.delta / zoom; - var n = GetNodeById(draggingNodeId); - if (n != null) { - n.position += graphDelta; - Repaint(); - e.Use(); - } - } - if (e.type == EventType.MouseUp && e.button == 0) { - draggingNode = false; - draggingNodeId = -1; - e.Use(); - } - } - } - - DagNode AddNode(string title) { - int nextId = nodes.Count > 0 ? nodes.Max(n => n.id) + 1 : 0; - var n = new DagNode() { id = nextId, title = title, position = Vector2.zero }; - nodes.Add(n); - return n; - } - - DagNode GetNodeById(int id) => nodes.FirstOrDefault(x => x.id == id); - - void DrawNodeCircle(DagNode n) { - Vector2 center = n.position; - float r = n.radius; - Rect nodeRect = new Rect(center.x - r, center.y - r, r * 2, r * 2); - - // circle background - Color bg = (n.id == selectedNodeId) ? new Color(0.15f, 0.5f, 0.9f) : new Color(0.2f, 0.2f, 0.2f); - EditorGUI.DrawRect(nodeRect, bg); - - // anti-aliased circle outline - Handles.color = Color.white * 0.9f; - Handles.DrawAAPolyLine(3f / zoom, GetCircleOutlinePoints(center, r, 48).ToArray()); - - // label - Vector2 labelPos = center - new Vector2(0, 8); - GUI.Label(new Rect(labelPos.x - r, labelPos.y - 8, r * 2, 18), n.title, labelStyle); - } - - List GetCircleOutlinePoints(Vector2 center, float radius, int segments) { - var pts = new List(segments + 1); - for (int i = 0; i <= segments; i++) { - float a = (float)i / segments * Mathf.PI * 2f; - pts.Add(new Vector3(center.x + Mathf.Cos(a) * radius, center.y + Mathf.Sin(a) * radius, 0)); - } - return pts; - } - - void DrawEdgeCircleNodes(DagNode from, DagNode to) { - Vector2 a = from.position; - Vector2 b = to.position; - if (a == b) return; - - // Compute edge line that starts/ends at circle circumferences - Vector2 dir = (b - a).normalized; - Vector2 start = a + dir * from.radius; - Vector2 end = b - dir * to.radius; - - // Use a simple curved line: start -> control -> end (bezier) - Vector2 control = new Vector2((start.x + end.x) / 2f, (start.y + end.y) / 2f); - // Slight vertical offset to separate overlapping lines based on node ids - float offset = ((from.id * 7 + to.id * 11) % 7 - 3) * 6f / zoom; - control += new Vector2(0, offset); - - Handles.color = Color.white * 0.9f; - Handles.DrawAAPolyLine(3f / zoom, 20, GetBezierPoints(start, control, end, 24).ToArray()); - - // Arrow at end pointing towards 'b' - DrawArrowHead(end - dir * 2f, end, 10f / zoom, 12f / zoom, Color.white); - } - - List GetBezierPoints(Vector2 p0, Vector2 p1, Vector2 p2, int seg) { - var pts = new List(seg + 1); - for (int i = 0; i <= seg; i++) { - float t = (float)i / seg; - Vector2 p = (1 - t) * (1 - t) * p0 + 2 * (1 - t) * t * p1 + t * t * p2; - pts.Add(new Vector3(p.x, p.y, 0)); - } - return pts; - } - - void DrawArrowHead(Vector2 from, Vector2 to, float headWidth, float headLength, Color color) { - Vector2 dir = (to - from).normalized; - if (dir == Vector2.zero) return; - Vector2 right = new Vector2(-dir.y, dir.x); - - Vector3 p1 = to; - Vector3 p2 = to - dir * headLength + right * headWidth * 0.5f; - Vector3 p3 = to - dir * headLength - right * headWidth * 0.5f; - - Handles.color = color; - Handles.DrawAAConvexPolygon(p1, p2, p3); - } - - // Left-to-right layered layout (sources on the left, sinks on the right) - void ComputeLeftToRightLayout() { - // build adjacency and indegree - var adj = nodes.ToDictionary(n => n.id, n => new List()); - var indeg = nodes.ToDictionary(n => n.id, n => 0); - foreach (var e in edges) { - if (!adj.ContainsKey(e.fromId) || !adj.ContainsKey(e.toId)) continue; - adj[e.fromId].Add(e.toId); - indeg[e.toId]++; - } - - // Kahn's algorithm to compute topological layers (horizontal layers) - Dictionary layer = new Dictionary(); - Queue q = new Queue(indeg.Where(kv => kv.Value == 0).Select(kv => kv.Key)); - foreach (var id in q) layer[id] = 0; - - while (q.Count > 0) { - int u = q.Dequeue(); - int l = layer[u]; - foreach (var v in adj[u]) { - // prefer placing v at least one layer after u - if (!layer.ContainsKey(v) || layer[v] < l + 1) layer[v] = l + 1; - indeg[v]--; - if (indeg[v] == 0) q.Enqueue(v); - } - } - - // Any unreachable nodes -> assign next layers - int maxLayer = layer.Count > 0 ? layer.Values.Max() : 0; - foreach (var n in nodes) { - if (!layer.ContainsKey(n.id)) { - maxLayer++; - layer[n.id] = maxLayer; - } - } - - // Group nodes by layer (left to right) - var layers = layer.GroupBy(kv => kv.Value).OrderBy(g => g.Key).Select(g => g.Select(x => x.Key).ToList()).ToList(); - - // Layout parameters (horizontal spacing drives left->right) - float hSpacing = 220f; - float vSpacing = 120f; - - // Place nodes: x increases with layer index, y spaced within layer - for (int li = 0; li < layers.Count; li++) { - var lst = layers[li]; - float totalHeight = (lst.Count - 1) * vSpacing; - for (int i = 0; i < lst.Count; i++) { - int id = lst[i]; - var n = GetNodeById(id); - if (n == null) continue; - float x = li * hSpacing; - float y = -totalHeight / 2f + i * vSpacing; - n.position = new Vector2(x, y); - } - } - - Repaint(); - } - - void FitToView() { - if (nodes.Count == 0) return; - Rect bounds = new Rect(nodes[0].position - Vector2.one * nodes[0].radius, Vector2.one * nodes[0].radius * 2f); - foreach (var n in nodes) - bounds = RectUnion(bounds, new Rect(n.position - Vector2.one * n.radius, Vector2.one * n.radius * 2f)); - - Vector2 center = bounds.center; - pan = -center; - zoom = 1.0f; - Repaint(); - } - - static Rect RectUnion(Rect a, Rect b) { - float xMin = Mathf.Min(a.xMin, b.xMin); - float xMax = Mathf.Max(a.xMax, b.xMax); - float yMin = Mathf.Min(a.yMin, b.yMin); - float yMax = Mathf.Max(a.yMax, b.yMax); - return Rect.MinMaxRect(xMin, yMin, xMax, yMax); - } - - Vector2 ScreenToGraph(Vector2 screenPos) { - Vector2 origin = new Vector2(position.width / 2, position.height / 2); - // invert the GUI.matrix transform (approx for current simple transforms) - return (screenPos - (origin + pan)) / zoom + origin * (1 - 1 / zoom); - } - - int HitTestNode(Vector2 graphPos) { - // returns node id under point or -1 - for (int i = nodes.Count - 1; i >= 0; i--) { - var n = nodes[i]; - if ((graphPos - n.position).sqrMagnitude <= n.radius * n.radius) return n.id; - } - return -1; - } - } - -} \ No newline at end of file diff --git a/Editor/DAGWindow.cs.meta b/Editor/DAGWindow.cs.meta deleted file mode 100644 index ea0ee9e..0000000 --- a/Editor/DAGWindow.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 95393aed582b8b30d965400672aec4d8 \ No newline at end of file diff --git a/Runtime/Scripts/Core/Neuron.cs b/Runtime/Scripts/Core/Neuron.cs index 530f44f..8e47dc8 100644 --- a/Runtime/Scripts/Core/Neuron.cs +++ b/Runtime/Scripts/Core/Neuron.cs @@ -524,16 +524,9 @@ namespace NanoBrain { } public virtual void RemoveReceiver(Nucleus receiverToRemove) { - int n1 = _receivers.Count; this._receivers.RemoveAll(receiver => receiver == receiverToRemove); - int n2 = _receivers.Count; - Debug.Log($" Removed {n1} - {n2} receivers"); - - n1 = receiverToRemove.synapses.Count; receiverToRemove.synapses.RemoveAll(synapse => synapse.neuron == this); - n2 = receiverToRemove.synapses.Count; - Debug.Log($" Removed {n1} - {n2} synapses"); - } + } #endregion Receivers From 830e3e7265cba18a669a540f395cb1f1c128360c Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Mon, 20 Apr 2026 17:44:27 +0200 Subject: [PATCH 17/34] Added full graph view mode --- Editor/BrainEditorWindow.cs | 135 ++++++++++++++++++------------------ Editor/ClusterInspector.cs | 80 ++++++++++++++++++++- Editor/ClusterViewer.cs | 72 ++++++++++++------- 3 files changed, 192 insertions(+), 95 deletions(-) diff --git a/Editor/BrainEditorWindow.cs b/Editor/BrainEditorWindow.cs index 2b16ffe..078095c 100644 --- a/Editor/BrainEditorWindow.cs +++ b/Editor/BrainEditorWindow.cs @@ -12,6 +12,7 @@ namespace NanoBrain { public string title; public Vector2 position; public float radius = 20f; // circle radius + public Nucleus nucleus; } [System.Serializable] @@ -19,15 +20,15 @@ namespace NanoBrain { public int fromId; public int toId; } + public class Dag { + public List nodes = new(); + public List edges = new(); + } public class BrainEditorWindow : EditorWindow { - readonly List nodes = new(); - readonly List edges = new(); + Dag dag = new(); Vector2 pan = Vector2.zero; - float zoom = 1.0f; - const float minZoom = 0.5f; - const float maxZoom = 2.0f; private readonly System.Type acceptedType = typeof(ClusterPrefab); @@ -40,8 +41,9 @@ namespace NanoBrain { void OnEnable() { // Register callback so window updates when selection changes Selection.selectionChanged += OnSelectionChanged; - RefreshSelection(); - ComputeLayout(); + dag = RefreshSelection(); + ComputeLayout(dag); + Repaint(); } private void OnDisable() { @@ -49,33 +51,41 @@ namespace NanoBrain { } private void OnSelectionChanged() { - RefreshSelection(); - ComputeLayout(); + dag = RefreshSelection(); + ComputeLayout(dag); Repaint(); } - private void RefreshSelection() { + private Dag RefreshSelection() { ClusterPrefab prefab = Selection.activeObject as ClusterPrefab; - if (prefab != null && acceptedType.IsAssignableFrom(prefab.GetType())) { - GenerateGraph(prefab); - } + if (prefab != null && acceptedType.IsAssignableFrom(prefab.GetType())) + return GenerateGraph(prefab); + else + return new Dag(); } - private void GenerateGraph(ClusterPrefab prefab) { - nodes.Clear(); - edges.Clear(); + public Dag GenerateGraph(ClusterPrefab prefab) { + Dag dag = new(); int ix = 0; foreach (Nucleus nucleus in prefab.nuclei) { - nodes.Add(new DagNode() { id = ix, title = nucleus.name }); + DagNode node = new() { + id = ix, + title = nucleus.name + }; + dag.nodes.Add(node); if (nucleus is Neuron neuron) { foreach (Nucleus receiver in neuron.receivers) { - int receiverIx = prefab.GetNucleusIndex(receiver); - edges.Add(new DagEdge() { fromId = ix, toId = receiverIx }); + DagEdge edge = new() { + fromId = ix, + toId = prefab.GetNucleusIndex(receiver) + }; + dag.edges.Add(edge); } } ix++; } + return dag; } void OnGUI() { @@ -88,28 +98,28 @@ namespace NanoBrain { Vector2 windowCenter = new(position.width / 2f, position.height / 2f); // compute graph bounds center (in graph space) - Rect bounds = GetGraphBounds(); + Rect bounds = GetGraphBounds(dag); Vector2 graphCenter = bounds.center; // compute autoPan that recenters the graph (does not modify node positions) Vector2 autoPan = -graphCenter; // moves graph center to origin // total translation = windowCenter + autoPan + user pan Matrix4x4 oldMatrix = GUI.matrix; - GUI.matrix = Matrix4x4.TRS(windowCenter + autoPan + pan, Quaternion.identity, Vector3.one * zoom) * + GUI.matrix = Matrix4x4.TRS(windowCenter + autoPan + pan, Quaternion.identity, Vector3.one) * Matrix4x4.TRS(-windowCenter, Quaternion.identity, Vector3.one); // Draw edges first - foreach (DagEdge e in edges) { - DagNode from = GetNodeById(e.fromId); - DagNode to = GetNodeById(e.toId); + foreach (DagEdge e in dag.edges) { + DagNode from = GetNodeById(dag, e.fromId); + DagNode to = GetNodeById(dag, e.toId); if (from == null || to == null) continue; DrawEdgeCircleNodes(from, to); } // Draw nodes (circles) - foreach (DagNode n in nodes) + foreach (DagNode n in dag.nodes) DrawNucleus(n); GUI.matrix = oldMatrix; @@ -118,16 +128,6 @@ namespace NanoBrain { void HandleInput() { Event e = Event.current; - // Zoom with scroll - if (e.type == EventType.ScrollWheel) { - float oldZoom = zoom; - float delta = -e.delta.y * 0.01f; - zoom = Mathf.Clamp(zoom + delta, minZoom, maxZoom); - Vector2 mouse = e.mousePosition; - pan += (mouse - new Vector2(position.width / 2, position.height / 2)) * (1 - zoom / oldZoom); - e.Use(); - } - // Pan with middle or right+ctrl drag if (e.type == EventType.MouseDrag && (e.button == 2 || (e.button == 1 && e.control))) { pan += e.delta; @@ -135,12 +135,12 @@ namespace NanoBrain { } } - DagNode GetNodeById(int id) => nodes.FirstOrDefault(x => x.id == id); + public static DagNode GetNodeById(Dag dag, int id) => dag.nodes.FirstOrDefault(x => x.id == id); - void DrawNucleus(DagNode n) { + public static void DrawNucleus(DagNode n) { Vector3 position = n.position; - Handles.color = Color.white * 0.9f; + Handles.color = Color.black * 0.9f; Handles.DrawSolidDisc(n.position, Vector3.forward, n.radius); Handles.color = Color.white; @@ -153,7 +153,7 @@ namespace NanoBrain { Handles.Label(labelPos, n.title, style); } - void DrawEdgeCircleNodes(DagNode from, DagNode to) { + public static void DrawEdgeCircleNodes(DagNode from, DagNode to) { Vector2 a = from.position; Vector2 b = to.position; if (a == b) return; @@ -162,19 +162,10 @@ namespace NanoBrain { Handles.DrawLine(from.position, to.position); } - // Right-to-left layered layout (sources on the right, sinks on the left) - void ComputeLayout() { - // build adjacency and indegree - Dictionary> adjacency = nodes.ToDictionary(n => n.id, n => new List()); - Dictionary indegree = nodes.ToDictionary(n => n.id, n => 0); - foreach (DagEdge edge in edges) { - if (!adjacency.ContainsKey(edge.fromId) || !adjacency.ContainsKey(edge.toId)) - continue; - adjacency[edge.fromId].Add(edge.toId); - indegree[edge.toId]++; - } - Dictionary outdegree = nodes.ToDictionary(node => node.id, n => 0); - foreach (DagEdge edge in edges) { + public static void ComputeLayout(Dag dag) { + Dictionary> adjacency = dag.nodes.ToDictionary(n => n.id, n => new List()); + Dictionary outdegree = dag.nodes.ToDictionary(node => node.id, n => 0); + foreach (DagEdge edge in dag.edges) { if (!adjacency.ContainsKey(edge.fromId) || !adjacency.ContainsKey(edge.toId)) continue; adjacency[edge.fromId].Add(edge.toId); @@ -183,9 +174,10 @@ namespace NanoBrain { // Kahn's algorithm to compute topological layers (horizontal layers) // build parent list (reverse adjacency) and parentIndegree = number of children each parent has - Dictionary> parents = nodes.ToDictionary(n => n.id, _ => new List()); - Dictionary childCount = nodes.ToDictionary(n => n.id, _ => 0); - foreach (DagEdge edge in edges) { + Dictionary> parents = dag.nodes.ToDictionary(n => n.id, _ => new List()); + Dictionary childCount = dag.nodes.ToDictionary(n => n.id, _ => 0); + + foreach (DagEdge edge in dag.edges) { if (!adjacency.ContainsKey(edge.fromId) || !adjacency.ContainsKey(edge.toId)) continue; adjacency[edge.fromId].Add(edge.toId); parents[edge.toId].Add(edge.fromId); // parent of 'to' is 'from' @@ -210,10 +202,9 @@ namespace NanoBrain { } } - // Any unreachable nodes -> assign next layers int maxLayer = layer.Count > 0 ? layer.Values.Max() : 0; - foreach (DagNode node in nodes) { + foreach (DagNode node in dag.nodes) { if (!layer.ContainsKey(node.id)) { maxLayer++; layer[node.id] = maxLayer; @@ -221,7 +212,12 @@ namespace NanoBrain { } // Group nodes by layer (left to right) - List> layers = layer.GroupBy(kv => kv.Value).OrderBy(g => g.Key).Select(g => g.Select(x => x.Key).ToList()).ToList(); + List> layers = + layer. + GroupBy(kv => kv.Value). + OrderBy(g => g.Key). + Select(g => g.Select(x => x.Key).ToList()). + ToList(); // Same code without using Linq // Build layers dictionary: layerIndex -> List nodeIds @@ -247,25 +243,27 @@ namespace NanoBrain { // } float hSpacing = 100f; - float vSpacing = 100f; + float totalHeight = 400f; // Place nodes: x increases with layer index, y spaced within layer for (int layerIx = 0; layerIx < layers.Count; layerIx++) { List nodeList = layers[layerIx]; - float totalHeight = (nodeList.Count - 1) * vSpacing; + float spacing = totalHeight / nodeList.Count; + float margin = 10 + spacing / 2; for (int i = 0; i < nodeList.Count; i++) { int index = nodeList[i]; - DagNode node = GetNodeById(index); + DagNode node = GetNodeById(dag, index); if (node == null) continue; float x = hSpacing + layerIx * hSpacing; - float y = 400 - totalHeight / 2f + i * vSpacing; + //float y = 400 - totalHeight / 2f + i * vSpacing; + float y = margin + i * spacing; // Debug.Log($"({li}, {i}) -> {x}, {y}"); node.position = new Vector2(x, y); } } - Repaint(); + //Repaint(); } static Rect RectUnion(Rect a, Rect b) { @@ -276,12 +274,13 @@ namespace NanoBrain { return Rect.MinMaxRect(xMin, yMin, xMax, yMax); } - Rect GetGraphBounds() { - if (nodes == null || nodes.Count == 0) return new Rect(Vector2.zero, Vector2.one); + Rect GetGraphBounds(Dag dag) { + if (dag.nodes == null || dag.nodes.Count == 0) + return new Rect(Vector2.zero, Vector2.one); Rect bounds = new( - nodes[0].position - Vector2.one * nodes[0].radius, - 2f * nodes[0].radius * Vector2.one); - foreach (var n in nodes) + dag.nodes[0].position - Vector2.one * dag.nodes[0].radius, + 2f * dag.nodes[0].radius * Vector2.one); + foreach (var n in dag.nodes) bounds = RectUnion(bounds, new Rect(n.position - Vector2.one * n.radius, 2f * n.radius * Vector2.one)); return bounds; diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterInspector.cs index c93e228..06d9db2 100644 --- a/Editor/ClusterInspector.cs +++ b/Editor/ClusterInspector.cs @@ -62,8 +62,18 @@ namespace NanoBrain { public class GraphEditor : GraphView { - public GraphEditor(ClusterPrefab prefab) : base(prefab) { + public enum Mode { + Focus, + Full + } + public Mode mode = Mode.Focus; + public GraphEditor(ClusterPrefab prefab) : base(prefab) { + // create an EnumField for Mode + EnumField enumField = new(mode); + enumField.style.width = 80; + enumField.RegisterValueChangedCallback(OnModeChange); + outputContainer.Insert(0, enumField); Button addButton = new(() => OnAddClusterOutput()) { text = "Add" @@ -71,7 +81,22 @@ namespace NanoBrain { outputContainer.Add(addButton); Add(outputContainer); + } + private void OnModeChange(ChangeEvent evt) { + mode = (Mode)evt.newValue; + + Debug.Log("Mode changed to: " + mode); + } + + Nucleus selectedOutput; + protected override void OnOutputChanged(string outputName) { + if (this.currentNucleus.parent != null) + // Get nucleus in the parent instance + this.selectedOutput = this.currentNucleus.parent.GetNucleus(outputName); + else + // Get nucleus in the prefab + this.selectedOutput = this.prefab.GetNucleus(outputName); } void OnAddClusterOutput() { @@ -83,7 +108,6 @@ namespace NanoBrain { this.currentNucleus = newOutput; } - public void SetGraph(GameObject gameObject, Nucleus nucleus, VisualElement inspectorContainer) { this.gameObject = gameObject; //this.cluster = brain; @@ -123,6 +147,58 @@ namespace NanoBrain { inspectorContainer.Add(inspectorIMGUIContainer); } + protected override void DrawGraph() { + if (mode == Mode.Focus) + DrawFocusGraph(); + else + DrawFullGraph(); + } + + protected void DrawFullGraph() { + Dag dag = GenerateGraph(this.prefab); + BrainEditorWindow.ComputeLayout(dag); + // Draw edges + foreach (DagEdge e in dag.edges) { + DagNode from = dag.nodes.FirstOrDefault(x => x.id == e.fromId); + DagNode 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 (DagNode n in dag.nodes) + DrawNucleus(n.nucleus, n.position, 1, n.radius); + } + + public Dag GenerateGraph(ClusterPrefab prefab) { + Dag dag = new(); + + int ix = 0; + foreach (Nucleus nucleus in prefab.nuclei) { + DagNode node = new() { + id = ix, + title = nucleus.name, + nucleus = nucleus + }; + dag.nodes.Add(node); + if (nucleus is Neuron neuron) { + foreach (Nucleus receiver in neuron.receivers) { + DagEdge edge = new() { + fromId = ix, + toId = prefab.GetNucleusIndex(receiver) + }; + dag.edges.Add(edge); + } + } + ix++; + } + return dag; + } + #region Inspector private VisualElement inspectorIMGUIContainer; diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index 00d6132..241abf7 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -38,9 +38,9 @@ namespace NanoBrain { outputContainer = new() { style = { - flexDirection = FlexDirection.Row, - alignItems = Align.Center, - } + flexDirection = FlexDirection.Row, + alignItems = Align.Center, + } }; List names = this.prefab.outputs.Select(output => output.name).ToList(); @@ -59,7 +59,7 @@ namespace NanoBrain { RegisterCallback(evt => Unsubscribe()); } - void OnOutputChanged(string outputName) { + protected virtual void OnOutputChanged(string outputName) { if (this.currentNucleus.parent != null) // Get nucleus in the parent instance this.currentNucleus = this.currentNucleus.parent.GetNucleus(outputName); @@ -177,7 +177,11 @@ namespace NanoBrain { } - private void DrawGraph() { + protected virtual void DrawGraph() { + DrawFocusGraph(); + } + + protected void DrawFocusGraph() { float size = 20; Vector3 position = new(150, 210, 0); @@ -235,9 +239,6 @@ namespace NanoBrain { // Handles.Label(labelPos, receptorName, style); // } // else { - Handles.color = Color.white; - // The selected nucleus highlight ring - Handles.DrawSolidDisc(position, Vector3.forward, size + 2); float maxValue = 1; if (this.currentNucleus is Neuron neuron) maxValue = neuron.outputMagnitude; @@ -249,9 +250,6 @@ namespace NanoBrain { // } } else { - Handles.color = Color.white; - // The selected nucleus highlight ring - Handles.DrawSolidDisc(position, Vector3.forward, size + 2); float maxValue = 1; if (this.currentNucleus is Neuron neuron) maxValue = neuron.outputMagnitude; @@ -301,8 +299,7 @@ namespace NanoBrain { continue; Vector3 pos = new(50, margin + row * spacing, 0.0f); - Handles.color = Color.white; - Handles.DrawLine(parentPos, pos); + DrawEdge(parentPos, pos); DrawNucleus(receiverNucleus, pos, maxValue, size); row++; @@ -321,7 +318,7 @@ namespace NanoBrain { if (synapse.neuron == null) continue; - if (synapse.neuron.parent is Cluster cluster && + if (synapse.neuron.parent is Cluster cluster && cluster.siblingClusters != null && synapse.neuron.parent != nucleus.parent) { if (drawnArrays.Contains(cluster.siblingClusters)) @@ -372,16 +369,16 @@ namespace NanoBrain { if (synapse.neuron.parent != null && synapse.neuron.parent != this.currentNucleus.parent) { // the synapse nucleus is part of a subcluster //DrawNucleus(synapse.neuron.parent, pos, maxValue, size, color); - DrawNucleus(synapse.neuron, pos, maxValue, size, color); + DrawNucleus(synapse.neuron, pos, size, color); } else { - DrawNucleus(synapse.neuron, pos, maxValue, size, color); + DrawNucleus(synapse.neuron, pos, size, color); } row++; } } - private void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size) { + protected void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size) { Color color; if (Application.isPlaying) { float brightness = 0; @@ -391,10 +388,16 @@ namespace NanoBrain { } else color = Color.black; - DrawNucleus(nucleus, position, maxValue, size, color); + DrawNucleus(nucleus, position, size, color); } - private void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size, Color color) { + protected void DrawNucleus(Nucleus nucleus, Vector3 position, float size, Color color) { + 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); @@ -469,18 +472,18 @@ namespace NanoBrain { // This neuron is part of another cluster parentCluster1.name ??= ""; string baseName = ""; - int colonPos = parentCluster1.name.IndexOf(":"); - if (colonPos > 0 && colonPos < parentCluster1.name.Length - 2) - baseName = parentCluster1.name[..colonPos] + "."; - else - baseName = parentCluster1.name + "."; + int colonPos = parentCluster1.name.IndexOf(":"); + if (colonPos > 0 && colonPos < parentCluster1.name.Length - 2) + baseName = parentCluster1.name[..colonPos] + "."; + else + baseName = parentCluster1.name + "."; // if (colonPos > 0 && colonPos < parentCluster1.name.Length - 2) { // // if it is an array, we should not show the :0 of the first element // //baseName = baseName[..colonPos]; // Handles.Label(labelPos, baseName + nucleus.name, style); // } // else - Handles.Label(labelPos, baseName + nucleus.name, style); + Handles.Label(labelPos, baseName + nucleus.name, style); } else { nucleus.name ??= ""; @@ -562,6 +565,25 @@ namespace NanoBrain { } } + protected void DrawEdge(Vector2 from, Vector2 to) { + Handles.color = Color.white; + Handles.DrawLine(from, to); + } + + // protected void DrawNode(Vector2 position, float size) { + // Handles.color = Color.black * 0.9f; + // Handles.DrawSolidDisc(position, Vector3.forward, size); + + // Handles.color = Color.white; + // GUIStyle style = new(EditorStyles.label) { + // alignment = TextAnchor.UpperCenter, + // normal = { textColor = Color.white }, + // fontStyle = FontStyle.Bold, + // }; + // Vector3 labelPos = position - Vector3.down * (size + 10f); // below disc along up axis + // Handles.Label(labelPos, n.title, style); + // } + void OnSceneGUI(SceneView sceneView) { if (this.gameObject != null) { // if (this.currentNucleus is IReceptor receptor) { From 471ed3661c1736faa97078a459a04938db87da21 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Tue, 21 Apr 2026 10:46:21 +0200 Subject: [PATCH 18/34] Completed full graph integration --- Editor/BrainEditorWindow.cs | 290 ------------------------------- Editor/BrainEditorWindow.cs.meta | 2 - Editor/ClusterInspector.cs | 80 +-------- Editor/ClusterViewer.cs | 236 +++++++++++++++++++++++-- 4 files changed, 224 insertions(+), 384 deletions(-) delete mode 100644 Editor/BrainEditorWindow.cs delete mode 100644 Editor/BrainEditorWindow.cs.meta diff --git a/Editor/BrainEditorWindow.cs b/Editor/BrainEditorWindow.cs deleted file mode 100644 index 078095c..0000000 --- a/Editor/BrainEditorWindow.cs +++ /dev/null @@ -1,290 +0,0 @@ -using UnityEngine; -using UnityEditor; -using System.Collections.Generic; -using System.Linq; - -namespace NanoBrain { - - // Simple DAG data model - [System.Serializable] - public class DagNode { - public int id; - public string title; - public Vector2 position; - public float radius = 20f; // circle radius - public Nucleus nucleus; - } - - [System.Serializable] - public class DagEdge { - public int fromId; - public int toId; - } - public class Dag { - public List nodes = new(); - public List edges = new(); - } - - public class BrainEditorWindow : EditorWindow { - Dag dag = new(); - - Vector2 pan = Vector2.zero; - - private readonly System.Type acceptedType = typeof(ClusterPrefab); - - [MenuItem("Window/Brain Viewer")] - public static void ShowWindow() { - var w = GetWindow("Brain Viewer"); - w.minSize = new Vector2(500, 300); - } - - void OnEnable() { - // Register callback so window updates when selection changes - Selection.selectionChanged += OnSelectionChanged; - dag = RefreshSelection(); - ComputeLayout(dag); - Repaint(); - } - - private void OnDisable() { - Selection.selectionChanged -= OnSelectionChanged; - } - - private void OnSelectionChanged() { - dag = RefreshSelection(); - ComputeLayout(dag); - Repaint(); - } - - private Dag RefreshSelection() { - ClusterPrefab prefab = Selection.activeObject as ClusterPrefab; - if (prefab != null && acceptedType.IsAssignableFrom(prefab.GetType())) - return GenerateGraph(prefab); - else - return new Dag(); - } - - public Dag GenerateGraph(ClusterPrefab prefab) { - Dag dag = new(); - - int ix = 0; - foreach (Nucleus nucleus in prefab.nuclei) { - DagNode node = new() { - id = ix, - title = nucleus.name - }; - dag.nodes.Add(node); - if (nucleus is Neuron neuron) { - foreach (Nucleus receiver in neuron.receivers) { - DagEdge edge = new() { - fromId = ix, - toId = prefab.GetNucleusIndex(receiver) - }; - dag.edges.Add(edge); - } - } - ix++; - } - return dag; - } - - void OnGUI() { - HandleInput(); - - Rect rect = new(0, 0, position.width, position.height); - EditorGUI.DrawRect(rect, new Color(0.11f, 0.11f, 0.11f)); - - // compute window center - Vector2 windowCenter = new(position.width / 2f, position.height / 2f); - - // compute graph bounds center (in graph space) - Rect bounds = GetGraphBounds(dag); - Vector2 graphCenter = bounds.center; - - // compute autoPan that recenters the graph (does not modify node positions) - Vector2 autoPan = -graphCenter; // moves graph center to origin - // total translation = windowCenter + autoPan + user pan - Matrix4x4 oldMatrix = GUI.matrix; - GUI.matrix = Matrix4x4.TRS(windowCenter + autoPan + pan, Quaternion.identity, Vector3.one) * - Matrix4x4.TRS(-windowCenter, Quaternion.identity, Vector3.one); - - - // Draw edges first - foreach (DagEdge e in dag.edges) { - DagNode from = GetNodeById(dag, e.fromId); - DagNode to = GetNodeById(dag, e.toId); - if (from == null || to == null) - continue; - DrawEdgeCircleNodes(from, to); - } - - // Draw nodes (circles) - foreach (DagNode n in dag.nodes) - DrawNucleus(n); - - GUI.matrix = oldMatrix; - } - - void HandleInput() { - Event e = Event.current; - - // Pan with middle or right+ctrl drag - if (e.type == EventType.MouseDrag && (e.button == 2 || (e.button == 1 && e.control))) { - pan += e.delta; - e.Use(); - } - } - - public static DagNode GetNodeById(Dag dag, int id) => dag.nodes.FirstOrDefault(x => x.id == id); - - public static void DrawNucleus(DagNode n) { - Vector3 position = n.position; - - Handles.color = Color.black * 0.9f; - Handles.DrawSolidDisc(n.position, Vector3.forward, n.radius); - - Handles.color = Color.white; - GUIStyle style = new(EditorStyles.label) { - alignment = TextAnchor.UpperCenter, - normal = { textColor = Color.white }, - fontStyle = FontStyle.Bold, - }; - Vector3 labelPos = position - Vector3.down * (n.radius + 10f); // below disc along up axis - Handles.Label(labelPos, n.title, style); - } - - public static void DrawEdgeCircleNodes(DagNode from, DagNode to) { - Vector2 a = from.position; - Vector2 b = to.position; - if (a == b) return; - - Handles.color = Color.white * 0.9f; - Handles.DrawLine(from.position, to.position); - } - - public static void ComputeLayout(Dag dag) { - Dictionary> adjacency = dag.nodes.ToDictionary(n => n.id, n => new List()); - Dictionary outdegree = dag.nodes.ToDictionary(node => node.id, n => 0); - foreach (DagEdge edge in dag.edges) { - if (!adjacency.ContainsKey(edge.fromId) || !adjacency.ContainsKey(edge.toId)) - continue; - adjacency[edge.fromId].Add(edge.toId); - outdegree[edge.fromId]++; - } - - // Kahn's algorithm to compute topological layers (horizontal layers) - // build parent list (reverse adjacency) and parentIndegree = number of children each parent has - Dictionary> parents = dag.nodes.ToDictionary(n => n.id, _ => new List()); - Dictionary childCount = dag.nodes.ToDictionary(n => n.id, _ => 0); - - foreach (DagEdge edge in dag.edges) { - if (!adjacency.ContainsKey(edge.fromId) || !adjacency.ContainsKey(edge.toId)) continue; - adjacency[edge.fromId].Add(edge.toId); - parents[edge.toId].Add(edge.fromId); // parent of 'to' is 'from' - childCount[edge.fromId]++; // outdegree - } - - Dictionary layer = new(); - Queue queue = new(outdegree.Where(kv => kv.Value == 0).Select(kv => kv.Key)); - foreach (int id in queue) - layer[id] = 0; - - // process parents (reverse traversal) - while (queue.Count > 0) { - int u = queue.Dequeue(); - int l = layer[u]; - foreach (int p in parents[u]) { - if (!layer.ContainsKey(p) || layer[p] < l + 1) - layer[p] = l + 1; - childCount[p]--; // decrement remaining unprocessed children - if (childCount[p] == 0) - queue.Enqueue(p); - } - } - - // Any unreachable nodes -> assign next layers - int maxLayer = layer.Count > 0 ? layer.Values.Max() : 0; - foreach (DagNode node in dag.nodes) { - if (!layer.ContainsKey(node.id)) { - maxLayer++; - layer[node.id] = maxLayer; - } - } - - // Group nodes by layer (left to right) - List> layers = - layer. - GroupBy(kv => kv.Value). - OrderBy(g => g.Key). - Select(g => g.Select(x => x.Key).ToList()). - ToList(); - - // Same code without using Linq - // Build layers dictionary: layerIndex -> List nodeIds - // Dictionary> layersDict = new(); - // foreach (KeyValuePair kv in layer) { - // int nodeId = kv.Key; - // int layerIndex = kv.Value; - // if (!layersDict.TryGetValue(layerIndex, out List list)) { - // list = new List(); - // layersDict[layerIndex] = list; - // } - // list.Add(nodeId); - // } - - // // Determine sorted layer indices - // List layerIndices = new(layersDict.Keys); - // layerIndices.Sort(); // ascending order - - // // Build final List> in sorted order - // List> layers = new(); - // foreach (int idx in layerIndices) { - // layers.Add(layersDict[idx]); - // } - - float hSpacing = 100f; - float totalHeight = 400f; - - // Place nodes: x increases with layer index, y spaced within layer - for (int layerIx = 0; layerIx < layers.Count; layerIx++) { - List nodeList = layers[layerIx]; - float spacing = totalHeight / nodeList.Count; - float margin = 10 + spacing / 2; - for (int i = 0; i < nodeList.Count; i++) { - int index = nodeList[i]; - DagNode node = GetNodeById(dag, index); - if (node == null) - continue; - float x = hSpacing + layerIx * hSpacing; - //float y = 400 - totalHeight / 2f + i * vSpacing; - float y = margin + i * spacing; - // Debug.Log($"({li}, {i}) -> {x}, {y}"); - node.position = new Vector2(x, y); - } - } - - //Repaint(); - } - - static Rect RectUnion(Rect a, Rect b) { - float xMin = Mathf.Min(a.xMin, b.xMin); - float xMax = Mathf.Max(a.xMax, b.xMax); - float yMin = Mathf.Min(a.yMin, b.yMin); - float yMax = Mathf.Max(a.yMax, b.yMax); - return Rect.MinMaxRect(xMin, yMin, xMax, yMax); - } - - Rect GetGraphBounds(Dag dag) { - if (dag.nodes == null || dag.nodes.Count == 0) - return new Rect(Vector2.zero, Vector2.one); - Rect bounds = new( - dag.nodes[0].position - Vector2.one * dag.nodes[0].radius, - 2f * dag.nodes[0].radius * Vector2.one); - foreach (var n in dag.nodes) - bounds = RectUnion(bounds, - new Rect(n.position - Vector2.one * n.radius, 2f * n.radius * Vector2.one)); - return bounds; - } - } - -} \ No newline at end of file diff --git a/Editor/BrainEditorWindow.cs.meta b/Editor/BrainEditorWindow.cs.meta deleted file mode 100644 index 5d8b61f..0000000 --- a/Editor/BrainEditorWindow.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: f041740900808273ab006e7d276a78e9 diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterInspector.cs index 06d9db2..556f6a2 100644 --- a/Editor/ClusterInspector.cs +++ b/Editor/ClusterInspector.cs @@ -62,18 +62,7 @@ namespace NanoBrain { public class GraphEditor : GraphView { - public enum Mode { - Focus, - Full - } - public Mode mode = Mode.Focus; - public GraphEditor(ClusterPrefab prefab) : base(prefab) { - // create an EnumField for Mode - EnumField enumField = new(mode); - enumField.style.width = 80; - enumField.RegisterValueChangedCallback(OnModeChange); - outputContainer.Insert(0, enumField); Button addButton = new(() => OnAddClusterOutput()) { text = "Add" @@ -83,22 +72,6 @@ namespace NanoBrain { Add(outputContainer); } - private void OnModeChange(ChangeEvent evt) { - mode = (Mode)evt.newValue; - - Debug.Log("Mode changed to: " + mode); - } - - Nucleus selectedOutput; - protected override void OnOutputChanged(string outputName) { - if (this.currentNucleus.parent != null) - // Get nucleus in the parent instance - this.selectedOutput = this.currentNucleus.parent.GetNucleus(outputName); - else - // Get nucleus in the prefab - this.selectedOutput = this.prefab.GetNucleus(outputName); - } - void OnAddClusterOutput() { Nucleus newOutput = new Neuron(this.prefab, "New Output"); this.prefab.RefreshOutputs(); @@ -115,6 +88,7 @@ namespace NanoBrain { this.serializedBrain = new SerializedObject(this.prefab); this.currentNucleus = nucleus; Rebuild(inspectorContainer); + OnOutputChanged(outputsField.choices[0]); } void Rebuild(VisualElement inspectorContainer) { @@ -147,58 +121,6 @@ namespace NanoBrain { inspectorContainer.Add(inspectorIMGUIContainer); } - protected override void DrawGraph() { - if (mode == Mode.Focus) - DrawFocusGraph(); - else - DrawFullGraph(); - } - - protected void DrawFullGraph() { - Dag dag = GenerateGraph(this.prefab); - BrainEditorWindow.ComputeLayout(dag); - // Draw edges - foreach (DagEdge e in dag.edges) { - DagNode from = dag.nodes.FirstOrDefault(x => x.id == e.fromId); - DagNode 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 (DagNode n in dag.nodes) - DrawNucleus(n.nucleus, n.position, 1, n.radius); - } - - public Dag GenerateGraph(ClusterPrefab prefab) { - Dag dag = new(); - - int ix = 0; - foreach (Nucleus nucleus in prefab.nuclei) { - DagNode node = new() { - id = ix, - title = nucleus.name, - nucleus = nucleus - }; - dag.nodes.Add(node); - if (nucleus is Neuron neuron) { - foreach (Nucleus receiver in neuron.receivers) { - DagEdge edge = new() { - fromId = ix, - toId = prefab.GetNucleusIndex(receiver) - }; - dag.edges.Add(edge); - } - } - ix++; - } - return dag; - } - #region Inspector private VisualElement inspectorIMGUIContainer; diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index 241abf7..972738c 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -22,6 +22,12 @@ namespace NanoBrain { protected VisualElement outputContainer; protected readonly PopupField outputsField; + public enum Mode { + Focus, + Full + } + public Mode mode = Mode.Focus; + public GraphView(ClusterPrefab prefab) { this.prefab = prefab; @@ -43,6 +49,12 @@ namespace NanoBrain { } }; + EnumField enumField = new(mode); + enumField.style.width = 80; + enumField.RegisterValueChangedCallback(OnModeChange); + outputContainer.Add(enumField); + + List names = this.prefab.outputs.Select(output => output.name).ToList(); if (names.Count > 0 && names.First() != null) { outputsField = new(names, names.First()) { @@ -59,13 +71,19 @@ namespace NanoBrain { RegisterCallback(evt => Unsubscribe()); } + protected virtual void OnModeChange(ChangeEvent evt) { + mode = (Mode)evt.newValue; + } + + protected Nucleus selectedOutput; protected virtual void OnOutputChanged(string outputName) { if (this.currentNucleus.parent != null) // Get nucleus in the parent instance - this.currentNucleus = this.currentNucleus.parent.GetNucleus(outputName); + this.selectedOutput = this.currentNucleus.parent.GetNucleus(outputName); else // Get nucleus in the prefab - this.currentNucleus = this.prefab.GetNucleus(outputName); + this.selectedOutput = this.prefab.GetNucleus(outputName); + this.currentNucleus = this.selectedOutput; } bool subscribed = false; @@ -82,22 +100,21 @@ namespace NanoBrain { subscribed = false; } - public void SetGraph(GameObject gameObject, Nucleus nucleus) { //}, VisualElement inspectorContainer) { + public void SetGraph(GameObject gameObject, Nucleus nucleus) { this.gameObject = gameObject; - //this.cluster = brain; if (Application.isPlaying == false) this.serializedBrain = new SerializedObject(this.prefab); this.currentNucleus = nucleus; Rebuild(); //inspectorContainer); + OnOutputChanged(outputsField.choices[0]); + } - void Rebuild() { //VisualElement inspectorContainer) { + void Rebuild() { BuildLayers(); - if (this.currentNucleus == null) { - // inspectorContainer.Clear(); + if (this.currentNucleus == null) return; - } string path = AssetDatabase.GetAssetPath(this.prefab); // or known path this.prefabAsset = AssetDatabase.LoadAssetAtPath(path); @@ -106,7 +123,6 @@ namespace NanoBrain { this.prefabAsset = CreateInstance(); //Debug.LogError("Cluster Prefab is not found on disk"); } - //DrawInspector(inspectorContainer); } protected void BuildLayers() { @@ -178,7 +194,68 @@ namespace NanoBrain { } protected virtual void DrawGraph() { - DrawFocusGraph(); + if (mode == Mode.Focus) + DrawFocusGraph(); + else + DrawFullGraph(); + } + + protected void DrawFullGraph() { + //Dag dag = GenerateGraph(this.prefab); + Dag dag = GenerateGraph(this.selectedOutput); + Dag.ComputeLayout(dag); + // Draw edges + foreach (DagEdge e in dag.edges) { + DagNode from = dag.nodes.FirstOrDefault(x => x.id == e.fromId); + DagNode 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 (DagNode n in dag.nodes) + DrawNucleus(n.nucleus, n.position, 1, n.radius); + } + + public Dag GenerateGraph(Nucleus rootNucleus) { + Dag dag = new(); + if (rootNucleus == null) + return dag; + + int ix = 0; + DagNode receiver = new() { + id = ix, + //title = nucleus.name, + nucleus = rootNucleus + }; + dag.nodes.Add(receiver); + ix++; + DescendGraph(receiver, ref ix, dag); + return dag; + } + + private void DescendGraph(DagNode receiver, ref int ix, Dag dag) { + foreach (Synapse synapse in receiver.nucleus.synapses) { + DagNode synapseNode = dag.FindNode(synapse.neuron.name); + if (synapseNode == null) { + synapseNode = new() { + id = ix, + nucleus = synapse.neuron + }; + dag.nodes.Add(synapseNode); + } + DagEdge edge = new() { + fromId = synapseNode.id, + toId = receiver.id + }; + dag.edges.Add(edge); + ix++; + DescendGraph(synapseNode, ref ix, dag); + } } protected void DrawFocusGraph() { @@ -416,7 +493,7 @@ namespace NanoBrain { fontStyle = FontStyle.Bold, }; - if (nucleus.parent != null && nucleus.parent is Cluster parentCluster) { + if (nucleus.parent is Cluster parentCluster) { if (expandArray) { // Put array indices above elements style.alignment = TextAnchor.LowerCenter; @@ -463,12 +540,12 @@ namespace NanoBrain { } } - if (expandArray == false) {// || nucleus is not IReceptor) { + if (expandArray == false) { // put name below nucleus Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron style.alignment = TextAnchor.UpperCenter; - if (nucleus.parent != null && nucleus.parent is Cluster parentCluster1) { + if (nucleus.parent != currentNucleus.parent && nucleus.parent is Cluster parentCluster1) { // This neuron is part of another cluster parentCluster1.name ??= ""; string baseName = ""; @@ -612,4 +689,137 @@ namespace NanoBrain { public int ix = 0; public List neuroids = new(); } + + [System.Serializable] + public class DagNode { + public int id; + public Vector2 position; + public float radius = 20f; // circle radius + public Nucleus nucleus; + } + + [System.Serializable] + public class DagEdge { + public int fromId; + public int toId; + } + public class Dag { + public List nodes = new(); + public List edges = new(); + + public DagNode FindNode(string name) { + foreach (DagNode node in this.nodes) { + if (node.nucleus.name == name) + return node; + } + return null; + } + + public static DagNode GetNodeById(Dag dag, int id) => dag.nodes.FirstOrDefault(x => x.id == id); + + public static void ComputeLayout(Dag dag) { + Dictionary> adjacency = dag.nodes.ToDictionary(n => n.id, n => new List()); + Dictionary outdegree = dag.nodes.ToDictionary(node => node.id, n => 0); + foreach (DagEdge edge in dag.edges) { + if (!adjacency.ContainsKey(edge.fromId) || !adjacency.ContainsKey(edge.toId)) + continue; + adjacency[edge.fromId].Add(edge.toId); + outdegree[edge.fromId]++; + } + + // Kahn's algorithm to compute topological layers (horizontal layers) + // build parent list (reverse adjacency) and parentIndegree = number of children each parent has + Dictionary> parents = dag.nodes.ToDictionary(n => n.id, _ => new List()); + Dictionary childCount = dag.nodes.ToDictionary(n => n.id, _ => 0); + + foreach (DagEdge edge in dag.edges) { + if (!adjacency.ContainsKey(edge.fromId) || !adjacency.ContainsKey(edge.toId)) continue; + adjacency[edge.fromId].Add(edge.toId); + parents[edge.toId].Add(edge.fromId); // parent of 'to' is 'from' + childCount[edge.fromId]++; // outdegree + } + + Dictionary layer = new(); + Queue queue = new(outdegree.Where(kv => kv.Value == 0).Select(kv => kv.Key)); + foreach (int id in queue) + layer[id] = 0; + + // process parents (reverse traversal) + while (queue.Count > 0) { + int u = queue.Dequeue(); + int l = layer[u]; + foreach (int p in parents[u]) { + if (!layer.ContainsKey(p) || layer[p] < l + 1) + layer[p] = l + 1; + childCount[p]--; // decrement remaining unprocessed children + if (childCount[p] == 0) + queue.Enqueue(p); + } + } + + // Any unreachable nodes -> assign next layers + int maxLayer = layer.Count > 0 ? layer.Values.Max() : 0; + foreach (DagNode node in dag.nodes) { + if (!layer.ContainsKey(node.id)) { + maxLayer++; + layer[node.id] = maxLayer; + } + } + + // Group nodes by layer (left to right) + List> layers = + layer. + GroupBy(kv => kv.Value). + OrderBy(g => g.Key). + Select(g => g.Select(x => x.Key).ToList()). + ToList(); + + // Same code without using Linq + // Build layers dictionary: layerIndex -> List nodeIds + // Dictionary> layersDict = new(); + // foreach (KeyValuePair kv in layer) { + // int nodeId = kv.Key; + // int layerIndex = kv.Value; + // if (!layersDict.TryGetValue(layerIndex, out List list)) { + // list = new List(); + // layersDict[layerIndex] = list; + // } + // list.Add(nodeId); + // } + + // // Determine sorted layer indices + // List layerIndices = new(layersDict.Keys); + // layerIndices.Sort(); // ascending order + + // // Build final List> in sorted order + // List> layers = new(); + // foreach (int idx in layerIndices) { + // layers.Add(layersDict[idx]); + // } + + float hSpacing = 100f; + float totalHeight = 400f; + + // Place nodes: x increases with layer index, y spaced within layer + for (int layerIx = 0; layerIx < layers.Count; layerIx++) { + List nodeList = layers[layerIx]; + float spacing = totalHeight / nodeList.Count; + float margin = 10 + spacing / 2; + for (int i = 0; i < nodeList.Count; i++) { + int index = nodeList[i]; + DagNode node = GetNodeById(dag, index); + if (node == null) + continue; + float x = hSpacing + layerIx * hSpacing; + //float y = 400 - totalHeight / 2f + i * vSpacing; + float y = margin + i * spacing; + // Debug.Log($"({li}, {i}) -> {x}, {y}"); + node.position = new Vector2(x, y); + } + } + + //Repaint(); + } + } + } \ No newline at end of file From 02047a4bd9bf57152807d141567c15c17b12cda0 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Tue, 21 Apr 2026 11:25:06 +0200 Subject: [PATCH 19/34] Adde full graph scrollbar --- Editor/ClusterInspector.cs | 19 ++++--- Editor/ClusterViewer.cs | 113 +++++++++++++++++++++++++------------ 2 files changed, 86 insertions(+), 46 deletions(-) diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterInspector.cs index 556f6a2..3f8e52e 100644 --- a/Editor/ClusterInspector.cs +++ b/Editor/ClusterInspector.cs @@ -67,16 +67,16 @@ namespace NanoBrain { Button addButton = new(() => OnAddClusterOutput()) { text = "Add" }; - outputContainer.Add(addButton); + topMenuContainer?.Add(addButton); - Add(outputContainer); + Add(topMenuContainer); } void OnAddClusterOutput() { Nucleus newOutput = new Neuron(this.prefab, "New Output"); this.prefab.RefreshOutputs(); - outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); - outputsField.value = newOutput.name; + outputsPopup.choices = this.prefab.outputs.Select(output => output.name).ToList(); + outputsPopup.value = newOutput.name; this.currentNucleus = newOutput; } @@ -88,7 +88,8 @@ namespace NanoBrain { this.serializedBrain = new SerializedObject(this.prefab); this.currentNucleus = nucleus; Rebuild(inspectorContainer); - OnOutputChanged(outputsField.choices[0]); + if (outputsPopup != null) + OnOutputChanged(outputsPopup.choices[0]); } void Rebuild(VisualElement inspectorContainer) { @@ -173,7 +174,7 @@ namespace NanoBrain { if (newName != this.currentNucleus.name) { this.currentNucleus.name = newName; this.prefab.RefreshOutputs(); - outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); + outputsPopup.choices = this.prefab.outputs.Select(output => output.name).ToList(); anythingChanged = true; } } @@ -492,10 +493,10 @@ namespace NanoBrain { } this.prefab.nuclei.Remove(nucleus); - if (outputsField.value == nucleus.name) { + if (outputsPopup.value == nucleus.name) { this.prefab.RefreshOutputs(); - outputsField.choices = this.prefab.outputs.Select(output => output.name).ToList(); - outputsField.index = 0; + outputsPopup.choices = this.prefab.outputs.Select(output => output.name).ToList(); + outputsPopup.index = 0; } Neuron.Delete(nucleus); diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index 972738c..50f5fdd 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -13,14 +13,18 @@ namespace NanoBrain { protected readonly ClusterPrefab prefab; protected SerializedObject serializedBrain; protected Nucleus currentNucleus; + protected Nucleus selectedOutput; + protected GameObject gameObject; private List layers = new(); private readonly Dictionary neuroidPositions = new(); private bool expandArray = false; protected ClusterPrefab prefabAsset; - protected VisualElement outputContainer; - protected readonly PopupField outputsField; + protected VisualElement topMenuContainer; + protected ScrollView scrollView; + protected IMGUIContainer graphContainer; + protected readonly PopupField outputsPopup; public enum Mode { Focus, @@ -34,37 +38,49 @@ namespace NanoBrain { name = "content"; style.flexGrow = 1; - IMGUIContainer graphContainer = new(OnIMGUI); - graphContainer.style.position = Position.Absolute; - graphContainer.style.left = 0; graphContainer.style.top = 0; - graphContainer.style.right = 0; graphContainer.style.bottom = 0; - graphContainer.pickingMode = PickingMode.Position; - graphContainer.focusable = true; - Add(graphContainer); - - outputContainer = new() { + topMenuContainer = new() { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, } }; - EnumField enumField = new(mode); - enumField.style.width = 80; - enumField.RegisterValueChangedCallback(OnModeChange); - outputContainer.Add(enumField); - + EnumField modePopup = new(mode); + modePopup.style.width = 80; + modePopup.RegisterValueChangedCallback(OnModeChange); + topMenuContainer.Add(modePopup); List names = this.prefab.outputs.Select(output => output.name).ToList(); if (names.Count > 0 && names.First() != null) { - outputsField = new(names, names.First()) { + outputsPopup = new(names, names.First()) { style = { flexGrow = 1 } }; - outputsField.RegisterValueChangedCallback(evt => OnOutputChanged(evt.newValue)); - outputContainer.Add(outputsField); + outputsPopup.RegisterValueChangedCallback(evt => OnOutputChanged(evt.newValue)); + topMenuContainer.Add(outputsPopup); } + Add(topMenuContainer); + + 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.style.flexGrow = 1; + scrollView.horizontalScrollerVisibility = ScrollerVisibility.Auto; // Auto shows when needed + scrollView.verticalScrollerVisibility = ScrollerVisibility.Hidden; + + graphContainer = new(OnIMGUI); + //graphContainer.style.position = Position.Relative; // or omit this line + //graphContainer.style.position = Position.Absolute; + // graphContainer.style.left = 0; graphContainer.style.top = 0; + // graphContainer.style.right = 0; graphContainer.style.bottom = 0; + graphContainer.pickingMode = PickingMode.Position; + graphContainer.focusable = true; + //graphContainer.style.width = 1200; + //graphContainer.style.width = new StyleLength(StyleKeyword.Null); // allow content to determine width + + scrollView.contentContainer.Add(graphContainer); + Add(scrollView); - Add(outputContainer); // Subscribe when added to panel (editor UI ready) RegisterCallback(evt => Subscribe()); @@ -75,7 +91,6 @@ namespace NanoBrain { mode = (Mode)evt.newValue; } - protected Nucleus selectedOutput; protected virtual void OnOutputChanged(string outputName) { if (this.currentNucleus.parent != null) // Get nucleus in the parent instance @@ -86,6 +101,7 @@ namespace NanoBrain { this.currentNucleus = this.selectedOutput; } + bool subscribed = false; void Subscribe() { if (subscribed) return; @@ -106,8 +122,8 @@ namespace NanoBrain { this.serializedBrain = new SerializedObject(this.prefab); this.currentNucleus = nucleus; Rebuild(); //inspectorContainer); - OnOutputChanged(outputsField.choices[0]); - + if (outputsPopup != null) + OnOutputChanged(outputsPopup.choices[0]); } void Rebuild() { @@ -190,9 +206,10 @@ namespace NanoBrain { Handles.BeginGUI(); DrawGraph(); Handles.EndGUI(); - } + #region Graph + protected virtual void DrawGraph() { if (mode == Mode.Focus) DrawFocusGraph(); @@ -200,6 +217,8 @@ namespace NanoBrain { DrawFullGraph(); } + #region Full Graph + protected void DrawFullGraph() { //Dag dag = GenerateGraph(this.prefab); Dag dag = GenerateGraph(this.selectedOutput); @@ -219,6 +238,31 @@ namespace NanoBrain { // Draw nodes foreach (DagNode n in dag.nodes) DrawNucleus(n.nucleus, n.position, 1, n.radius); + + // Determine graph width + float width = 0; + float currentNucleusPosition = 0; + foreach (DagNode 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) { @@ -258,6 +302,10 @@ namespace NanoBrain { } } + #endregion Full Graph + + #region Focus Graph + protected void DrawFocusGraph() { float size = 20; Vector3 position = new(150, 210, 0); @@ -334,6 +382,7 @@ namespace NanoBrain { maxValue = cluster.defaultOutput.outputMagnitude; DrawNucleus(this.currentNucleus, position, maxValue, 20); } + graphContainer.style.width = 300; } private void DrawReceivers(Nucleus nucleus, Vector3 parentPos, float size) { @@ -455,6 +504,8 @@ namespace NanoBrain { } } + #endregion Focus Graph + protected void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size) { Color color; if (Application.isPlaying) { @@ -647,19 +698,7 @@ namespace NanoBrain { Handles.DrawLine(from, to); } - // protected void DrawNode(Vector2 position, float size) { - // Handles.color = Color.black * 0.9f; - // Handles.DrawSolidDisc(position, Vector3.forward, size); - - // Handles.color = Color.white; - // GUIStyle style = new(EditorStyles.label) { - // alignment = TextAnchor.UpperCenter, - // normal = { textColor = Color.white }, - // fontStyle = FontStyle.Bold, - // }; - // Vector3 labelPos = position - Vector3.down * (size + 10f); // below disc along up axis - // Handles.Label(labelPos, n.title, style); - // } + #endregion Graph void OnSceneGUI(SceneView sceneView) { if (this.gameObject != null) { From c2e4e1b33f92e73c5197b471081a3334e45405ee Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Tue, 21 Apr 2026 12:08:37 +0200 Subject: [PATCH 20/34] Fix Cluster array extension --- Editor/ClusterInspector.cs | 7 ++- Editor/ClusterViewer.cs | 83 +++++++++++++++++---------------- Runtime/Scripts/Core/Cluster.cs | 12 +++-- 3 files changed, 56 insertions(+), 46 deletions(-) diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterInspector.cs index 3f8e52e..30f7605 100644 --- a/Editor/ClusterInspector.cs +++ b/Editor/ClusterInspector.cs @@ -197,9 +197,9 @@ namespace NanoBrain { if (this.currentNucleus is Cluster cluster) { EditorGUILayout.BeginHorizontal(); if (cluster.siblingClusters != null && cluster.siblingClusters.Length > 1) - EditorGUILayout.IntField("Array size", cluster.siblingClusters.Count()); + EditorGUILayout.IntField("Array size", cluster.siblingClusters.Count(), GUILayout.MinWidth(150)); else - EditorGUILayout.IntField("Array size", 1); + EditorGUILayout.IntField("Array size", 1, GUILayout.MinWidth(150)); if (GUILayout.Button("Add")) { Undo.RecordObject(prefabAsset, "Array add " + prefabAsset.name); cluster.AddInstance(this.prefab); @@ -224,10 +224,13 @@ namespace NanoBrain { } EditorGUIUtility.wideMode = true; + float previousLabelWidth = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth = 100; + Vector3 newBias = EditorGUILayout.Vector3Field("Bias", this.currentNucleus.bias); anythingChanged |= newBias != this.currentNucleus.bias; this.currentNucleus.bias = newBias; + EditorGUIUtility.labelWidth = previousLabelWidth; Nucleus[] array = null; int elementIx = -1; diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index 50f5fdd..e34e63f 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -189,11 +189,9 @@ namespace NanoBrain { if (nucleus == null) return; layer.neuroids.Add(nucleus); - //nucleus.layerIx = layer.ix; // Store its position Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1); neuroidPositions[nucleus] = neuroidPosition; - } public void OnIMGUI() { @@ -224,9 +222,9 @@ namespace NanoBrain { Dag dag = GenerateGraph(this.selectedOutput); Dag.ComputeLayout(dag); // Draw edges - foreach (DagEdge e in dag.edges) { - DagNode from = dag.nodes.FirstOrDefault(x => x.id == e.fromId); - DagNode to = dag.nodes.FirstOrDefault(x => x.id == e.toId); + 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; @@ -236,13 +234,13 @@ namespace NanoBrain { } // Draw nodes - foreach (DagNode n in dag.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 (DagNode node in dag.nodes) { + foreach (Dag.Node node in dag.nodes) { if (node.position.x > width) width = node.position.x; if (node.nucleus == currentNucleus) @@ -271,7 +269,7 @@ namespace NanoBrain { return dag; int ix = 0; - DagNode receiver = new() { + Dag.Node receiver = new() { id = ix, //title = nucleus.name, nucleus = rootNucleus @@ -282,9 +280,9 @@ namespace NanoBrain { return dag; } - private void DescendGraph(DagNode receiver, ref int ix, Dag dag) { + private void DescendGraph(Dag.Node receiver, ref int ix, Dag dag) { foreach (Synapse synapse in receiver.nucleus.synapses) { - DagNode synapseNode = dag.FindNode(synapse.neuron.name); + Dag.Node synapseNode = dag.FindNode(synapse.neuron.name); if (synapseNode == null) { synapseNode = new() { id = ix, @@ -292,7 +290,7 @@ namespace NanoBrain { }; dag.nodes.Add(synapseNode); } - DagEdge edge = new() { + Dag.Edge edge = new() { fromId = synapseNode.id, toId = receiver.id }; @@ -480,6 +478,11 @@ namespace NanoBrain { // continue; // drawnArrays.Add(clusterReceptor.nucleiArray); // } + if (synapse.neuron.parent is Cluster cluster) { + if (drawnArrays.Contains(cluster.siblingClusters)) + continue; + drawnArrays.Add(cluster.siblingClusters); + } Vector3 pos = new(250, margin + row * spacing, 0.0f); Handles.color = Color.white; Handles.DrawLine(parentPos, pos); @@ -679,13 +682,13 @@ namespace NanoBrain { else expandArray = false; } - // else if (nucleus.parent != this.currentNucleus.parent) { - // // We go to a different cluster - // // select the cluster, not the neuron in the cluster - // this.currentNucleus = nucleus.parent; - // expandArray = false; - // BuildLayers(); - // } + else if (nucleus.parent != this.currentNucleus.parent) { + // We go to a different cluster + // select the cluster, not the neuron in the cluster + this.currentNucleus = nucleus.parent; + expandArray = false; + BuildLayers(); + } else { this.currentNucleus = nucleus; expandArray = false; @@ -729,37 +732,37 @@ namespace NanoBrain { public List neuroids = new(); } - [System.Serializable] - public class DagNode { - public int id; - public Vector2 position; - public float radius = 20f; // circle radius - public Nucleus nucleus; - } - - [System.Serializable] - public class DagEdge { - public int fromId; - public int toId; - } public class Dag { - public List nodes = new(); - public List edges = new(); - public DagNode FindNode(string name) { - foreach (DagNode node in this.nodes) { + public class Node { + public int id; + public Vector2 position; + public float radius = 20f; // circle radius + public Nucleus nucleus; + } + + public class Edge { + public int fromId; + public int toId; + } + + public List nodes = new(); + public List edges = new(); + + public Node FindNode(string name) { + foreach (Node node in this.nodes) { if (node.nucleus.name == name) return node; } return null; } - public static DagNode GetNodeById(Dag dag, int id) => dag.nodes.FirstOrDefault(x => x.id == id); + public static Node GetNodeById(Dag dag, int id) => dag.nodes.FirstOrDefault(x => x.id == id); public static void ComputeLayout(Dag dag) { Dictionary> adjacency = dag.nodes.ToDictionary(n => n.id, n => new List()); Dictionary outdegree = dag.nodes.ToDictionary(node => node.id, n => 0); - foreach (DagEdge edge in dag.edges) { + foreach (Edge edge in dag.edges) { if (!adjacency.ContainsKey(edge.fromId) || !adjacency.ContainsKey(edge.toId)) continue; adjacency[edge.fromId].Add(edge.toId); @@ -771,7 +774,7 @@ namespace NanoBrain { Dictionary> parents = dag.nodes.ToDictionary(n => n.id, _ => new List()); Dictionary childCount = dag.nodes.ToDictionary(n => n.id, _ => 0); - foreach (DagEdge edge in dag.edges) { + foreach (Edge edge in dag.edges) { if (!adjacency.ContainsKey(edge.fromId) || !adjacency.ContainsKey(edge.toId)) continue; adjacency[edge.fromId].Add(edge.toId); parents[edge.toId].Add(edge.fromId); // parent of 'to' is 'from' @@ -798,7 +801,7 @@ namespace NanoBrain { // Any unreachable nodes -> assign next layers int maxLayer = layer.Count > 0 ? layer.Values.Max() : 0; - foreach (DagNode node in dag.nodes) { + foreach (Node node in dag.nodes) { if (!layer.ContainsKey(node.id)) { maxLayer++; layer[node.id] = maxLayer; @@ -846,7 +849,7 @@ namespace NanoBrain { float margin = 10 + spacing / 2; for (int i = 0; i < nodeList.Count; i++) { int index = nodeList[i]; - DagNode node = GetNodeById(dag, index); + Node node = GetNodeById(dag, index); if (node == null) continue; float x = hSpacing + layerIx * hSpacing; diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index 2fb58b2..2482247 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -281,7 +281,8 @@ namespace NanoBrain { public void AddInstance(ClusterPrefab prefab) { // Ensure siblingClusters exists - this.siblingClusters ??= new Cluster[1] { this }; + if (this.siblingClusters == null || this.siblingClusters.Length == 0) + this.siblingClusters = new Cluster[1] { this }; // Prepare the new array int newLength = this.siblingClusters.Length + 1; @@ -299,7 +300,7 @@ namespace NanoBrain { newSiblings[newLength - 1] = newCluster; // All siblingClusters need to user this array! - foreach (Cluster sibling in this.siblingClusters) + foreach (Cluster sibling in newSiblings) sibling.siblingClusters = newSiblings; } @@ -349,7 +350,7 @@ namespace NanoBrain { if (this.siblingClusters[ix].defaultOutput.lastUpdate < unusedCluster.defaultOutput.lastUpdate) unusedCluster = this.siblingClusters[ix]; } - + RemoveThingCluster(unusedCluster); return unusedCluster; } @@ -534,7 +535,10 @@ namespace NanoBrain { public virtual List CollectReceivers() { List receivers = new(); - foreach (Neuron output in this.outputs) { + foreach (Nucleus outputNucleus in this.clusterNuclei) { + if (outputNucleus is not Neuron output) + continue; + foreach (Nucleus receiver in output.receivers) { // Only add receivers outside this cluster if (receiver.clusterPrefab != this.prefab) From c708f4da9e257c21064d9ed15193479cf64827ed Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Tue, 21 Apr 2026 12:47:29 +0200 Subject: [PATCH 21/34] Improved clusterarray support --- Editor/ClusterViewer.cs | 41 +++++++++++++-------------- Runtime/Scripts/Core/Cluster.cs | 49 +++++++++++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 23 deletions(-) diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index e34e63f..925cfb0 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -58,7 +58,6 @@ namespace NanoBrain { outputsPopup.RegisterValueChangedCallback(evt => OnOutputChanged(evt.newValue)); topMenuContainer.Add(outputsPopup); } - Add(topMenuContainer); scrollView = new(ScrollViewMode.Horizontal); scrollView.style.position = Position.Absolute; @@ -80,6 +79,7 @@ namespace NanoBrain { scrollView.contentContainer.Add(graphContainer); Add(scrollView); + Add(topMenuContainer); // Subscribe when added to panel (editor UI ready) @@ -437,17 +437,21 @@ namespace NanoBrain { // This is used to 'scale' the output value colors of the nuclei float maxValue = 0; int neuronCount = 0; - List drawnArrays = new(); + //List drawnArrays = new(); + Cluster[] drawnCluster = null; foreach (Synapse synapse in nucleus.synapses) { if (synapse.neuron == null) continue; if (synapse.neuron.parent is Cluster cluster && - cluster.siblingClusters != null && + //cluster.siblingClusters != null && synapse.neuron.parent != nucleus.parent) { - if (drawnArrays.Contains(cluster.siblingClusters)) + + //if (drawnArrays.Contains(cluster.siblingClusters)) + if (drawnCluster is not null && cluster.SameSiblingsAs(drawnCluster)) continue; - drawnArrays.Add(cluster.siblingClusters); + //drawnArrays.Add(cluster.siblingClusters); + drawnCluster = cluster.siblingClusters; } if (synapse.neuron is Neuron synapseNeuron) { float value = synapseNeuron.outputMagnitude * synapse.weight; @@ -463,25 +467,22 @@ namespace NanoBrain { float margin = 10 + spacing / 2; int row = 0; - drawnArrays = new(); + //drawnArrays = new(); + drawnCluster = null; foreach (Synapse synapse in nucleus.synapses) { if (synapse.neuron is null) continue; - // if (synapse.neuron is Receptor neuron) { - // if (drawnArrays.Contains(neuron.nucleiArray)) - // continue; - // drawnArrays.Add(neuron.nucleiArray); - // } - // else if (synapse.neuron.parent is ClusterReceptor clusterReceptor) { - // if (drawnArrays.Contains(clusterReceptor.nucleiArray)) - // continue; - // drawnArrays.Add(clusterReceptor.nucleiArray); - // } - if (synapse.neuron.parent is Cluster cluster) { - if (drawnArrays.Contains(cluster.siblingClusters)) + if (synapse.neuron.parent is Cluster cluster && + //cluster.siblingClusters != null && + synapse.neuron.parent != nucleus.parent) { + + // if (drawnArrays.Contains(cluster.siblingClusters)) + // continue; + // drawnArrays.Add(cluster.siblingClusters); + if (drawnCluster is not null && cluster.SameSiblingsAs(drawnCluster)) continue; - drawnArrays.Add(cluster.siblingClusters); + drawnCluster = cluster.siblingClusters; } Vector3 pos = new(250, margin + row * spacing, 0.0f); Handles.color = Color.white; @@ -547,7 +548,7 @@ namespace NanoBrain { fontStyle = FontStyle.Bold, }; - if (nucleus.parent is Cluster parentCluster) { + if (nucleus.parent is Cluster parentCluster && parentCluster != currentNucleus.parent) { if (expandArray) { // Put array indices above elements style.alignment = TextAnchor.LowerCenter; diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index 2482247..4159793 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -28,8 +28,6 @@ namespace NanoBrain { } } - //[SerializeReference] - //public ClusterArray clusterArray; [SerializeReference] public Cluster[] siblingClusters; public Dictionary thingClusters = new(); @@ -117,6 +115,44 @@ namespace NanoBrain { } } + // Copy the siblings for clusters + for (int nucleusIx = 0; nucleusIx < prefabNuclei.Length; nucleusIx++) { + Nucleus prefabNucleus = prefabNuclei[nucleusIx]; + if (prefabNucleus is not Cluster prefabCluster) + continue; + + if (prefabCluster.siblingClusters == null || prefabCluster.siblingClusters.Length == 0) + continue; + + Cluster clonedNucleus = clonedNuclei[nucleusIx] as Cluster; + if (prefabCluster == prefabCluster.siblingClusters[0]) { + // We clone the array only for the first entry + //NucleusArray clonedArray = new(prefabReceptor.nucleiArray.Length); + Cluster[] clonedArray = new Cluster[prefabCluster.siblingClusters.Length]; + int arrayIx = 0; + foreach (Cluster prefabArrayNucleus in prefabCluster.siblingClusters) { + int arrayNucleusIx = GetNucleusIndex(prefabNuclei, prefabArrayNucleus); + if (arrayNucleusIx >= 0) { + Cluster clonedArrayNucleus = clonedNuclei[arrayNucleusIx] as Cluster; + clonedArray[arrayIx] = clonedArrayNucleus; + } + else { + Debug.LogError($" Could not find prefab nucleus {prefabNucleus.name} in the clones"); + } + arrayIx++; + } + //clonedNucleus.array = clonedArray; + clonedNucleus.siblingClusters = clonedArray; + } + else { + // The others will refer to the array created for the first nucleus in the array + int firstNucleusIx = GetNucleusIndex(prefabNuclei, prefabCluster.siblingClusters[0]); + Cluster clonedFirstNucleus = clonedNuclei[firstNucleusIx] as Cluster; + clonedNucleus.siblingClusters = clonedFirstNucleus.siblingClusters; + } + } + + foreach (Nucleus nucleus in this.clusterNuclei) { if (nucleus is Cluster clonedSubCluster) RestoreAllExternalReceivers(clonedSubCluster, this.prefab, this); @@ -278,7 +314,6 @@ namespace NanoBrain { #region Cluster Array - public void AddInstance(ClusterPrefab prefab) { // Ensure siblingClusters exists if (this.siblingClusters == null || this.siblingClusters.Length == 0) @@ -366,6 +401,14 @@ namespace NanoBrain { thingClusters.Remove(thingId); } + public bool SameSiblingsAs(Cluster[] otherSiblingClusters) { + for (int ix = 0; ix < this.siblingClusters.Length; ix++) { + if (this.siblingClusters[ix] != otherSiblingClusters[ix]) + return false; + } + return true; + } + #endregion ClusterArray public ClusterPrefab prefab; From 1a1919fb8e476cc3c138a75a841c5702a4cae304 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Tue, 21 Apr 2026 14:16:12 +0200 Subject: [PATCH 22/34] Fix expansion of clsuter arrays --- Editor/ClusterViewer.cs | 108 +++++++++++++++++++--------------------- 1 file changed, 51 insertions(+), 57 deletions(-) diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index 925cfb0..1ae3662 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -313,64 +313,54 @@ namespace NanoBrain { // Draw selected Nucleus if (expandArray) { - // if (this.currentNucleus is IReceptor receptor1) { - // float maxValue = 0; - // foreach (Nucleus nucleus in receptor1.nucleiArray) { - // if (nucleus is Neuron neuron) { - // float value = neuron.outputMagnitude; - // if (value > maxValue) - // maxValue = value; - // } - // } - - // float spacing = 400f / receptor1.nucleiArray.Count(); - // float margin = 10 + spacing / 2; - // float xMin = 150 - size; - // float xMax = 150 + size; - // float yMin = 10 + margin - size / 2; - // float yMax = 400 - margin + size; - // Vector3[] verts = new Vector3[4] { - // new(xMin, yMin, 0), - // new(xMax, yMin, 0), - // new(xMax, yMax, 0), - // new(xMin, yMax, 0) - // }; - // Handles.color = Color.black; - // Handles.DrawAAConvexPolygon(verts); - // int row = 0; - // foreach (Nucleus nucleus in receptor1.nucleiArray) { - // Vector3 pos = new(150, margin + row * spacing, 0.0f); - // Handles.color = Color.white; - // // The selected nucleus highlight ring - // Handles.DrawSolidDisc(pos, Vector3.forward, size + 2); - // DrawNucleus(nucleus, pos, maxValue, size); - // row++; - // } - // GUIStyle style = new(EditorStyles.label) { - // alignment = TextAnchor.UpperCenter, - // normal = { textColor = Color.white }, - // fontStyle = FontStyle.Bold, - // }; - // Vector3 labelPos = new(150, yMax + size + 5, 0); - // string receptorName = receptor1.GetName(); - // int colonPos = receptorName.IndexOf(":"); - // if (colonPos > 0) { - // string baseName = receptorName[..colonPos]; - // Handles.Label(labelPos, baseName, style); - // } - // else - // Handles.Label(labelPos, receptorName, style); - // } - // 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); + if (this.currentNucleus is Cluster cluster) { + float spacing = 400f / cluster.siblingClusters.Length; + float margin = 10 + spacing / 2; + float xMin = 150 - size; + float xMax = 150 + size; + float yMin = 10 + margin - size / 2; + float yMax = 400 - margin + size; + Vector3[] verts = new Vector3[4] { + new(xMin, yMin, 0), + new(xMax, yMin, 0), + new(xMax, yMax, 0), + new(xMin, yMax, 0) + }; + Handles.color = Color.black; + Handles.DrawAAConvexPolygon(verts); + int row = 0; + foreach (Cluster sibling in cluster.siblingClusters) { + 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; @@ -473,7 +463,7 @@ namespace NanoBrain { if (synapse.neuron is null) continue; - if (synapse.neuron.parent is Cluster cluster && + if (synapse.neuron.parent is Cluster cluster && //cluster.siblingClusters != null && synapse.neuron.parent != nucleus.parent) { @@ -558,6 +548,8 @@ namespace NanoBrain { string extName = nucleus.name[(colonPos1 + 2)..]; Handles.Label(labelPos1, extName, style); } + else + Handles.Label(labelPos1, "0", style); } else { if (parentCluster.siblingClusters != null && parentCluster.siblingClusters.Length > 1) { @@ -581,6 +573,8 @@ namespace NanoBrain { string extName = nucleus.name[(colonPos1 + 2)..]; Handles.Label(labelPos1, extName, style); } + else + Handles.Label(labelPos1, "0", style); } else { if (cluster.siblingClusters != null && cluster.siblingClusters.Length > 1) { @@ -595,7 +589,7 @@ namespace NanoBrain { } } - if (expandArray == false) { + if (expandArray == false || nucleus != currentNucleus) { // put name below nucleus Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron style.alignment = TextAnchor.UpperCenter; From befb69d00cb841dbf1c41f41d088f5c4eeb565d0 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Tue, 21 Apr 2026 14:25:25 +0200 Subject: [PATCH 23/34] full graph with collapsed clusters --- Editor/ClusterViewer.cs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index 1ae3662..0940fc3 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -282,11 +282,16 @@ namespace NanoBrain { private void DescendGraph(Dag.Node receiver, ref int ix, Dag dag) { foreach (Synapse synapse in receiver.nucleus.synapses) { - Dag.Node synapseNode = dag.FindNode(synapse.neuron.name); + 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 = synapse.neuron + nucleus = nucleus }; dag.nodes.Add(synapseNode); } @@ -744,9 +749,20 @@ namespace NanoBrain { public List nodes = new(); public List edges = new(); - public Node FindNode(string name) { + public Node FindNode(string name, bool justBaseName = true) { + if (justBaseName) { + int colonPos = name.IndexOf(":"); + if (colonPos > 0) + name = name[..colonPos]; + } foreach (Node node in this.nodes) { - if (node.nucleus.name == name) + string nodeName = node.nucleus.name; + if (justBaseName) { + int colonPos = nodeName.IndexOf(":"); + if (colonPos > 0) + nodeName = nodeName[..colonPos]; + } + if (nodeName == name) return node; } return null; From 8801fa2ff2b96de74772e5e2bd5faab21502567f Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Tue, 21 Apr 2026 15:25:18 +0200 Subject: [PATCH 24/34] Cluster reimport fixes --- Editor/ClusterInspector.cs | 21 ++++++++++++++++++--- Runtime/Scripts/Core/Cluster.cs | 10 ++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterInspector.cs index 30f7605..f32357e 100644 --- a/Editor/ClusterInspector.cs +++ b/Editor/ClusterInspector.cs @@ -439,9 +439,24 @@ namespace NanoBrain { } private void ReimportCluster(Cluster subCluster) { - Cluster reimportedCluster = new(subCluster.prefab, this.prefab); - subCluster.MoveReceivers(reimportedCluster); - // subcluster should be garbage now... + if (subCluster.siblingClusters == null || subCluster.siblingClusters.Length <= 0) { + Cluster reimportedCluster = new(subCluster.prefab, this.prefab); + subCluster.MoveReceivers(reimportedCluster); + // subcluster should be garbage now... + } + else { + List newSiblingsList = new(); + foreach (Cluster sibling in subCluster.siblingClusters) { + Cluster reimportedCluster = new(sibling.prefab, this.prefab) { + name = sibling.name + }; + sibling.MoveReceivers(reimportedCluster); + newSiblingsList.Add(reimportedCluster); + } + Cluster[] newSiblings = newSiblingsList.ToArray(); + foreach (Cluster sibling in newSiblings) + sibling.siblingClusters = newSiblings; + } } int selectedConnectNucleus = -1; diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index 4159793..d6ef274 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -402,6 +402,8 @@ namespace NanoBrain { } public bool SameSiblingsAs(Cluster[] otherSiblingClusters) { + if (this.siblingClusters == null) + return false; for (int ix = 0; ix < this.siblingClusters.Length; ix++) { if (this.siblingClusters[ix] != otherSiblingClusters[ix]) return false; @@ -592,23 +594,23 @@ namespace NanoBrain { } public void MoveReceivers(Cluster newCluster) { + Debug.Log($"Move receivers for {this.name} to {newCluster.name}"); foreach (Nucleus outputNucleus in this.clusterNuclei) { if (outputNucleus is not Neuron output) continue; // Find the existing output in the new cluster if (newCluster.GetNucleus(output.name) is not Neuron newOutput) { - Debug.LogWarning("Could not find output {output.name} in {newCluster.name}"); + Debug.LogWarning($"Could not find output {this.name}.{output.name} in {newCluster.name}"); continue; } - Debug.Log($"Check {output.name} receivers"); + Debug.Log($"Check {this.name}.{output.name} receivers"); Nucleus[] receivers = output.receivers.ToArray(); foreach (Nucleus receiver in receivers) { - Debug.Log("."); if (receiver.clusterPrefab != this.prefab) { // Replace synapse with new synapse // to the new cluster - Debug.Log($"move {receiver.name} from {output.name} to {newOutput.name}"); + Debug.Log($"move {receiver.name} from {this.name}.{output.name} to {newCluster.name}.{newOutput.name}"); Synapse synapse = receiver.GetSynapse(output); newOutput.AddReceiver(receiver, synapse.weight); output.RemoveReceiver(receiver); From b6630ad84eed6c7cfa8df9ff89e6bd626183a718 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Tue, 21 Apr 2026 17:13:56 +0200 Subject: [PATCH 25/34] First steps to using instanceCount for clusters --- Editor/ClusterInspector.cs | 100 ++++++++++-------- Editor/ClusterViewer.cs | 24 ++++- Runtime/Scripts/Core/Cluster.cs | 99 +++++++++++++++-- Runtime/Scripts/Core/Nucleus.cs | 4 + .../ScriptableObjects/ClusterPrefab.cs | 1 + 5 files changed, 167 insertions(+), 61 deletions(-) diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterInspector.cs index f32357e..a6668b9 100644 --- a/Editor/ClusterInspector.cs +++ b/Editor/ClusterInspector.cs @@ -196,13 +196,16 @@ namespace NanoBrain { if (this.currentNucleus is Cluster cluster) { EditorGUILayout.BeginHorizontal(); - if (cluster.siblingClusters != null && cluster.siblingClusters.Length > 1) + if (cluster.instanceCount > 1) + EditorGUILayout.IntField("Array size", cluster.instanceCount, GUILayout.MinWidth(150)); + else if (cluster.siblingClusters != null && cluster.siblingClusters.Length > 1) EditorGUILayout.IntField("Array size", cluster.siblingClusters.Count(), GUILayout.MinWidth(150)); else EditorGUILayout.IntField("Array size", 1, GUILayout.MinWidth(150)); if (GUILayout.Button("Add")) { Undo.RecordObject(prefabAsset, "Array add " + prefabAsset.name); - cluster.AddInstance(this.prefab); + //cluster.AddInstance(this.prefab); + cluster.AddInstance(); anythingChanged = true; } if (GUILayout.Button("Del")) { @@ -213,8 +216,36 @@ namespace NanoBrain { EditorGUILayout.EndHorizontal(); } - // Synapses + SynapsesInspector(ref anythingChanged); + ActivationInspector(ref anythingChanged); + if (GUILayout.Button("Delete this neuron")) + DeleteNucleus(this.currentNucleus); + + if (this.currentNucleus is Cluster subCluster) { + if (GUILayout.Button("Reimport Cluster")) + ReimportCluster(subCluster); + if (GUILayout.Button("Edit Cluster")) + EditCluster(subCluster); + } + + EditorGUILayout.Space(); + breakOnWake = EditorGUILayout.Toggle("Break on wake", breakOnWake); + if (breakOnWake && this.currentNucleus is Neuron currentNeuron) { + if (currentNeuron.isSleeping == false) + Debug.Break(); + } + trace = EditorGUILayout.Toggle("Trace", trace); + this.currentNucleus.trace = trace; + + serializedObject.ApplyModifiedProperties(); + if (anythingChanged) { + EditorUtility.SetDirty(prefabAsset); + AssetDatabase.SaveAssets(); + } + } + + protected void SynapsesInspector(ref bool anythingChanged) { showSynapses = EditorGUILayout.BeginFoldoutHeaderGroup(showSynapses, "Synapses"); if (showSynapses) { if (this.currentNucleus is Neuron neuron2) { @@ -248,19 +279,17 @@ namespace NanoBrain { else elementIx = thisElementIx; } - // if (array.Contains(synapse.nucleus)) - // continue; + if (array.Contains(synapse.neuron)) + continue; else if (array.Contains(synapse.neuron.parent)) continue; } else { - // if (synapse.neuron.parent is IReceptor iReceptor) { - // array = iReceptor.nucleiArray; - // if (iReceptor is Cluster iCluster) - // elementIx = Cluster.GetNucleusIndex(iCluster.clusterNuclei, synapse.neuron); - // } - // else if (synapse.nucleus is Receptor receptor2) // && receptor2.array != null && receptor2.array.nuclei.Length > 1) - // array = receptor2.nucleiArray; + if (synapse.neuron.parent is Cluster iReceptor) { + array = iReceptor.siblingClusters; + if (iReceptor is Cluster iCluster) + elementIx = Cluster.GetNucleusIndex(iCluster.clusterNuclei, synapse.neuron); + } } EditorGUILayout.Space(); @@ -325,8 +354,9 @@ namespace NanoBrain { anythingChanged |= AddSynapse(this.prefab, this.currentNucleus); } EditorGUILayout.EndFoldoutHeaderGroup(); + } - // Activation + protected void ActivationInspector(ref bool anythingChanged) { if (this.currentNucleus is not Cluster) { EditorGUILayout.Space(); @@ -356,30 +386,6 @@ namespace NanoBrain { EditorGUILayout.EndFoldoutHeaderGroup(); } - if (GUILayout.Button("Delete this neuron")) - DeleteNucleus(this.currentNucleus); - - if (this.currentNucleus is Cluster subCluster) { - if (GUILayout.Button("Reimport Cluster")) - ReimportCluster(subCluster); - if (GUILayout.Button("Edit Cluster")) - EditCluster(subCluster); - } - - EditorGUILayout.Space(); - breakOnWake = EditorGUILayout.Toggle("Break on wake", breakOnWake); - if (breakOnWake && this.currentNucleus is Neuron currentNeuron) { - if (currentNeuron.isSleeping == false) - Debug.Break(); - } - trace = EditorGUILayout.Toggle("Trace", trace); - this.currentNucleus.trace = trace; - - serializedObject.ApplyModifiedProperties(); - if (anythingChanged) { - EditorUtility.SetDirty(prefabAsset); - AssetDatabase.SaveAssets(); - } } #region Synapses @@ -426,8 +432,8 @@ namespace NanoBrain { protected virtual void AddClusterInput(Nucleus nucleus) { ClusterPickerWindow.ShowPicker(brain => OnClusterPicked(nucleus, brain), "Select Cluster"); } - private void OnClusterPicked(Nucleus nucleus, ClusterPrefab prefab) { - Cluster subclusterInstance = new(prefab, this.prefab); + private void OnClusterPicked(Nucleus nucleus, ClusterPrefab selectedPrefab) { + Cluster subclusterInstance = new(selectedPrefab, this.prefab); subclusterInstance.defaultOutput.AddReceiver(nucleus); } @@ -443,8 +449,10 @@ namespace NanoBrain { Cluster reimportedCluster = new(subCluster.prefab, this.prefab); subCluster.MoveReceivers(reimportedCluster); // subcluster should be garbage now... + this.currentNucleus = reimportedCluster; } else { + this.currentNucleus = null; List newSiblingsList = new(); foreach (Cluster sibling in subCluster.siblingClusters) { Cluster reimportedCluster = new(sibling.prefab, this.prefab) { @@ -452,6 +460,8 @@ namespace NanoBrain { }; sibling.MoveReceivers(reimportedCluster); newSiblingsList.Add(reimportedCluster); + // make the first reimportedCluster the new current nucleus + this.currentNucleus ??= reimportedCluster; } Cluster[] newSiblings = newSiblingsList.ToArray(); foreach (Cluster sibling in newSiblings) @@ -460,7 +470,7 @@ namespace NanoBrain { } int selectedConnectNucleus = -1; - // Connect to another nucleus in the same cluster + // Connect to another nucleus protected virtual bool ConnectNucleus(ClusterPrefab cluster, Nucleus nucleusToConnect) { if (cluster == null) return false; @@ -485,14 +495,10 @@ namespace NanoBrain { EditorGUILayout.EndHorizontal(); if (connecting) { Nucleus nucleus = nuclei.ElementAt(selectedConnectNucleus); - // if (nucleus is IReceptor receptor) - // receptor.AddArrayReceiver(this.currentNucleus); - // else - if (nucleus is Neuron neuron) + if (nucleus is Cluster subCluster) + subCluster.AddArrayReceiver(this.currentNucleus); + else if (nucleus is Neuron neuron) neuron.AddReceiver(this.currentNucleus); - else if (nucleus is Cluster subCluster) - subCluster.defaultOutput.AddReceiver(this.currentNucleus); - } return connecting; } diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index 0940fc3..34a9c11 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -557,8 +557,16 @@ namespace NanoBrain { Handles.Label(labelPos1, "0", style); } else { - if (parentCluster.siblingClusters != null && parentCluster.siblingClusters.Length > 1) { - // draw the array size label + // draw the array size label + if (parentCluster.instanceCount > 1) { + if (color.grayscale > 0.5f) + style.normal.textColor = Color.black; + else + style.normal.textColor = Color.white; + Handles.Label(labelPosition, parentCluster.instanceCount.ToString(), style); + style.normal.textColor = Color.white; + } + else if (parentCluster.siblingClusters != null && parentCluster.siblingClusters.Length > 1) { if (color.grayscale > 0.5f) style.normal.textColor = Color.black; else @@ -582,8 +590,16 @@ namespace NanoBrain { Handles.Label(labelPos1, "0", style); } else { - if (cluster.siblingClusters != null && cluster.siblingClusters.Length > 1) { - // draw the array size label + // draw the array size label + if (cluster.instanceCount > 1) { + if (color.grayscale > 0.5f) + style.normal.textColor = Color.black; + else + style.normal.textColor = Color.white; + Handles.Label(labelPosition, cluster.instanceCount.ToString(), style); + style.normal.textColor = Color.white; + } + else if (cluster.siblingClusters != null && cluster.siblingClusters.Length > 1) { if (color.grayscale > 0.5f) style.normal.textColor = Color.black; else diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index d6ef274..f58f539 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -15,6 +15,7 @@ namespace NanoBrain { /// Clusters can be nested inside other clusters. [Serializable] public class Cluster : Nucleus { + // It may be that clusters will not be nuclei anymore in the future.... /// /// The base name of the cluster. I don't think this is actively used at this moment @@ -28,8 +29,11 @@ namespace NanoBrain { } } + // This should not be serialized [SerializeReference] public Cluster[] siblingClusters; + // This serialization should be enough + public int instanceCount = 1; public Dictionary thingClusters = new(); #region Init @@ -141,7 +145,6 @@ namespace NanoBrain { } arrayIx++; } - //clonedNucleus.array = clonedArray; clonedNucleus.siblingClusters = clonedArray; } else { @@ -152,6 +155,50 @@ namespace NanoBrain { } } + // Collect the subclusters + List subClusters = new(); + foreach (Nucleus nucleus in prefabNuclei) { + foreach (Synapse synapse in nucleus.synapses) { + Nucleus synapseNucleus = synapse.neuron; + if (synapseNucleus is not Cluster subCluster) + continue; + if (subClusters.Contains(subCluster)) + continue; + subClusters.Add(subCluster); + } + } + // Create the subcluster instances + foreach (Cluster subCluster in subClusters) { + for (int ix = 0; ix < subCluster.instanceCount; ix++) { + // create the new instance + Cluster clusterInstance = new(subCluster.prefab); + // connect it + foreach ((Neuron sender, Nucleus receiver) in subCluster.CollectConnections()) { + int receiverIx = GetNucleusIndex(prefabNuclei, receiver); + if (receiverIx < 0) + continue; + + if (clonedNuclei[receiverIx] is not Nucleus clonedReceiver) + continue; + + // Find the synapse for the weight + float weight = 1; + foreach (Synapse synapse in receiver.synapses) { + // Find the weight for this synapse + if (synapse.neuron == sender) { + weight = synapse.weight; + break; + } + } + + if (clusterInstance.GetNucleus(sender.name) is not Neuron clonedSender) + continue; + + clonedSender.AddReceiver(clonedReceiver, weight); + } + } + } + foreach (Nucleus nucleus in this.clusterNuclei) { if (nucleus is Cluster clonedSubCluster) @@ -245,6 +292,7 @@ namespace NanoBrain { } public override Nucleus ShallowCloneTo(Cluster parent) { + // Clusters should not be cloned, but instantiated from the prefab.... Cluster clone = new(this.prefab, parent) { name = this.name, clusterPrefab = this.clusterPrefab, @@ -314,6 +362,10 @@ namespace NanoBrain { #region Cluster Array + public void AddInstance() { + this.instanceCount++; + } + public void AddInstance(ClusterPrefab prefab) { // Ensure siblingClusters exists if (this.siblingClusters == null || this.siblingClusters.Length == 0) @@ -340,18 +392,22 @@ namespace NanoBrain { } public void RemoveInstance() { - if (this.siblingClusters == null || this.siblingClusters.Length <= 1) - return; + if (instanceCount > 1) + instanceCount--; + else { + if (this.siblingClusters == null || this.siblingClusters.Length <= 1) + return; - // Prepare the new array - int newLength = this.siblingClusters.Length - 1; - Cluster[] newClusters = new Cluster[newLength]; + // Prepare the new array + int newLength = this.siblingClusters.Length - 1; + Cluster[] newClusters = new Cluster[newLength]; - for (int i = 0; i < newLength; i++) - newClusters[i] = this.siblingClusters[i]; + for (int i = 0; i < newLength; i++) + newClusters[i] = this.siblingClusters[i]; - Neuron.Delete(this.siblingClusters[^1]); - this.siblingClusters = newClusters; + Neuron.Delete(this.siblingClusters[^1]); + this.siblingClusters = newClusters; + } } public virtual Cluster GetThingCluster() { @@ -411,6 +467,13 @@ namespace NanoBrain { return true; } + public void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1) { + foreach (Cluster cluster in this.siblingClusters) { + cluster.defaultOutput.AddReceiver(receiverToAdd, weight); + } + + } + #endregion ClusterArray public ClusterPrefab prefab; @@ -593,6 +656,22 @@ namespace NanoBrain { return receivers; } + public List<(Neuron, Nucleus)> CollectConnections() { + List<(Neuron, Nucleus)> connections = new(); + + foreach (Nucleus outputNucleus in this.clusterNuclei) { + if (outputNucleus is not Neuron output) + continue; + + foreach (Nucleus receiver in output.receivers) { + // Only add receivers outside this cluster + if (receiver.clusterPrefab != this.prefab) + connections.Add((output, receiver)); + } + } + return connections; + } + public void MoveReceivers(Cluster newCluster) { Debug.Log($"Move receivers for {this.name} to {newCluster.name}"); foreach (Nucleus outputNucleus in this.clusterNuclei) { diff --git a/Runtime/Scripts/Core/Nucleus.cs b/Runtime/Scripts/Core/Nucleus.cs index 60352b2..f983a2d 100644 --- a/Runtime/Scripts/Core/Nucleus.cs +++ b/Runtime/Scripts/Core/Nucleus.cs @@ -88,6 +88,10 @@ public abstract class Nucleus { return synapse; } + // public Synapse AddSynapse(ClusterPrefab clusterPrefab, string neuronName, float weight = 1) { + + // } + /// /// Find a synapse /// diff --git a/Runtime/Scripts/ScriptableObjects/ClusterPrefab.cs b/Runtime/Scripts/ScriptableObjects/ClusterPrefab.cs index e50093f..2920733 100644 --- a/Runtime/Scripts/ScriptableObjects/ClusterPrefab.cs +++ b/Runtime/Scripts/ScriptableObjects/ClusterPrefab.cs @@ -10,6 +10,7 @@ namespace NanoBrain { public class ClusterPrefab : ScriptableObject { /// The nuclei in this cluster [SerializeReference] + // This list should not include any clusters... public List nuclei = new(); /// From 0ab2d2102df21d65364ccf91e263154fce525a52 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Tue, 21 Apr 2026 17:40:16 +0200 Subject: [PATCH 26/34] Migrating and cleaning up --- Editor/ClusterInspector.cs | 7 -- Editor/ClusterViewer.cs | 151 +++++++++++++++++-------------------- 2 files changed, 71 insertions(+), 87 deletions(-) diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterInspector.cs index a6668b9..15fe536 100644 --- a/Editor/ClusterInspector.cs +++ b/Editor/ClusterInspector.cs @@ -437,13 +437,6 @@ namespace NanoBrain { subclusterInstance.defaultOutput.AddReceiver(nucleus); } - private void EditCluster(Cluster subCluster) { - // May be used with storedPrefab... - Selection.activeObject = subCluster.prefab; - EditorGUIUtility.PingObject(subCluster.prefab); - _ = CreateEditor(subCluster.prefab); - } - private void ReimportCluster(Cluster subCluster) { if (subCluster.siblingClusters == null || subCluster.siblingClusters.Length <= 0) { Cluster reimportedCluster = new(subCluster.prefab, this.prefab); diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index 34a9c11..b42a41b 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -543,72 +543,10 @@ namespace NanoBrain { fontStyle = FontStyle.Bold, }; - if (nucleus.parent is Cluster parentCluster && parentCluster != currentNucleus.parent) { - if (expandArray) { - // Put array indices above elements - style.alignment = TextAnchor.LowerCenter; - Vector3 labelPos1 = position + Vector3.down * (size + 5); // below disc - int colonPos1 = nucleus.name.IndexOf(":"); - if (colonPos1 > 0) { - string extName = nucleus.name[(colonPos1 + 2)..]; - Handles.Label(labelPos1, extName, style); - } - else - Handles.Label(labelPos1, "0", style); - } - else { - // draw the array size label - if (parentCluster.instanceCount > 1) { - if (color.grayscale > 0.5f) - style.normal.textColor = Color.black; - else - style.normal.textColor = Color.white; - Handles.Label(labelPosition, parentCluster.instanceCount.ToString(), style); - style.normal.textColor = Color.white; - } - else if (parentCluster.siblingClusters != null && parentCluster.siblingClusters.Length > 1) { - if (color.grayscale > 0.5f) - style.normal.textColor = Color.black; - else - style.normal.textColor = Color.white; - Handles.Label(labelPosition, parentCluster.siblingClusters.Length.ToString(), style); - style.normal.textColor = Color.white; - } - } - } - else if (nucleus is Cluster cluster) { - if (expandArray) { - // Put array indices above elements - style.alignment = TextAnchor.LowerCenter; - Vector3 labelPos1 = position + Vector3.down * (size + 5); // below disc - int colonPos1 = nucleus.name.IndexOf(":"); - if (colonPos1 > 0) { - string extName = nucleus.name[(colonPos1 + 2)..]; - Handles.Label(labelPos1, extName, style); - } - else - Handles.Label(labelPos1, "0", style); - } - else { - // draw the array size label - if (cluster.instanceCount > 1) { - if (color.grayscale > 0.5f) - style.normal.textColor = Color.black; - else - style.normal.textColor = Color.white; - Handles.Label(labelPosition, cluster.instanceCount.ToString(), style); - style.normal.textColor = Color.white; - } - else if (cluster.siblingClusters != null && cluster.siblingClusters.Length > 1) { - if (color.grayscale > 0.5f) - style.normal.textColor = Color.black; - else - style.normal.textColor = Color.white; - Handles.Label(labelPosition, cluster.siblingClusters.Length.ToString(), style); - style.normal.textColor = Color.white; - } - } - } + if (nucleus.parent is Cluster parentCluster && 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 @@ -645,12 +583,6 @@ namespace NanoBrain { } } - // Draw Cluster ring - if (nucleus.parent != currentNucleus.parent || nucleus is Cluster) { - Handles.color = Color.white; - Handles.DrawWireDisc(position, Vector3.forward, size + 5); - } - // Tooltip Rect neuronRect = new(position.x - size, position.y - size, size * 2, size * 2); @@ -672,6 +604,57 @@ namespace NanoBrain { } } + private 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.siblingClusters != null && cluster.siblingClusters.Length > 1) { + Handles.Label(labelPosition, cluster.siblingClusters.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 DrawEdge(Vector2 from, Vector2 to) { + Handles.color = Color.white; + Handles.DrawLine(from, to); + } + private void HandleMouseHover(Nucleus nucleus, Rect rect) { GUIContent tooltip; if (nucleus is Neuron neuron) { @@ -691,12 +674,18 @@ namespace NanoBrain { GUI.Box(tooltipRect, tooltip); } - private void HandleClicked(Nucleus nucleus) { + protected void HandleClicked(Nucleus nucleus) { if (nucleus == this.currentNucleus) { - if (nucleus is Cluster) //is Receptor) // || nucleus is ClusterReceptor) - expandArray = !expandArray; - else - expandArray = false; + if (Application.isPlaying) { + if (nucleus is Cluster) + expandArray = !expandArray; + else + expandArray = false; + } + else { + if (nucleus is Cluster cluster) + EditCluster(cluster); + } } else if (nucleus.parent != this.currentNucleus.parent) { // We go to a different cluster @@ -712,9 +701,11 @@ namespace NanoBrain { } } - protected void DrawEdge(Vector2 from, Vector2 to) { - Handles.color = Color.white; - Handles.DrawLine(from, to); + protected void EditCluster(Cluster subCluster) { + // May be used with storedPrefab... + Selection.activeObject = subCluster.prefab; + EditorGUIUtility.PingObject(subCluster.prefab); + _ = CreateEditor(subCluster.prefab); } #endregion Graph From e17a24974314073106fb50ba4792c87367e2376f Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Wed, 22 Apr 2026 11:29:51 +0200 Subject: [PATCH 27/34] Cross-cluster editor links --- Editor/Brain_Editor.cs | 4 +-- Editor/ClusterInspector.cs | 10 +++++-- Editor/ClusterViewer.cs | 56 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/Editor/Brain_Editor.cs b/Editor/Brain_Editor.cs index a0de070..8037d8c 100644 --- a/Editor/Brain_Editor.cs +++ b/Editor/Brain_Editor.cs @@ -11,7 +11,7 @@ namespace NanoBrain { protected static VisualElement mainContainer; protected static VisualElement inspectorContainer; - protected Brain component; + public Brain component; private SerializedProperty brainProp; public void OnEnable() { @@ -55,7 +55,7 @@ namespace NanoBrain { return root; } - public static ClusterViewer.GraphView CreateViewer(VisualElement root, ClusterPrefab cluster, Nucleus output, GameObject gameObject) { + public ClusterViewer.GraphView CreateViewer(VisualElement root, ClusterPrefab cluster, Nucleus output, GameObject gameObject) { VisualElement mainContainer = new() { style = { flexDirection = FlexDirection.Row, diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterInspector.cs index 15fe536..1adbe18 100644 --- a/Editor/ClusterInspector.cs +++ b/Editor/ClusterInspector.cs @@ -9,6 +9,7 @@ namespace NanoBrain { [CustomEditor(typeof(ClusterPrefab))] public class ClusterInspector : ClusterViewer { + public override VisualElement CreateInspectorGUI() { ClusterPrefab prefab = target as ClusterPrefab; if (prefab != null) @@ -23,7 +24,7 @@ namespace NanoBrain { return root; } - public static GraphView CreateInspector(VisualElement root, ClusterPrefab cluster, Nucleus output, GameObject gameObject) { + public GraphView CreateInspector(VisualElement root, ClusterPrefab cluster, Nucleus output, GameObject gameObject) { root.style.paddingLeft = 0; root.style.paddingRight = 0; root.style.paddingTop = 0; @@ -41,6 +42,7 @@ namespace NanoBrain { graphContainer.style.width = 300; graphContainer.style.overflow = Overflow.Hidden; + VisualElement inspectorContainer = new() { name = "inspector", style = { @@ -55,7 +57,7 @@ namespace NanoBrain { mainContainer.Add(inspectorContainer); root.Add(mainContainer); - graphContainer.SetGraph(gameObject, output, inspectorContainer); + graphContainer.SetGraph(gameObject, output, inspectorContainer, this); return graphContainer; } @@ -81,8 +83,10 @@ namespace NanoBrain { this.currentNucleus = newOutput; } - public void SetGraph(GameObject gameObject, Nucleus nucleus, VisualElement inspectorContainer) { + public void SetGraph(GameObject gameObject, Nucleus nucleus, VisualElement inspectorContainer, ClusterInspector editor) { this.gameObject = gameObject; + this.currentEditor = editor; + //this.cluster = brain; if (Application.isPlaying == false) this.serializedBrain = new SerializedObject(this.prefab); diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index b42a41b..2784689 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -9,6 +9,9 @@ namespace NanoBrain { public class ClusterViewer : Editor { + //public static ClusterViewer previousEditor; + public static ClusterPrefab previousPrefab; + public class GraphView : VisualElement { protected readonly ClusterPrefab prefab; protected SerializedObject serializedBrain; @@ -26,6 +29,9 @@ namespace NanoBrain { protected IMGUIContainer graphContainer; protected readonly PopupField outputsPopup; + public ClusterInspector currentEditor; + //public ClusterViewer previousEditor; + public enum Mode { Focus, Full @@ -101,7 +107,6 @@ namespace NanoBrain { this.currentNucleus = this.selectedOutput; } - bool subscribed = false; void Subscribe() { if (subscribed) return; @@ -118,6 +123,7 @@ namespace NanoBrain { public void SetGraph(GameObject gameObject, Nucleus nucleus) { this.gameObject = gameObject; + if (Application.isPlaying == false) this.serializedBrain = new SerializedObject(this.prefab); this.currentNucleus = nucleus; @@ -387,7 +393,11 @@ namespace NanoBrain { else return; - int nodeCount = receivers.Count(); //neuron != null ? neuron.receivers.Count() : 1; + int nodeCount = receivers.Count(); + if (nucleus == this.selectedOutput && 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 @@ -423,6 +433,11 @@ namespace NanoBrain { DrawNucleus(receiverNucleus, pos, maxValue, size); row++; } + if (nucleus == this.selectedOutput && ClusterViewer.previousPrefab != null) { + Vector3 pos = new(50, margin + row * spacing, 0); + DrawEdge(parentPos, pos); + DrawClusterPrefab(ClusterViewer.previousPrefab, pos, size); + } } private void DrawSynapses(Nucleus nucleus, Vector3 parentPos, float size) { @@ -650,6 +665,40 @@ namespace NanoBrain { 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 DrawEdge(Vector2 from, Vector2 to) { Handles.color = Color.white; Handles.DrawLine(from, to); @@ -705,7 +754,8 @@ namespace NanoBrain { // May be used with storedPrefab... Selection.activeObject = subCluster.prefab; EditorGUIUtility.PingObject(subCluster.prefab); - _ = CreateEditor(subCluster.prefab); + ClusterViewer.previousPrefab = this.prefab; + ClusterInspector newEditor = CreateEditor(subCluster.prefab) as ClusterInspector; } #endregion Graph From 04bab9264faa6844665423b89f2b9da97a45f386 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Wed, 22 Apr 2026 12:44:36 +0200 Subject: [PATCH 28/34] Fix links to multiple cluster neurons & cleanup --- Editor/ClusterInspector.cs | 149 +++++++++++++++++--------------- Editor/ClusterViewer.cs | 58 ++++--------- Runtime/Scripts/Core/Cluster.cs | 7 +- 3 files changed, 99 insertions(+), 115 deletions(-) diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterInspector.cs index 1adbe18..a7dc1f2 100644 --- a/Editor/ClusterInspector.cs +++ b/Editor/ClusterInspector.cs @@ -153,15 +153,11 @@ namespace NanoBrain { fontStyle = FontStyle.Bold }; + // Nucleus type string nucleusType = this.currentNucleus.GetType().Name; - // if (this.currentNucleus.parent != null) { - // string clusterName = this.currentNucleus.parent.name; - // GUILayout.Label(clusterName + ": " + nucleusType, headerStyle); - // } - // else GUILayout.Label(nucleusType, headerStyle); - + // Nucleus name if (this.currentNucleus.parent is Cluster parentCluster) { EditorGUILayout.BeginHorizontal(); if (GUILayout.Button(this.currentNucleus.parent.name)) @@ -183,6 +179,7 @@ namespace NanoBrain { } } + // Current output value if (Application.isPlaying) { if (currentNucleus is Neuron currentNeuron1) { GUIContent nameLabel = new("Output", currentNeuron1.outputValue.ToString()); @@ -194,44 +191,63 @@ namespace NanoBrain { else EditorGUILayout.LabelField(" "); - if (this.currentNucleus is MemoryCell memory) { - memory.staticMemory = EditorGUILayout.Toggle("Static Memory", memory.staticMemory); - } + // Memory cell + if (this.currentNucleus is MemoryCell memory) + MemoryCellInspector(memory, ref anythingChanged); + // Cluster + else if (this.currentNucleus is Cluster cluster) + ClusterInspector(cluster, ref anythingChanged); + // Other + else + NucleusInspector(this.currentNucleus, ref anythingChanged); - if (this.currentNucleus is Cluster cluster) { - EditorGUILayout.BeginHorizontal(); - if (cluster.instanceCount > 1) - EditorGUILayout.IntField("Array size", cluster.instanceCount, GUILayout.MinWidth(150)); - else if (cluster.siblingClusters != null && cluster.siblingClusters.Length > 1) - EditorGUILayout.IntField("Array size", cluster.siblingClusters.Count(), GUILayout.MinWidth(150)); - else - EditorGUILayout.IntField("Array size", 1, GUILayout.MinWidth(150)); - if (GUILayout.Button("Add")) { - Undo.RecordObject(prefabAsset, "Array add " + prefabAsset.name); - //cluster.AddInstance(this.prefab); - cluster.AddInstance(); - anythingChanged = true; - } - if (GUILayout.Button("Del")) { - Undo.RecordObject(prefabAsset, "Array delete " + prefabAsset.name); - cluster.RemoveInstance(); - anythingChanged = true; - } - EditorGUILayout.EndHorizontal(); - } - - SynapsesInspector(ref anythingChanged); - ActivationInspector(ref anythingChanged); - - if (GUILayout.Button("Delete this neuron")) + if (GUILayout.Button("Delete")) DeleteNucleus(this.currentNucleus); - if (this.currentNucleus is Cluster subCluster) { - if (GUILayout.Button("Reimport Cluster")) - ReimportCluster(subCluster); - if (GUILayout.Button("Edit Cluster")) - EditCluster(subCluster); + serializedObject.ApplyModifiedProperties(); + if (anythingChanged) { + EditorUtility.SetDirty(prefabAsset); + AssetDatabase.SaveAssets(); } + } + + protected void MemoryCellInspector(MemoryCell memoryCell, ref bool anythingChanged) { + memoryCell.staticMemory = EditorGUILayout.Toggle("Static Memory", memoryCell.staticMemory); + NucleusInspector(memoryCell, ref anythingChanged); + } + + protected void ClusterInspector(Cluster cluster, ref bool anythingChanged) { + EditorGUILayout.BeginHorizontal(); + + int instanceCount = cluster.instanceCount; + if (instanceCount <= 1) { + if (cluster.siblingClusters != null && cluster.siblingClusters.Length > 1) + instanceCount = cluster.siblingClusters.Count(); + else + instanceCount = 1; + } + EditorGUILayout.IntField("Instances", instanceCount, GUILayout.MinWidth(150)); + + if (GUILayout.Button("Add")) { + Undo.RecordObject(prefabAsset, "Array add " + prefabAsset.name); + //cluster.AddInstance(this.prefab); + cluster.AddInstance(); + anythingChanged = true; + } + if (GUILayout.Button("Del")) { + Undo.RecordObject(prefabAsset, "Array delete " + prefabAsset.name); + cluster.RemoveInstance(); + anythingChanged = true; + } + EditorGUILayout.EndHorizontal(); + + if (GUILayout.Button("Reimport Cluster")) + ReimportCluster(cluster); + } + + protected void NucleusInspector(Nucleus nucleus, ref bool anythingChanged) { + SynapsesInspector(ref anythingChanged); + ActivationInspector(ref anythingChanged); EditorGUILayout.Space(); breakOnWake = EditorGUILayout.Toggle("Break on wake", breakOnWake); @@ -242,11 +258,6 @@ namespace NanoBrain { trace = EditorGUILayout.Toggle("Trace", trace); this.currentNucleus.trace = trace; - serializedObject.ApplyModifiedProperties(); - if (anythingChanged) { - EditorUtility.SetDirty(prefabAsset); - AssetDatabase.SaveAssets(); - } } protected void SynapsesInspector(ref bool anythingChanged) { @@ -361,35 +372,31 @@ namespace NanoBrain { } protected void ActivationInspector(ref bool anythingChanged) { - - if (this.currentNucleus is not Cluster) { - EditorGUILayout.Space(); - showActivation = EditorGUILayout.BeginFoldoutHeaderGroup(showActivation, "Activation"); - if (showActivation) { - if (this.currentNucleus is Neuron neuron) { - if (this.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)); - Neuron.ActivationType newPreset = (Neuron.ActivationType)EditorGUILayout.EnumPopup(neuron.curvePreset, GUILayout.MinWidth(50)); - anythingChanged |= newPreset != neuron.curvePreset; - neuron.curvePreset = newPreset; - EditorGUILayout.EndHorizontal(); - } - // if (neuron is Receptor receptor2) { - // if (receptor2.nucleiArray == null || receptor2.nucleiArray.Count() == 0) - // receptor2.array = new NucleusArray(neuron); - // } + EditorGUILayout.Space(); + showActivation = EditorGUILayout.BeginFoldoutHeaderGroup(showActivation, "Activation"); + if (showActivation) { + if (this.currentNucleus is Neuron neuron) { + if (this.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)); + Neuron.ActivationType newPreset = (Neuron.ActivationType)EditorGUILayout.EnumPopup(neuron.curvePreset, GUILayout.MinWidth(50)); + anythingChanged |= newPreset != neuron.curvePreset; + neuron.curvePreset = newPreset; + EditorGUILayout.EndHorizontal(); } - - EditorGUILayout.Space(); + // if (neuron is Receptor receptor2) { + // if (receptor2.nucleiArray == null || receptor2.nucleiArray.Count() == 0) + // receptor2.array = new NucleusArray(neuron); + // } } - EditorGUILayout.EndFoldoutHeaderGroup(); - } + EditorGUILayout.Space(); + } + EditorGUILayout.EndFoldoutHeaderGroup(); } #region Synapses diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index 2784689..b5727a2 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -447,28 +447,20 @@ namespace NanoBrain { // This is used to 'scale' the output value colors of the nuclei float maxValue = 0; int neuronCount = 0; - //List drawnArrays = new(); - Cluster[] drawnCluster = null; + List drawnNeurons = new(); foreach (Synapse synapse in nucleus.synapses) { if (synapse.neuron == null) continue; - if (synapse.neuron.parent is Cluster cluster && - //cluster.siblingClusters != null && - synapse.neuron.parent != nucleus.parent) { + // Draw multiple synapses to the same neuron only once + if (drawnNeurons.Contains(synapse.neuron)) + continue; + drawnNeurons.Add(synapse.neuron); + + float value = synapse.neuron.outputMagnitude * synapse.weight; + if (value > maxValue) + maxValue = value; - //if (drawnArrays.Contains(cluster.siblingClusters)) - if (drawnCluster is not null && cluster.SameSiblingsAs(drawnCluster)) - continue; - //drawnArrays.Add(cluster.siblingClusters); - drawnCluster = cluster.siblingClusters; - } - if (synapse.neuron is Neuron synapseNeuron) { - float value = synapseNeuron.outputMagnitude * synapse.weight; - // Debug.Log($"{synapse.nucleus.name}: {value} {length(synapse.nucleus.outputValue)} {synapse.weight}"); - if (value > maxValue) - maxValue = value; - } neuronCount++; } @@ -477,23 +469,15 @@ namespace NanoBrain { float margin = 10 + spacing / 2; int row = 0; - //drawnArrays = new(); - drawnCluster = null; + drawnNeurons = new(); foreach (Synapse synapse in nucleus.synapses) { if (synapse.neuron is null) continue; - if (synapse.neuron.parent is Cluster cluster && - //cluster.siblingClusters != null && - synapse.neuron.parent != nucleus.parent) { + if (drawnNeurons.Contains(synapse.neuron)) + continue; + drawnNeurons.Add(synapse.neuron); - // if (drawnArrays.Contains(cluster.siblingClusters)) - // continue; - // drawnArrays.Add(cluster.siblingClusters); - if (drawnCluster is not null && cluster.SameSiblingsAs(drawnCluster)) - continue; - drawnCluster = cluster.siblingClusters; - } Vector3 pos = new(250, margin + row * spacing, 0.0f); Handles.color = Color.white; Handles.DrawLine(parentPos, pos); @@ -502,18 +486,10 @@ namespace NanoBrain { if (maxValue == 0 || !float.IsFinite(maxValue)) maxValue = 1; float brightness = 0; - if (synapse.neuron is Neuron synapseNeuron) - brightness = synapseNeuron.outputMagnitude * synapse.weight / maxValue; + brightness = synapse.neuron.outputMagnitude * synapse.weight / maxValue; color = new Color(brightness, brightness, brightness, 1f); } - if (synapse.neuron.parent != null && synapse.neuron.parent != this.currentNucleus.parent) { - // the synapse nucleus is part of a subcluster - //DrawNucleus(synapse.neuron.parent, pos, maxValue, size, color); - DrawNucleus(synapse.neuron, pos, size, color); - } - else { - DrawNucleus(synapse.neuron, pos, size, color); - } + DrawNucleus(synapse.neuron, pos, size, color); row++; } } @@ -619,7 +595,7 @@ namespace NanoBrain { } } - private void DrawCluster(Cluster cluster, Vector3 position, Color color, float size) { + protected void DrawCluster(Cluster cluster, Vector3 position, Color color, float size) { GUIStyle labelTextStyle = new(EditorStyles.label) { normal = { textColor = Color.white }, fontStyle = FontStyle.Bold, @@ -704,7 +680,7 @@ namespace NanoBrain { Handles.DrawLine(from, to); } - private void HandleMouseHover(Nucleus nucleus, Rect rect) { + protected void HandleMouseHover(Nucleus nucleus, Rect rect) { GUIContent tooltip; if (nucleus is Neuron neuron) { tooltip = new( diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index f58f539..acb312a 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -468,9 +468,10 @@ namespace NanoBrain { } public void AddArrayReceiver(Nucleus receiverToAdd, float weight = 1) { - foreach (Cluster cluster in this.siblingClusters) { - cluster.defaultOutput.AddReceiver(receiverToAdd, weight); - } + this.defaultOutput.AddReceiver(receiverToAdd, weight); + // foreach (Cluster cluster in this.siblingClusters) { + // cluster.defaultOutput.AddReceiver(receiverToAdd, weight); + // } } From d583e67e397b3288f13c0e07cce5e80ab8d71b2b Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Wed, 22 Apr 2026 17:21:25 +0200 Subject: [PATCH 29/34] WIP cluster references/instance --- Editor/Brain_Editor.cs | 5 +- .../{ClusterInspector.cs => ClusterEditor.cs} | 39 +-- ...nspector.cs.meta => ClusterEditor.cs.meta} | 0 Editor/ClusterViewer.cs | 18 +- Runtime/Scripts/Brain.cs | 21 +- Runtime/Scripts/Core/Cluster.cs | 249 +++++++++++------- Runtime/Scripts/Core/Nucleus.cs | 2 + 7 files changed, 196 insertions(+), 138 deletions(-) rename Editor/{ClusterInspector.cs => ClusterEditor.cs} (92%) rename Editor/{ClusterInspector.cs.meta => ClusterEditor.cs.meta} (100%) diff --git a/Editor/Brain_Editor.cs b/Editor/Brain_Editor.cs index 8037d8c..449a004 100644 --- a/Editor/Brain_Editor.cs +++ b/Editor/Brain_Editor.cs @@ -7,7 +7,7 @@ using UnityEngine.UIElements; namespace NanoBrain { [CustomEditor(typeof(Brain))] - public class NanoBrainComponent_Editor : Editor { + public class Brain_Editor : Editor { protected static VisualElement mainContainer; protected static VisualElement inspectorContainer; @@ -24,7 +24,7 @@ namespace NanoBrain { } public override VisualElement CreateInspectorGUI() { - Cluster brain = component.brain; + Cluster brain = component.InitializeBrain(); if (Application.isPlaying == false) serializedObject.Update(); @@ -47,6 +47,7 @@ namespace NanoBrain { root.Add(brainField); //} + if (brain != null) CreateViewer(root, brain.prefab, brain.defaultOutput, component.gameObject); diff --git a/Editor/ClusterInspector.cs b/Editor/ClusterEditor.cs similarity index 92% rename from Editor/ClusterInspector.cs rename to Editor/ClusterEditor.cs index a7dc1f2..4dfc4e1 100644 --- a/Editor/ClusterInspector.cs +++ b/Editor/ClusterEditor.cs @@ -8,7 +8,7 @@ using UnityEngine.UIElements; namespace NanoBrain { [CustomEditor(typeof(ClusterPrefab))] - public class ClusterInspector : ClusterViewer { + public class ClusterEditor : ClusterViewer { public override VisualElement CreateInspectorGUI() { ClusterPrefab prefab = target as ClusterPrefab; @@ -18,13 +18,13 @@ namespace NanoBrain { serializedObject.Update(); VisualElement root = new(); - CreateInspector(root, prefab, prefab.output, null); + CreateEditor(root, prefab, prefab.output, null); serializedObject.ApplyModifiedProperties(); return root; } - public GraphView CreateInspector(VisualElement root, ClusterPrefab cluster, Nucleus output, GameObject gameObject) { + public GraphView CreateEditor(VisualElement root, ClusterPrefab cluster, Nucleus output, GameObject gameObject) { root.style.paddingLeft = 0; root.style.paddingRight = 0; root.style.paddingTop = 0; @@ -42,7 +42,6 @@ namespace NanoBrain { graphContainer.style.width = 300; graphContainer.style.overflow = Overflow.Hidden; - VisualElement inspectorContainer = new() { name = "inspector", style = { @@ -57,7 +56,7 @@ namespace NanoBrain { mainContainer.Add(inspectorContainer); root.Add(mainContainer); - graphContainer.SetGraph(gameObject, output, inspectorContainer, this); + graphContainer.SetGraph(gameObject, output, inspectorContainer); return graphContainer; } @@ -83,11 +82,9 @@ namespace NanoBrain { this.currentNucleus = newOutput; } - public void SetGraph(GameObject gameObject, Nucleus nucleus, VisualElement inspectorContainer, ClusterInspector editor) { + public void SetGraph(GameObject gameObject, Nucleus nucleus, VisualElement inspectorContainer) { this.gameObject = gameObject; - this.currentEditor = editor; - //this.cluster = brain; if (Application.isPlaying == false) this.serializedBrain = new SerializedObject(this.prefab); this.currentNucleus = nucleus; @@ -96,7 +93,7 @@ namespace NanoBrain { OnOutputChanged(outputsPopup.choices[0]); } - void Rebuild(VisualElement inspectorContainer) { + private void Rebuild(VisualElement inspectorContainer) { BuildLayers(); if (this.currentNucleus == null) { @@ -121,6 +118,11 @@ namespace NanoBrain { // create a SerializedObject wrapper so Unity inspector controls work (and Undo) SerializedObject so = new(prefabAsset); + + foreach (Nucleus nucleus in this.prefab.nuclei) { + nucleus.Initialize(); + } + this.inspectorIMGUIContainer = new IMGUIContainer(() => InspectorHandler(so)); inspectorContainer.Add(inspectorIMGUIContainer); @@ -257,7 +259,6 @@ namespace NanoBrain { } trace = EditorGUILayout.Toggle("Trace", trace); this.currentNucleus.trace = trace; - } protected void SynapsesInspector(ref bool anythingChanged) { @@ -319,19 +320,27 @@ namespace NanoBrain { else { EditorGUILayout.BeginHorizontal(); - if (synapse.neuron.parent != null && synapse.neuron.parent != this.currentNucleus) { + if (synapse.neuron.clusterPrefab != this.currentNucleus.clusterPrefab) { // If it is a cluster GUIStyle labelStyle = new(GUI.skin.label); float labelWidth = 200; if (synapse.neuron.clusterPrefab != null) { - labelWidth = labelStyle.CalcSize(new GUIContent($"{synapse.neuron.parent.baseName}.")).x; - GUILayout.Label($"{synapse.neuron.parent.baseName}", GUILayout.Width(labelWidth)); + labelWidth = labelStyle.CalcSize(new GUIContent($"{synapse.neuron.clusterPrefab.name}.")).x; + GUILayout.Label($"{synapse.neuron.clusterPrefab.name}", GUILayout.Width(labelWidth)); } - string[] options = synapse.neuron.parent.clusterNuclei.Select(n => n.name).ToArray(); + //string[] options = synapse.neuron.parent.clusterNuclei.Select(n => n.name).ToArray(); + string[] options = synapse.neuron.clusterPrefab.nuclei.Select(n => n.name).ToArray(); int selectedIndex = System.Array.IndexOf(options, synapse.neuron.name); int newIndex = EditorGUILayout.Popup(selectedIndex, options); - if (newIndex != selectedIndex && synapse.neuron.parent.clusterNuclei[newIndex] is Neuron newNeuron) + // if (newIndex != selectedIndex && synapse.neuron.clusterPrefab.nuclei[newIndex] is Neuron newNeuron) + // ChangeSynapse(synapse, newNeuron); + if (newIndex != selectedIndex) { + // It shall be ensured that the parent.clusterNuclei and + // clusterPrefab.nuclei contain the same neurons in the same order.... + Nucleus selectedNucleus = synapse.neuron.parent.clusterNuclei[newIndex]; + Neuron newNeuron = selectedNucleus as Neuron; ChangeSynapse(synapse, newNeuron); + } } else GUILayout.Label(synapse.neuron.name); diff --git a/Editor/ClusterInspector.cs.meta b/Editor/ClusterEditor.cs.meta similarity index 100% rename from Editor/ClusterInspector.cs.meta rename to Editor/ClusterEditor.cs.meta diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index b5727a2..ba9b47a 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -9,7 +9,6 @@ namespace NanoBrain { public class ClusterViewer : Editor { - //public static ClusterViewer previousEditor; public static ClusterPrefab previousPrefab; public class GraphView : VisualElement { @@ -29,9 +28,6 @@ namespace NanoBrain { protected IMGUIContainer graphContainer; protected readonly PopupField outputsPopup; - public ClusterInspector currentEditor; - //public ClusterViewer previousEditor; - public enum Mode { Focus, Full @@ -93,8 +89,8 @@ namespace NanoBrain { RegisterCallback(evt => Unsubscribe()); } - protected virtual void OnModeChange(ChangeEvent evt) { - mode = (Mode)evt.newValue; + protected virtual void OnModeChange(ChangeEvent changeEvent) { + this.mode = (Mode)changeEvent.newValue; } protected virtual void OnOutputChanged(string outputName) { @@ -441,8 +437,6 @@ namespace NanoBrain { } private void DrawSynapses(Nucleus nucleus, Vector3 parentPos, float size) { - int nodeCount = nucleus.synapses.Count; - // Determine the maximum value in this layer // This is used to 'scale' the output value colors of the nuclei float maxValue = 0; @@ -452,7 +446,7 @@ namespace NanoBrain { if (synapse.neuron == null) continue; - // Draw multiple synapses to the same neuron only once + // Count multiple synapses to the same neuron only once if (drawnNeurons.Contains(synapse.neuron)) continue; drawnNeurons.Add(synapse.neuron); @@ -474,6 +468,7 @@ namespace NanoBrain { if (synapse.neuron is null) continue; + // Draw multiple synapses to the same neuron only once if (drawnNeurons.Contains(synapse.neuron)) continue; drawnNeurons.Add(synapse.neuron); @@ -485,8 +480,7 @@ namespace NanoBrain { if (Application.isPlaying) { if (maxValue == 0 || !float.IsFinite(maxValue)) maxValue = 1; - float brightness = 0; - brightness = synapse.neuron.outputMagnitude * synapse.weight / maxValue; + float brightness = synapse.neuron.outputMagnitude * synapse.weight / maxValue; color = new Color(brightness, brightness, brightness, 1f); } DrawNucleus(synapse.neuron, pos, size, color); @@ -731,7 +725,7 @@ namespace NanoBrain { Selection.activeObject = subCluster.prefab; EditorGUIUtility.PingObject(subCluster.prefab); ClusterViewer.previousPrefab = this.prefab; - ClusterInspector newEditor = CreateEditor(subCluster.prefab) as ClusterInspector; + ClusterEditor newEditor = CreateEditor(subCluster.prefab) as ClusterEditor; } #endregion Graph diff --git a/Runtime/Scripts/Brain.cs b/Runtime/Scripts/Brain.cs index 81f2b4c..36fd6e7 100644 --- a/Runtime/Scripts/Brain.cs +++ b/Runtime/Scripts/Brain.cs @@ -20,17 +20,24 @@ namespace NanoBrain { /// public Cluster brain { get { - if (brainInstance == null && brainPrefab != null) { - brainInstance = new Cluster(brainPrefab) { - name = brainPrefab.name - }; - } else if (brainInstance != null && brainPrefab == null) { - brainInstance = null; - } + // 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; + } + /// /// Update the weight for all Synapses coming from the Neuron with the given name /// diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index acb312a..525e296 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -17,6 +17,8 @@ namespace NanoBrain { public class Cluster : Nucleus { // It may be that clusters will not be nuclei anymore in the future.... + public ClusterPrefab prefab; + /// /// The base name of the cluster. I don't think this is actively used at this moment /// @@ -36,6 +38,12 @@ namespace NanoBrain { public int instanceCount = 1; public Dictionary thingClusters = new(); + [SerializeReference] + public List clusterNuclei = new(); + // the nuclei sorted using topological sorting + // to ensure that the cluster is computer in the right order + public List sortedNuclei; + #region Init /// @@ -79,8 +87,9 @@ namespace NanoBrain { /// Where which the clone be found??? private void ClonePrefab() { Nucleus[] prefabNuclei = this.prefab.nuclei.ToArray(); + // first clone the nuclei without their connections - foreach (Nucleus nucleus in this.prefab.nuclei) { + foreach (Nucleus nucleus in prefabNuclei) { nucleus.ShallowCloneTo(this); } Nucleus[] clonedNuclei = this.clusterNuclei.ToArray(); @@ -95,110 +104,152 @@ namespace NanoBrain { if (clonedNucleus == null || clonedNucleus is not Neuron clonedNeuron) continue; - // Copy the receivers, which will also create the synapses - // Clusters do not have receivers... - foreach (Nucleus receiver in prefabNeuron.receivers.ToArray()) { - int ix = GetNucleusIndex(prefabNuclei, receiver); - if (ix < 0) - continue; - - if (clonedNuclei[ix] is not Nucleus clonedReceiver) - continue; - - // Find the synapse for the weight - float weight = 1; - foreach (Synapse synapse in receiver.synapses) { - // Find the weight for this synapse - if (synapse.neuron == prefabNucleus) { - weight = synapse.weight; - break; - } - } - - clonedNeuron.AddReceiver(clonedReceiver, weight); - } - } - - // Copy the siblings for clusters - for (int nucleusIx = 0; nucleusIx < prefabNuclei.Length; nucleusIx++) { - Nucleus prefabNucleus = prefabNuclei[nucleusIx]; - if (prefabNucleus is not Cluster prefabCluster) - continue; - - if (prefabCluster.siblingClusters == null || prefabCluster.siblingClusters.Length == 0) - continue; - - Cluster clonedNucleus = clonedNuclei[nucleusIx] as Cluster; - if (prefabCluster == prefabCluster.siblingClusters[0]) { - // We clone the array only for the first entry - //NucleusArray clonedArray = new(prefabReceptor.nucleiArray.Length); - Cluster[] clonedArray = new Cluster[prefabCluster.siblingClusters.Length]; - int arrayIx = 0; - foreach (Cluster prefabArrayNucleus in prefabCluster.siblingClusters) { - int arrayNucleusIx = GetNucleusIndex(prefabNuclei, prefabArrayNucleus); - if (arrayNucleusIx >= 0) { - Cluster clonedArrayNucleus = clonedNuclei[arrayNucleusIx] as Cluster; - clonedArray[arrayIx] = clonedArrayNucleus; - } - else { - Debug.LogError($" Could not find prefab nucleus {prefabNucleus.name} in the clones"); - } - arrayIx++; - } - clonedNucleus.siblingClusters = clonedArray; - } - else { - // The others will refer to the array created for the first nucleus in the array - int firstNucleusIx = GetNucleusIndex(prefabNuclei, prefabCluster.siblingClusters[0]); - Cluster clonedFirstNucleus = clonedNuclei[firstNucleusIx] as Cluster; - clonedNucleus.siblingClusters = clonedFirstNucleus.siblingClusters; - } - } - - // Collect the subclusters - List subClusters = new(); - foreach (Nucleus nucleus in prefabNuclei) { - foreach (Synapse synapse in nucleus.synapses) { - Nucleus synapseNucleus = synapse.neuron; - if (synapseNucleus is not Cluster subCluster) - continue; - if (subClusters.Contains(subCluster)) - continue; - subClusters.Add(subCluster); - } - } - // Create the subcluster instances - foreach (Cluster subCluster in subClusters) { - for (int ix = 0; ix < subCluster.instanceCount; ix++) { - // create the new instance - Cluster clusterInstance = new(subCluster.prefab); - // connect it - foreach ((Neuron sender, Nucleus receiver) in subCluster.CollectConnections()) { - int receiverIx = GetNucleusIndex(prefabNuclei, receiver); - if (receiverIx < 0) + foreach (Synapse prefabSynapse in prefabNeuron.synapses) { + Neuron synapseNeuron = prefabSynapse.neuron; + if (synapseNeuron.parent is not null && synapseNeuron.clusterPrefab != this.clusterPrefab) { + // Neuron is in another cluster, find the cloned cluster first + Cluster prefabCluster = synapseNeuron.parent; + int clusterIx = GetNucleusIndex(prefabNuclei, prefabCluster); + if (clusterIx < 0) + // Could not find the cluster in the prefab + continue; + if (clonedNuclei[clusterIx] is not Cluster clonedCluster) + // Could not find the cloned cluster continue; - if (clonedNuclei[receiverIx] is not Nucleus clonedReceiver) + // Now find the neuron in that cloned cluster + int neuronIx = GetNucleusIndex(prefabCluster.prefab.nuclei, prefabSynapse.neuron); + if (neuronIx < 0) + // Could not find the neuron in the prefab cluster + continue; + if (clonedCluster.clusterNuclei[neuronIx] is not Neuron clonedSender) + // Could not find the neuron in the cloned cluster continue; - // Find the synapse for the weight - float weight = 1; - foreach (Synapse synapse in receiver.synapses) { - // Find the weight for this synapse - if (synapse.neuron == sender) { - weight = synapse.weight; - break; + clonedSender.AddReceiver(clonedNeuron, prefabSynapse.weight); + } + else { + int ix = GetNucleusIndex(prefabNuclei, prefabSynapse.neuron); + if (ix < 0) + continue; + if (clonedNuclei[ix] is not Neuron clonedSender) + continue; + + // Copy the receivers which will also create the synapse + clonedSender.AddReceiver(clonedNeuron, prefabSynapse.weight); + } + } + + // // Copy the receivers, which will also create the synapses + // foreach (Nucleus receiver in prefabNeuron.receivers.ToArray()) { + // int ix = GetNucleusIndex(prefabNuclei, receiver); + // if (ix < 0) + // continue; + + // if (clonedNuclei[ix] is not Nucleus clonedReceiver) + // continue; + + // // Find the synapse for the weight + // float weight = 1; + // foreach (Synapse synapse in receiver.synapses) { + // // Find the weight for this synapse + // if (synapse.neuron == prefabNucleus) { + // weight = synapse.weight; + // break; + // } + // } + + // clonedNeuron.AddReceiver(clonedReceiver, weight); + // } + } + /* + // Copy the siblings for clusters + for (int nucleusIx = 0; nucleusIx < prefabNuclei.Length; nucleusIx++) { + Nucleus prefabNucleus = prefabNuclei[nucleusIx]; + if (prefabNucleus is not Cluster prefabCluster) + continue; + + if (prefabCluster.siblingClusters == null || prefabCluster.siblingClusters.Length == 0) + continue; + + Cluster clonedNucleus = clonedNuclei[nucleusIx] as Cluster; + if (prefabCluster == prefabCluster.siblingClusters[0]) { + // We clone the array only for the first entry + //NucleusArray clonedArray = new(prefabReceptor.nucleiArray.Length); + Cluster[] clonedArray = new Cluster[prefabCluster.siblingClusters.Length]; + int arrayIx = 0; + foreach (Cluster prefabArrayNucleus in prefabCluster.siblingClusters) { + int arrayNucleusIx = GetNucleusIndex(prefabNuclei, prefabArrayNucleus); + if (arrayNucleusIx >= 0) { + Cluster clonedArrayNucleus = clonedNuclei[arrayNucleusIx] as Cluster; + clonedArray[arrayIx] = clonedArrayNucleus; + } + else { + Debug.LogError($" Could not find prefab nucleus {prefabNucleus.name} in the clones"); + } + arrayIx++; + } + clonedNucleus.siblingClusters = clonedArray; + } + else { + // The others will refer to the array created for the first nucleus in the array + int firstNucleusIx = GetNucleusIndex(prefabNuclei, prefabCluster.siblingClusters[0]); + Cluster clonedFirstNucleus = clonedNuclei[firstNucleusIx] as Cluster; + clonedNucleus.siblingClusters = clonedFirstNucleus.siblingClusters; } } - if (clusterInstance.GetNucleus(sender.name) is not Neuron clonedSender) - continue; + /* + // Collect the subclusters + List subClusters = new(); + foreach (Nucleus nucleus in prefabNuclei) { + foreach (Synapse synapse in nucleus.synapses) { + Nucleus synapseNucleus = synapse.neuron; + Cluster subCluster = synapseNucleus.parent; + if (subCluster is null || + synapseNucleus.clusterPrefab == this.clusterPrefab) { - clonedSender.AddReceiver(clonedReceiver, weight); - } - } - } + continue; + } + // if (synapseNucleus is not Cluster subCluster) + // continue; + if (subClusters.Contains(subCluster)) + continue; + subClusters.Add(subCluster); + } + } + // Create the subcluster instances + foreach (Cluster subCluster in subClusters) { + for (int ix = 0; ix < subCluster.instanceCount; ix++) { + // create the new instance + Cluster clusterInstance = new(subCluster.prefab); + // connect it + foreach ((Neuron sender, Nucleus receiver) in subCluster.CollectConnections()) { + int receiverIx = GetNucleusIndex(prefabNuclei, receiver); + if (receiverIx < 0) + continue; + if (clonedNuclei[receiverIx] is not Nucleus clonedReceiver) + continue; + + // Find the synapse for the weight + float weight = 1; + foreach (Synapse synapse in receiver.synapses) { + // Find the weight for this synapse + if (synapse.neuron == sender) { + weight = synapse.weight; + break; + } + } + + if (clusterInstance.GetNucleus(sender.name) is not Neuron clonedSender) + continue; + + clonedSender.AddReceiver(clonedReceiver, weight); + } + } + } + */ foreach (Nucleus nucleus in this.clusterNuclei) { if (nucleus is Cluster clonedSubCluster) @@ -477,14 +528,8 @@ namespace NanoBrain { #endregion ClusterArray - public ClusterPrefab prefab; - [SerializeReference] - public List clusterNuclei = new(); - // the nuclei sorted using topological sorting - // to ensure that the cluster is computer in the right order - public List sortedNuclei; //public Dictionary nucleiDict = new(); public List _inputs = null; diff --git a/Runtime/Scripts/Core/Nucleus.cs b/Runtime/Scripts/Core/Nucleus.cs index f983a2d..314fee3 100644 --- a/Runtime/Scripts/Core/Nucleus.cs +++ b/Runtime/Scripts/Core/Nucleus.cs @@ -59,6 +59,8 @@ public abstract class Nucleus { //ClusterArray, } + public virtual void Initialize() {} + #region Synapses /// From 96439cc324e3e8c8bbff85c407ab0922d193892a Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Thu, 23 Apr 2026 09:33:15 +0200 Subject: [PATCH 30/34] Visualize all outputs --- Editor/ClusterEditor.cs | 7 +- Editor/ClusterViewer.cs | 388 +++++++++++++++++++++++++--------------- 2 files changed, 251 insertions(+), 144 deletions(-) diff --git a/Editor/ClusterEditor.cs b/Editor/ClusterEditor.cs index 4dfc4e1..fa860aa 100644 --- a/Editor/ClusterEditor.cs +++ b/Editor/ClusterEditor.cs @@ -89,8 +89,9 @@ namespace NanoBrain { this.serializedBrain = new SerializedObject(this.prefab); this.currentNucleus = nucleus; Rebuild(inspectorContainer); - if (outputsPopup != null) - OnOutputChanged(outputsPopup.choices[0]); + this.selectedOutput = this.prefab.outputs[0]; + // if (outputsPopup != null) + // OnOutputChanged(outputsPopup.choices[0]); } private void Rebuild(VisualElement inspectorContainer) { @@ -163,7 +164,7 @@ namespace NanoBrain { if (this.currentNucleus.parent is Cluster parentCluster) { EditorGUILayout.BeginHorizontal(); if (GUILayout.Button(this.currentNucleus.parent.name)) - EditCluster(parentCluster); + OnClusterClick(parentCluster); EditorGUI.BeginDisabledGroup(true); EditorGUILayout.TextField(this.currentNucleus.name, boldTextFieldStyle); EditorGUI.EndDisabledGroup(); diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index ba9b47a..2796d01 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -52,14 +52,14 @@ namespace NanoBrain { modePopup.RegisterValueChangedCallback(OnModeChange); topMenuContainer.Add(modePopup); - List names = this.prefab.outputs.Select(output => output.name).ToList(); - if (names.Count > 0 && names.First() != null) { - outputsPopup = new(names, names.First()) { - style = { flexGrow = 1 } - }; - outputsPopup.RegisterValueChangedCallback(evt => OnOutputChanged(evt.newValue)); - topMenuContainer.Add(outputsPopup); - } + // List names = this.prefab.outputs.Select(output => output.name).ToList(); + // if (names.Count > 0 && names.First() != null) { + // outputsPopup = new(names, names.First()) { + // style = { flexGrow = 1 } + // }; + // outputsPopup.RegisterValueChangedCallback(evt => OnOutputChanged(evt.newValue)); + // topMenuContainer.Add(outputsPopup); + // } scrollView = new(ScrollViewMode.Horizontal); scrollView.style.position = Position.Absolute; @@ -93,15 +93,15 @@ namespace NanoBrain { this.mode = (Mode)changeEvent.newValue; } - protected virtual void OnOutputChanged(string outputName) { - if (this.currentNucleus.parent != null) - // Get nucleus in the parent instance - this.selectedOutput = this.currentNucleus.parent.GetNucleus(outputName); - else - // Get nucleus in the prefab - this.selectedOutput = this.prefab.GetNucleus(outputName); - this.currentNucleus = this.selectedOutput; - } + // protected virtual void OnOutputChanged(string outputName) { + // if (this.currentNucleus.parent != null) + // // Get nucleus in the parent instance + // this.selectedOutput = this.currentNucleus.parent.GetNucleus(outputName); + // else + // // Get nucleus in the prefab + // this.selectedOutput = this.prefab.GetNucleus(outputName); + // this.currentNucleus = this.selectedOutput; + // } bool subscribed = false; void Subscribe() { @@ -124,8 +124,9 @@ namespace NanoBrain { this.serializedBrain = new SerializedObject(this.prefab); this.currentNucleus = nucleus; Rebuild(); //inspectorContainer); - if (outputsPopup != null) - OnOutputChanged(outputsPopup.choices[0]); + this.selectedOutput = this.prefab.outputs[0]; + // if (outputsPopup != null) + // OnOutputChanged(outputsPopup.choices[0]); } void Rebuild() { @@ -143,63 +144,7 @@ namespace NanoBrain { } } - protected void BuildLayers() { - // A temporary list to track what's been added to layers - this.layers = new(); - int layerIx = 0; - - Nucleus selectedNucleus = this.currentNucleus; - if (selectedNucleus == null) - return; - NeuroidLayer currentLayer = new() { ix = layerIx }; - - if (selectedNucleus is Neuron selectedNeuron && selectedNeuron.receivers != null) { - foreach (Nucleus receiver in selectedNeuron.receivers) { - Nucleus outputNeuroid = receiver; - if (outputNeuroid != null) { - AddToLayer(currentLayer, outputNeuroid); - // Debug.Log($"layer {layerIx} nucleus {outputNeuroid.name}"); - } - } - } - if (currentLayer.neuroids.Count > 0) { - this.layers.Add(currentLayer); - layerIx++; - currentLayer = new() { ix = layerIx }; - } - - AddToLayer(currentLayer, selectedNucleus); - this.layers.Add(currentLayer); - // Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}"); - - layerIx++; - currentLayer = new() { ix = layerIx }; - - if (selectedNucleus.synapses != null) { - foreach (Synapse synapse in selectedNucleus.synapses) { - Nucleus input = synapse.neuron; - AddToLayer(currentLayer, input); - // Debug.Log($"layer {layerIx} nucleus {input.name}"); - } - } - if (currentLayer.neuroids.Count > 0) { - this.layers.Add(currentLayer); - } - } - - private void AddToLayer(NeuroidLayer layer, Nucleus nucleus) { - if (nucleus == null) - return; - layer.neuroids.Add(nucleus); - // Store its position - Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1); - neuroidPositions[nucleus] = neuroidPosition; - } - public void OnIMGUI() { - if (currentNucleus == null) - return; - if (Application.isPlaying == false) serializedBrain.Update(); @@ -315,72 +260,78 @@ namespace NanoBrain { float size = 20; Vector3 position = new(150, 210, 0); - DrawReceivers(this.currentNucleus, position, size); - DrawSynapses(this.currentNucleus, position, size); + if (this.currentNucleus != null) { + DrawReceivers(this.currentNucleus, position, size); + DrawSynapses(this.currentNucleus, position, size); - // Draw selected Nucleus - if (expandArray) { - float maxValue = 1; + // Draw selected Nucleus + if (expandArray) { + float maxValue = 1; - if (this.currentNucleus is Cluster cluster) { - float spacing = 400f / cluster.siblingClusters.Length; - 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] { + if (this.currentNucleus is Cluster cluster) { + float spacing = 400f / cluster.siblingClusters.Length; + float margin = 10 + spacing / 2; + float xMin = 150 - size; + float xMax = 150 + size; + float yMin = 10 + margin - size / 2; + float yMax = 400 - margin + size; + Vector3[] verts = new Vector3[4] { new(xMin, yMin, 0), new(xMax, yMin, 0), new(xMax, yMax, 0), new(xMin, yMax, 0) }; - Handles.color = Color.black; - Handles.DrawAAConvexPolygon(verts); - int row = 0; - foreach (Cluster sibling in cluster.siblingClusters) { - 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++; + Handles.color = Color.black; + Handles.DrawAAConvexPolygon(verts); + int row = 0; + foreach (Cluster sibling in cluster.siblingClusters) { + 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); } - 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 { + if (this.currentNucleus is Neuron neuron) + maxValue = neuron.outputMagnitude; + + DrawNucleus(this.currentNucleus, position, maxValue, 20); + } - else - Handles.Label(labelPos, clusterName, style); } 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 { - 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); + DrawAllOutputs(position, size); + DrawOutputs(position, size); } graphContainer.style.width = 300; } - private void DrawReceivers(Nucleus nucleus, Vector3 parentPos, float size) { + protected void DrawReceivers(Nucleus nucleus, Vector3 parentPos, float size) { List receivers; if (nucleus is Neuron neuron) receivers = neuron.receivers; @@ -389,8 +340,9 @@ namespace NanoBrain { else return; + // For top-level nodes, add link to previous editor or 'all-outputs' int nodeCount = receivers.Count(); - if (nucleus == this.selectedOutput && ClusterViewer.previousPrefab != null) { + if (nucleus == this.selectedOutput) {// && ClusterViewer.previousPrefab != null) { // Add link to previous editor nodeCount++; } @@ -413,12 +365,6 @@ namespace NanoBrain { int row = 0; List drawnArrays = new(); foreach (Nucleus receiver in receivers) { - // if (receiver is Receptor receptor) { - // if (drawnArrays.Contains(receptor.nucleiArray)) - // continue; - // drawnArrays.Add(receptor.nucleiArray); - // } - Nucleus receiverNucleus = receiver; if (receiverNucleus == null) continue; @@ -429,14 +375,20 @@ namespace NanoBrain { DrawNucleus(receiverNucleus, pos, maxValue, size); row++; } - if (nucleus == this.selectedOutput && ClusterViewer.previousPrefab != null) { + if (nucleus == this.selectedOutput) { Vector3 pos = new(50, margin + row * spacing, 0); DrawEdge(parentPos, pos); - DrawClusterPrefab(ClusterViewer.previousPrefab, pos, size); + if (ClusterViewer.previousPrefab != null) + DrawClusterPrefab(ClusterViewer.previousPrefab, pos, size); + else + DrawAllOutputs(pos, size); } } - private void DrawSynapses(Nucleus nucleus, Vector3 parentPos, float size) { + protected void DrawSynapses(Nucleus nucleus, Vector3 parentPos, float size) { + 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; @@ -474,8 +426,9 @@ namespace NanoBrain { drawnNeurons.Add(synapse.neuron); Vector3 pos = new(250, margin + row * spacing, 0.0f); - Handles.color = Color.white; - Handles.DrawLine(parentPos, pos); + DrawEdge(parentPos, pos); + // Handles.color = Color.white; + // Handles.DrawLine(parentPos, pos); Color color = Color.black; if (Application.isPlaying) { if (maxValue == 0 || !float.IsFinite(maxValue)) @@ -488,6 +441,115 @@ namespace NanoBrain { } } + protected void DrawOutputs(Vector2 parentPos, float size) { + int outputCount = this.prefab.outputs.Count; + + // Determine the spacing of the nuclei in the layer + float spacing = 400f / outputCount; + float margin = 10 + spacing / 2; + + int row = 0; + List drawnNuclei = new(); + foreach (Nucleus nucleus in this.prefab.outputs) { + + // 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); + // Handles.color = Color.white; + // Handles.DrawLine(parentPos, pos); + Color color = Color.black; + DrawNucleus(nucleus, pos, size, color); + row++; + } + } + + + protected void BuildLayers() { + // A temporary list to track what's been added to layers + this.layers = new(); + int layerIx = 0; + + if (this.currentNucleus == null) { + BuildAllOutputs(); + return; + } + + // Nucleus selectedNucleus = this.currentNucleus; + // if (selectedNucleus == null) + // return; + NeuroidLayer currentLayer = new() { ix = layerIx }; + + // Receivers layer + if (this.currentNucleus is Neuron selectedNeuron && selectedNeuron.receivers != null) { + foreach (Nucleus receiver in selectedNeuron.receivers) { + Nucleus outputNeuroid = receiver; + if (outputNeuroid != null) { + AddToLayer(currentLayer, outputNeuroid); + // Debug.Log($"layer {layerIx} nucleus {outputNeuroid.name}"); + } + } + } + + // Create next layer + if (currentLayer.neuroids.Count > 0) { + this.layers.Add(currentLayer); + layerIx++; + currentLayer = new() { ix = layerIx }; + } + + // Current Nucleus layer + AddToLayer(currentLayer, this.currentNucleus); + this.layers.Add(currentLayer); + // Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}"); + + // Create next layer + layerIx++; + currentLayer = new() { ix = layerIx }; + + // Synapses layer + if (this.currentNucleus.synapses != null) { + foreach (Synapse synapse in this.currentNucleus.synapses) { + Nucleus input = synapse.neuron; + AddToLayer(currentLayer, input); + // Debug.Log($"layer {layerIx} nucleus {input.name}"); + } + } + if (currentLayer.neuroids.Count > 0) { + this.layers.Add(currentLayer); + } + } + + protected void BuildAllOutputs() { + return; + // Debug.Log("Build all outputs"); + // this.layers = new(); + // int layerIx = 0; + // NeuroidLayer currentLayer = new() { ix = layerIx }; + + // // empty layer, as 'All outputs' is not a nucleus + + // layerIx++; + // currentLayer = new() {ix = layerIx}; + + // foreach (Nucleus nucleus in this.prefab.outputs) { + // AddToLayer(currentLayer, nucleus); + // } + } + + private void AddToLayer(NeuroidLayer layer, Nucleus nucleus) { + if (nucleus == null) + return; + layer.neuroids.Add(nucleus); + // Store its position + Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1); + neuroidPositions[nucleus] = neuroidPosition; + } + + #endregion Focus Graph protected void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size) { @@ -504,6 +566,9 @@ namespace NanoBrain { } 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; @@ -538,7 +603,7 @@ namespace NanoBrain { Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron style.alignment = TextAnchor.UpperCenter; - if (nucleus.parent != currentNucleus.parent && nucleus.parent is Cluster parentCluster1) { + if (nucleus.parent != null && nucleus.parent != currentNucleus.parent && nucleus.parent is Cluster parentCluster1) { // This neuron is part of another cluster parentCluster1.name ??= ""; string baseName = ""; @@ -582,9 +647,9 @@ namespace NanoBrain { // Consume the event so the scene doesn't also handle it e.Use(); if (nucleus is Cluster parentCluster2) - HandleClicked(parentCluster2); + OnNeuronClick(parentCluster2); else - HandleClicked(nucleus); + OnNeuronClick(nucleus); } } } @@ -669,9 +734,41 @@ namespace NanoBrain { } } - protected void DrawEdge(Vector2 from, Vector2 to) { + 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, "All\noutputs", 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); + // 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) { @@ -693,7 +790,7 @@ namespace NanoBrain { GUI.Box(tooltipRect, tooltip); } - protected void HandleClicked(Nucleus nucleus) { + protected void OnNeuronClick(Nucleus nucleus) { if (nucleus == this.currentNucleus) { if (Application.isPlaying) { if (nucleus is Cluster) @@ -703,10 +800,10 @@ namespace NanoBrain { } else { if (nucleus is Cluster cluster) - EditCluster(cluster); + OnClusterClick(cluster); } } - else if (nucleus.parent != this.currentNucleus.parent) { + else if (nucleus.parent != null && nucleus.parent != this.currentNucleus.parent) { // We go to a different cluster // select the cluster, not the neuron in the cluster this.currentNucleus = nucleus.parent; @@ -715,12 +812,14 @@ namespace NanoBrain { } else { this.currentNucleus = nucleus; + if (this.currentNucleus is Neuron neuron && neuron.receivers.Count == 0) + this.selectedOutput = this.currentNucleus; expandArray = false; BuildLayers(); } } - protected void EditCluster(Cluster subCluster) { + protected void OnClusterClick(Cluster subCluster) { // May be used with storedPrefab... Selection.activeObject = subCluster.prefab; EditorGUIUtility.PingObject(subCluster.prefab); @@ -728,6 +827,13 @@ namespace NanoBrain { ClusterEditor newEditor = CreateEditor(subCluster.prefab) as ClusterEditor; } + protected void OnAllOutputsClick() { + this.currentNucleus = null; + this.selectedOutput = null; + expandArray = false; + BuildLayers(); + } + #endregion Graph void OnSceneGUI(SceneView sceneView) { From b12616bd0c2ccd6809fb056c2f200b362ab20589 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Thu, 23 Apr 2026 11:34:21 +0200 Subject: [PATCH 31/34] Fix neuron output visualisation --- Editor/Brain_Editor.cs | 21 ++-- Editor/ClusterEditor.cs | 27 +++-- Editor/ClusterViewer.cs | 177 +++++++++----------------------- Runtime/Scripts/Brain.cs | 26 ++--- Runtime/Scripts/Core/Cluster.cs | 2 +- 5 files changed, 84 insertions(+), 169 deletions(-) diff --git a/Editor/Brain_Editor.cs b/Editor/Brain_Editor.cs index 449a004..6a9e654 100644 --- a/Editor/Brain_Editor.cs +++ b/Editor/Brain_Editor.cs @@ -24,7 +24,6 @@ namespace NanoBrain { } public override VisualElement CreateInspectorGUI() { - Cluster brain = component.InitializeBrain(); if (Application.isPlaying == false) serializedObject.Update(); @@ -36,27 +35,23 @@ namespace NanoBrain { paddingRight = 0, paddingTop = 0, paddingBottom = 0 - } + } }; root.styleSheets.Add(Resources.Load("GraphStyles")); - //if (Application.isPlaying == false) { - PropertyField brainField = new(brainProp) { - label = "Cluster Prefab" - }; - root.Add(brainField); - //} + PropertyField brainField = new(brainProp) { + label = "Cluster Prefab" + }; + root.Add(brainField); - - if (brain != null) - CreateViewer(root, brain.prefab, brain.defaultOutput, component.gameObject); + CreateViewer(root, component.brain, component.gameObject); if (Application.isPlaying == false) serializedObject.ApplyModifiedProperties(); return root; } - public ClusterViewer.GraphView CreateViewer(VisualElement root, ClusterPrefab cluster, Nucleus output, GameObject gameObject) { + public ClusterViewer.GraphView CreateViewer(VisualElement root, Cluster cluster, GameObject gameObject) { VisualElement mainContainer = new() { style = { flexDirection = FlexDirection.Row, @@ -69,7 +64,7 @@ namespace NanoBrain { mainContainer.Add(graph); root.Add(mainContainer); - graph.SetGraph(gameObject, output); + graph.SetGraph(gameObject); return graph; } diff --git a/Editor/ClusterEditor.cs b/Editor/ClusterEditor.cs index fa860aa..1498a79 100644 --- a/Editor/ClusterEditor.cs +++ b/Editor/ClusterEditor.cs @@ -18,13 +18,13 @@ namespace NanoBrain { serializedObject.Update(); VisualElement root = new(); - CreateEditor(root, prefab, prefab.output, null); + CreateEditor(root, prefab, null); serializedObject.ApplyModifiedProperties(); return root; } - public GraphView CreateEditor(VisualElement root, ClusterPrefab cluster, Nucleus output, GameObject gameObject) { + public GraphView CreateEditor(VisualElement root, ClusterPrefab cluster, GameObject gameObject) { root.style.paddingLeft = 0; root.style.paddingRight = 0; root.style.paddingTop = 0; @@ -56,14 +56,22 @@ namespace NanoBrain { mainContainer.Add(inspectorContainer); root.Add(mainContainer); - graphContainer.SetGraph(gameObject, output, inspectorContainer); + graphContainer.SetGraph(gameObject, inspectorContainer); return graphContainer; } public class GraphEditor : GraphView { - public GraphEditor(ClusterPrefab prefab) : base(prefab) { + protected ClusterPrefab prefab; + + public GraphEditor(ClusterPrefab prefab) : base(prefab.output.parent) { + this.prefab = prefab; + + // In a Prefab editor, no instance exists but we need it for the ClusterViewer. + // So we create a temporary instance + Cluster cluster = new(prefab); + this.currentCluster = cluster; Button addButton = new(() => OnAddClusterOutput()) { text = "Add" @@ -82,20 +90,20 @@ namespace NanoBrain { this.currentNucleus = newOutput; } - public void SetGraph(GameObject gameObject, Nucleus nucleus, VisualElement inspectorContainer) { + public void SetGraph(GameObject gameObject, VisualElement inspectorContainer) { this.gameObject = gameObject; if (Application.isPlaying == false) this.serializedBrain = new SerializedObject(this.prefab); - this.currentNucleus = nucleus; + this.selectedOutput = this.currentCluster.outputs[0]; + this.currentNucleus = this.selectedOutput; + //this.currentCluster = this.currentNucleus.parent; Rebuild(inspectorContainer); - this.selectedOutput = this.prefab.outputs[0]; // if (outputsPopup != null) // OnOutputChanged(outputsPopup.choices[0]); } private void Rebuild(VisualElement inspectorContainer) { - BuildLayers(); if (this.currentNucleus == null) { inspectorContainer.Clear(); @@ -440,14 +448,12 @@ namespace NanoBrain { Neuron newNeuroid = new(this.prefab, "New neuron"); newNeuroid.AddReceiver(nucleus); this.currentNucleus = newNeuroid; - BuildLayers(); } protected virtual void AddMemoryCellInput(Nucleus nucleus) { MemoryCell newMemory = new(this.prefab, "New memory cell"); newMemory.AddReceiver(nucleus); this.currentNucleus = newMemory; - BuildLayers(); } protected virtual void AddClusterInput(Nucleus nucleus) { @@ -540,7 +546,6 @@ namespace NanoBrain { Neuron.Delete(nucleus); this.currentNucleus = this.prefab.output; - BuildLayers(); } Nucleus.Type selectedType = Nucleus.Type.None; diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index 2796d01..1af3311 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -12,14 +12,13 @@ namespace NanoBrain { public static ClusterPrefab previousPrefab; public class GraphView : VisualElement { - protected readonly ClusterPrefab prefab; + //protected readonly ClusterPrefab prefab; + protected Cluster currentCluster; protected SerializedObject serializedBrain; protected Nucleus currentNucleus; protected Nucleus selectedOutput; protected GameObject gameObject; - private List layers = new(); - private readonly Dictionary neuroidPositions = new(); private bool expandArray = false; protected ClusterPrefab prefabAsset; @@ -34,8 +33,8 @@ namespace NanoBrain { } public Mode mode = Mode.Focus; - public GraphView(ClusterPrefab prefab) { - this.prefab = prefab; + public GraphView(Cluster cluster) { + this.currentCluster = cluster; name = "content"; style.flexGrow = 1; @@ -52,15 +51,6 @@ namespace NanoBrain { modePopup.RegisterValueChangedCallback(OnModeChange); topMenuContainer.Add(modePopup); - // List names = this.prefab.outputs.Select(output => output.name).ToList(); - // if (names.Count > 0 && names.First() != null) { - // outputsPopup = new(names, names.First()) { - // style = { flexGrow = 1 } - // }; - // outputsPopup.RegisterValueChangedCallback(evt => OnOutputChanged(evt.newValue)); - // topMenuContainer.Add(outputsPopup); - // } - scrollView = new(ScrollViewMode.Horizontal); scrollView.style.position = Position.Absolute; scrollView.style.left = 0; scrollView.style.top = 0; @@ -93,16 +83,6 @@ namespace NanoBrain { this.mode = (Mode)changeEvent.newValue; } - // protected virtual void OnOutputChanged(string outputName) { - // if (this.currentNucleus.parent != null) - // // Get nucleus in the parent instance - // this.selectedOutput = this.currentNucleus.parent.GetNucleus(outputName); - // else - // // Get nucleus in the prefab - // this.selectedOutput = this.prefab.GetNucleus(outputName); - // this.currentNucleus = this.selectedOutput; - // } - bool subscribed = false; void Subscribe() { if (subscribed) return; @@ -117,25 +97,21 @@ namespace NanoBrain { subscribed = false; } - public void SetGraph(GameObject gameObject, Nucleus nucleus) { + public void SetGraph(GameObject gameObject) { this.gameObject = gameObject; if (Application.isPlaying == false) - this.serializedBrain = new SerializedObject(this.prefab); - this.currentNucleus = nucleus; - Rebuild(); //inspectorContainer); - this.selectedOutput = this.prefab.outputs[0]; - // if (outputsPopup != null) - // OnOutputChanged(outputsPopup.choices[0]); + this.serializedBrain = new SerializedObject(this.currentCluster.prefab); + this.selectedOutput = this.currentCluster.outputs[0]; + this.currentNucleus = this.selectedOutput; + Rebuild(); } void Rebuild() { - BuildLayers(); - if (this.currentNucleus == null) return; - string path = AssetDatabase.GetAssetPath(this.prefab); // or known path + string path = AssetDatabase.GetAssetPath(this.currentCluster.prefab); // or known path this.prefabAsset = AssetDatabase.LoadAssetAtPath(path); if (this.prefabAsset == null) { // create in memory save if it doesn't exist @@ -442,15 +418,36 @@ namespace NanoBrain { } protected void DrawOutputs(Vector2 parentPos, float size) { - int outputCount = this.prefab.outputs.Count; + // Determine the maximum value in this layer + // This is used to 'scale' the output value colors of the nuclei + float maxValue = 0; + int neuronCount = 0; + List 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 / outputCount; + float spacing = 400f / neuronCount; float margin = 10 + spacing / 2; int row = 0; - List drawnNuclei = new(); - foreach (Nucleus nucleus in this.prefab.outputs) { + 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)) @@ -459,97 +456,18 @@ namespace NanoBrain { 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 = neuron.outputMagnitude / maxValue; + color = new Color(brightness, brightness, brightness, 1f); + } DrawNucleus(nucleus, pos, size, color); row++; } } - - protected void BuildLayers() { - // A temporary list to track what's been added to layers - this.layers = new(); - int layerIx = 0; - - if (this.currentNucleus == null) { - BuildAllOutputs(); - return; - } - - // Nucleus selectedNucleus = this.currentNucleus; - // if (selectedNucleus == null) - // return; - NeuroidLayer currentLayer = new() { ix = layerIx }; - - // Receivers layer - if (this.currentNucleus is Neuron selectedNeuron && selectedNeuron.receivers != null) { - foreach (Nucleus receiver in selectedNeuron.receivers) { - Nucleus outputNeuroid = receiver; - if (outputNeuroid != null) { - AddToLayer(currentLayer, outputNeuroid); - // Debug.Log($"layer {layerIx} nucleus {outputNeuroid.name}"); - } - } - } - - // Create next layer - if (currentLayer.neuroids.Count > 0) { - this.layers.Add(currentLayer); - layerIx++; - currentLayer = new() { ix = layerIx }; - } - - // Current Nucleus layer - AddToLayer(currentLayer, this.currentNucleus); - this.layers.Add(currentLayer); - // Debug.Log($"layer {layerIx} nucleus {selectedNucleus.name}"); - - // Create next layer - layerIx++; - currentLayer = new() { ix = layerIx }; - - // Synapses layer - if (this.currentNucleus.synapses != null) { - foreach (Synapse synapse in this.currentNucleus.synapses) { - Nucleus input = synapse.neuron; - AddToLayer(currentLayer, input); - // Debug.Log($"layer {layerIx} nucleus {input.name}"); - } - } - if (currentLayer.neuroids.Count > 0) { - this.layers.Add(currentLayer); - } - } - - protected void BuildAllOutputs() { - return; - // Debug.Log("Build all outputs"); - // this.layers = new(); - // int layerIx = 0; - // NeuroidLayer currentLayer = new() { ix = layerIx }; - - // // empty layer, as 'All outputs' is not a nucleus - - // layerIx++; - // currentLayer = new() {ix = layerIx}; - - // foreach (Nucleus nucleus in this.prefab.outputs) { - // AddToLayer(currentLayer, nucleus); - // } - } - - private void AddToLayer(NeuroidLayer layer, Nucleus nucleus) { - if (nucleus == null) - return; - layer.neuroids.Add(nucleus); - // Store its position - Vector2Int neuroidPosition = new(layer.ix, layer.neuroids.Count - 1); - neuroidPositions[nucleus] = neuroidPosition; - } - - #endregion Focus Graph protected void DrawNucleus(Nucleus nucleus, Vector3 position, float maxValue, float size) { @@ -593,7 +511,7 @@ namespace NanoBrain { fontStyle = FontStyle.Bold, }; - if (nucleus.parent is Cluster parentCluster && parentCluster != currentNucleus.parent) + 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); @@ -603,7 +521,7 @@ namespace NanoBrain { Vector3 labelPos = position - Vector3.down * (size + 5); // below neuron style.alignment = TextAnchor.UpperCenter; - if (nucleus.parent != null && nucleus.parent != currentNucleus.parent && nucleus.parent is Cluster parentCluster1) { + if (nucleus.parent != null && currentNucleus != null && nucleus.parent != currentNucleus.parent && nucleus.parent is Cluster parentCluster1) { // This neuron is part of another cluster parentCluster1.name ??= ""; string baseName = ""; @@ -764,7 +682,7 @@ namespace NanoBrain { 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; @@ -803,19 +721,17 @@ namespace NanoBrain { OnClusterClick(cluster); } } - else if (nucleus.parent != null && nucleus.parent != this.currentNucleus.parent) { + else if (nucleus.parent != null && this.currentNucleus != null && nucleus.parent != this.currentNucleus.parent) { // We go to a different cluster // select the cluster, not the neuron in the cluster this.currentNucleus = nucleus.parent; expandArray = false; - BuildLayers(); } else { this.currentNucleus = nucleus; if (this.currentNucleus is Neuron neuron && neuron.receivers.Count == 0) this.selectedOutput = this.currentNucleus; expandArray = false; - BuildLayers(); } } @@ -823,7 +739,7 @@ namespace NanoBrain { // May be used with storedPrefab... Selection.activeObject = subCluster.prefab; EditorGUIUtility.PingObject(subCluster.prefab); - ClusterViewer.previousPrefab = this.prefab; + ClusterViewer.previousPrefab = this.currentCluster.prefab; ClusterEditor newEditor = CreateEditor(subCluster.prefab) as ClusterEditor; } @@ -831,7 +747,6 @@ namespace NanoBrain { this.currentNucleus = null; this.selectedOutput = null; expandArray = false; - BuildLayers(); } #endregion Graph diff --git a/Runtime/Scripts/Brain.cs b/Runtime/Scripts/Brain.cs index 36fd6e7..acb28f3 100644 --- a/Runtime/Scripts/Brain.cs +++ b/Runtime/Scripts/Brain.cs @@ -20,23 +20,23 @@ namespace NanoBrain { /// public Cluster brain { get { - // if (brainInstance == null && brainPrefab != null) { - // brainInstance = new Cluster(brainPrefab) { - // name = brainPrefab.name - // }; - // } else if (brainInstance != null && brainPrefab == null) { - // brainInstance = null; - // } + 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; - } + // public Cluster InitializeBrain() { + // brainInstance = new Cluster(brainPrefab) { + // name = brainPrefab.name + // }; + // return brainInstance; + // } /// /// Update the weight for all Synapses coming from the Neuron with the given name diff --git a/Runtime/Scripts/Core/Cluster.cs b/Runtime/Scripts/Core/Cluster.cs index 525e296..0b89721 100644 --- a/Runtime/Scripts/Core/Cluster.cs +++ b/Runtime/Scripts/Core/Cluster.cs @@ -636,7 +636,7 @@ namespace NanoBrain { if (this._outputs == null) { this._outputs = new(); foreach (Nucleus nucleus in this.clusterNuclei) { - if (nucleus is Neuron neuron) // && neuron.receivers.Count == 0) + if (nucleus is Neuron neuron && neuron.receivers.Count == 0) this._outputs.Add(neuron); } } From 4f8a6abbe9379097ba7b12cdcb4955ccf439917e Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Thu, 23 Apr 2026 11:50:59 +0200 Subject: [PATCH 32/34] Improved (but not fixed) cross-cluster monitoring --- Editor/ClusterViewer.cs | 49 ++++++++++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index 1af3311..fabe732 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -245,7 +245,7 @@ namespace NanoBrain { float maxValue = 1; if (this.currentNucleus is Cluster cluster) { - float spacing = 400f / cluster.siblingClusters.Length; + float spacing = 400f / cluster.instanceCount; float margin = 10 + spacing / 2; float xMin = 150 - size; float xMax = 150 + size; @@ -260,14 +260,24 @@ namespace NanoBrain { Handles.color = Color.black; Handles.DrawAAConvexPolygon(verts); int row = 0; - foreach (Cluster sibling in cluster.siblingClusters) { + if (cluster.siblingClusters == 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(sibling, pos, maxValue, size); + DrawNucleus(cluster, pos, maxValue, size); row++; } + else { + foreach (Cluster sibling in cluster.siblingClusters) { + 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 }, @@ -316,11 +326,14 @@ namespace NanoBrain { else return; - // For top-level nodes, add link to previous editor or 'all-outputs' + // For top-level nodes, add link to previous editor and/or 'Outputs' int nodeCount = receivers.Count(); - if (nucleus == this.selectedOutput) {// && ClusterViewer.previousPrefab != null) { - // Add link to previous editor + 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 @@ -353,11 +366,14 @@ namespace NanoBrain { } if (nucleus == this.selectedOutput) { Vector3 pos = new(50, margin + row * spacing, 0); - DrawEdge(parentPos, pos); - if (ClusterViewer.previousPrefab != null) + if (ClusterViewer.previousPrefab != null) { + DrawEdge(parentPos, pos); DrawClusterPrefab(ClusterViewer.previousPrefab, pos, size); - else - DrawAllOutputs(pos, size); + row++; + } + pos = new(50, margin + row * spacing, 0); + DrawEdge(parentPos, pos); + DrawAllOutputs(pos, size); } } @@ -658,7 +674,7 @@ namespace NanoBrain { fontStyle = FontStyle.Bold, alignment = TextAnchor.MiddleCenter, }; - Handles.Label(position, "All\noutputs", labelTextStyle); + Handles.Label(position, "Outputs", labelTextStyle); Rect neuronRect = new(position.x - size, position.y - size, size * 2, size * 2); Event e = Event.current; @@ -711,8 +727,15 @@ namespace NanoBrain { protected void OnNeuronClick(Nucleus nucleus) { if (nucleus == this.currentNucleus) { if (Application.isPlaying) { - if (nucleus is Cluster) - expandArray = !expandArray; + if (nucleus is Cluster cluster) { + if (expandArray) { + this.currentNucleus = cluster.defaultOutput; + if (this.currentNucleus is Neuron neuron && neuron.receivers.Count == 0) + this.selectedOutput = this.currentNucleus; expandArray = false; + } + else + expandArray = !expandArray; + } else expandArray = false; } From e4ba7f8497c9651b8229f300c4981df8c1e9217c Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Thu, 23 Apr 2026 11:55:17 +0200 Subject: [PATCH 33/34] Better cross-cluster monitoring --- Editor/ClusterViewer.cs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/Editor/ClusterViewer.cs b/Editor/ClusterViewer.cs index fabe732..af0eb48 100644 --- a/Editor/ClusterViewer.cs +++ b/Editor/ClusterViewer.cs @@ -727,15 +727,8 @@ namespace NanoBrain { protected void OnNeuronClick(Nucleus nucleus) { if (nucleus == this.currentNucleus) { if (Application.isPlaying) { - if (nucleus is Cluster cluster) { - if (expandArray) { - this.currentNucleus = cluster.defaultOutput; - if (this.currentNucleus is Neuron neuron && neuron.receivers.Count == 0) - this.selectedOutput = this.currentNucleus; expandArray = false; - } - else - expandArray = !expandArray; - } + if (nucleus is Cluster) + expandArray = !expandArray; else expandArray = false; } @@ -746,9 +739,17 @@ namespace NanoBrain { } else if (nucleus.parent != null && this.currentNucleus != null && nucleus.parent != this.currentNucleus.parent) { // We go to a different cluster - // select the cluster, not the neuron in the cluster - this.currentNucleus = nucleus.parent; - expandArray = false; + if (Application.isPlaying) { + this.currentNucleus = nucleus; + if (this.currentNucleus is Neuron neuron && neuron.receivers.Count == 0) + this.selectedOutput = this.currentNucleus; + expandArray = false; + } + else { + // select the cluster, not the neuron in the cluster + this.currentNucleus = nucleus.parent; + expandArray = false; + } } else { this.currentNucleus = nucleus; From cc9a845b643ffb4a9abe4f7da787ac5c5b14dae8 Mon Sep 17 00:00:00 2001 From: Pascal Serrarens Date: Thu, 23 Apr 2026 12:17:05 +0200 Subject: [PATCH 34/34] Fix sleeping for product combinator --- Runtime/Scripts/Core/Neuron.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Runtime/Scripts/Core/Neuron.cs b/Runtime/Scripts/Core/Neuron.cs index 8e47dc8..d0f9473 100644 --- a/Runtime/Scripts/Core/Neuron.cs +++ b/Runtime/Scripts/Core/Neuron.cs @@ -222,11 +222,10 @@ namespace NanoBrain { public void SleepCheck() { if (this.isSleeping) { #if UNITY_MATHEMATICS - this.bias = new float3(0, 0, 0); + this._outputValue = new float3(0, 0, 0); #else - this.bias = new Vector3(0,0,0); + this._outputValue = new Vector3(0,0,0); #endif - this._outputValue = this.bias; } } @@ -526,12 +525,13 @@ namespace NanoBrain { public virtual void RemoveReceiver(Nucleus receiverToRemove) { this._receivers.RemoveAll(receiver => receiver == receiverToRemove); receiverToRemove.synapses.RemoveAll(synapse => synapse.neuron == this); - } + } #endregion Receivers - public override void ProcessStimulus(Vector3 inputValue) {; + public override void ProcessStimulus(Vector3 inputValue) { + ; this.lastUpdate = Time.time; this.bias = inputValue; this.parent.UpdateFromNucleus(this);