Spaces:
Sleeping
Sleeping
dixiebone13-a11y commited on
Commit ·
4ad4378
0
Parent(s):
Synapse Link HEBB-9 - Hebbian learning neural sim
Browse files- .gitignore +4 -0
- Dockerfile +26 -0
- README.md +34 -0
- index.html +12 -0
- package.json +25 -0
- postcss.config.js +6 -0
- src/App.jsx +438 -0
- src/components/GridCell.jsx +38 -0
- src/index.css +3 -0
- src/main.jsx +10 -0
- tailwind.config.js +8 -0
- vite.config.js +10 -0
.gitignore
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
dist
|
| 3 |
+
.DS_Store
|
| 4 |
+
*.local
|
Dockerfile
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-slim AS build
|
| 2 |
+
WORKDIR /app
|
| 3 |
+
COPY package*.json ./
|
| 4 |
+
RUN npm install
|
| 5 |
+
COPY . .
|
| 6 |
+
RUN npm run build
|
| 7 |
+
|
| 8 |
+
FROM nginx:alpine
|
| 9 |
+
COPY --from=build /app/dist /usr/share/nginx/html
|
| 10 |
+
EXPOSE 7860
|
| 11 |
+
CMD ["nginx", "-g", "daemon off;", "-c", "/etc/nginx/nginx.conf"]
|
| 12 |
+
COPY <<'EOF' /etc/nginx/nginx.conf
|
| 13 |
+
worker_processes auto;
|
| 14 |
+
events { worker_connections 1024; }
|
| 15 |
+
http {
|
| 16 |
+
include /etc/nginx/mime.types;
|
| 17 |
+
server {
|
| 18 |
+
listen 7860;
|
| 19 |
+
root /usr/share/nginx/html;
|
| 20 |
+
index index.html;
|
| 21 |
+
location / {
|
| 22 |
+
try_files $uri $uri/ /index.html;
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
EOF
|
README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Synapse Link
|
| 3 |
+
emoji: 🧠
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: red
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# Synapse_Link HEBB-9
|
| 12 |
+
|
| 13 |
+
Interactive Hebbian Learning neural network simulation. Watch synaptic connections strengthen and decay in real-time as the memory grid learns to reconstruct the input signal.
|
| 14 |
+
|
| 15 |
+
## Controls
|
| 16 |
+
|
| 17 |
+
- **Click grid cells** to toggle stimulus neurons
|
| 18 |
+
- **Sync Source** — Generate a random input pattern
|
| 19 |
+
- **Enable Drift** — Shift the signal pattern over time
|
| 20 |
+
- **Purge** — Reset all grids and learned weights
|
| 21 |
+
|
| 22 |
+
## Parameters
|
| 23 |
+
|
| 24 |
+
| Param | Effect |
|
| 25 |
+
|-------|--------|
|
| 26 |
+
| SNR_Amp | Signal-to-noise strength — higher = cleaner memory encoding |
|
| 27 |
+
| Refresh_Hz | Simulation tick rate |
|
| 28 |
+
| Persistence | How long inactive cells retain their state |
|
| 29 |
+
| Cross_Talk | Spatial neighbor influence (like lateral inhibition) |
|
| 30 |
+
| Plasticity | Hebbian learning rate — how fast weights adapt |
|
| 31 |
+
|
| 32 |
+
## Tech
|
| 33 |
+
|
| 34 |
+
React 18 + Vite + Tailwind CSS, deployed as Docker on HuggingFace Spaces.
|
index.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>Synapse_Link HEBB-9</title>
|
| 7 |
+
</head>
|
| 8 |
+
<body>
|
| 9 |
+
<div id="root"></div>
|
| 10 |
+
<script type="module" src="/src/main.jsx"></script>
|
| 11 |
+
</body>
|
| 12 |
+
</html>
|
package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "synapse-link",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "1.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"preview": "vite preview"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"lucide-react": "^0.454.0",
|
| 13 |
+
"react": "^18.3.1",
|
| 14 |
+
"react-dom": "^18.3.1"
|
| 15 |
+
},
|
| 16 |
+
"devDependencies": {
|
| 17 |
+
"@types/react": "^18.3.12",
|
| 18 |
+
"@types/react-dom": "^18.3.1",
|
| 19 |
+
"@vitejs/plugin-react": "^4.3.4",
|
| 20 |
+
"autoprefixer": "^10.4.20",
|
| 21 |
+
"postcss": "^8.4.49",
|
| 22 |
+
"tailwindcss": "^3.4.15",
|
| 23 |
+
"vite": "^6.0.2"
|
| 24 |
+
}
|
| 25 |
+
}
|
postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
};
|
src/App.jsx
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
| 2 |
+
import {
|
| 3 |
+
Zap, Trash2, RefreshCw, Waves,
|
| 4 |
+
ShieldAlert, Binary, BrainCircuit, Dna
|
| 5 |
+
} from 'lucide-react';
|
| 6 |
+
import GridCell from './components/GridCell';
|
| 7 |
+
|
| 8 |
+
const GRID_SIZE = 8;
|
| 9 |
+
const CELL_COUNT = GRID_SIZE * GRID_SIZE;
|
| 10 |
+
const MAX_HISTORY = 50;
|
| 11 |
+
|
| 12 |
+
// Pre-allocate empty arrays to avoid recreating on every clear
|
| 13 |
+
const EMPTY_BOOLS = Object.freeze(Array(CELL_COUNT).fill(false));
|
| 14 |
+
const EMPTY_WEIGHTS = Object.freeze(Array(CELL_COUNT).fill(0));
|
| 15 |
+
const FULL_HISTORY = Object.freeze(Array(MAX_HISTORY).fill(100));
|
| 16 |
+
|
| 17 |
+
const App = () => {
|
| 18 |
+
// ── Render state (drives UI) ──
|
| 19 |
+
const [signalGrid, setSignalGrid] = useState([...EMPTY_BOOLS]);
|
| 20 |
+
const [memoryGrid, setMemoryGrid] = useState([...EMPTY_BOOLS]);
|
| 21 |
+
const [weights, setWeights] = useState([...EMPTY_WEIGHTS]);
|
| 22 |
+
const [accuracy, setAccuracy] = useState(100);
|
| 23 |
+
const [history, setHistory] = useState([...FULL_HISTORY]);
|
| 24 |
+
const [glitch, setGlitch] = useState(false);
|
| 25 |
+
const [isDrifting, setIsDrifting] = useState(false);
|
| 26 |
+
|
| 27 |
+
// Slider state (UI display)
|
| 28 |
+
const [strength, setStrength] = useState(85);
|
| 29 |
+
const [frequency, setFrequency] = useState(50);
|
| 30 |
+
const [persistence, setPersistence] = useState(30);
|
| 31 |
+
const [crossTalk, setCrossTalk] = useState(20);
|
| 32 |
+
const [plasticity, setPlasticity] = useState(40);
|
| 33 |
+
|
| 34 |
+
// ── Refs (simulation reads from these — no dependency churn) ──
|
| 35 |
+
const signalRef = useRef(signalGrid);
|
| 36 |
+
const memoryRef = useRef(memoryGrid);
|
| 37 |
+
const weightsRef = useRef(weights);
|
| 38 |
+
const strengthRef = useRef(strength);
|
| 39 |
+
const frequencyRef = useRef(frequency);
|
| 40 |
+
const persistenceRef = useRef(persistence);
|
| 41 |
+
const crossTalkRef = useRef(crossTalk);
|
| 42 |
+
const plasticityRef = useRef(plasticity);
|
| 43 |
+
const isDriftingRef = useRef(isDrifting);
|
| 44 |
+
|
| 45 |
+
const simTimerRef = useRef(null);
|
| 46 |
+
const driftTimerRef = useRef(null);
|
| 47 |
+
const waveCanvasRef = useRef(null);
|
| 48 |
+
const waveFrameRef = useRef(null);
|
| 49 |
+
const waveFrameCount = useRef(0);
|
| 50 |
+
const accuracyRef = useRef(100);
|
| 51 |
+
const containerRef = useRef(null);
|
| 52 |
+
|
| 53 |
+
// Keep refs in sync with state
|
| 54 |
+
useEffect(() => { signalRef.current = signalGrid; }, [signalGrid]);
|
| 55 |
+
useEffect(() => { strengthRef.current = strength; }, [strength]);
|
| 56 |
+
useEffect(() => { frequencyRef.current = frequency; }, [frequency]);
|
| 57 |
+
useEffect(() => { persistenceRef.current = persistence; }, [persistence]);
|
| 58 |
+
useEffect(() => { crossTalkRef.current = crossTalk; }, [crossTalk]);
|
| 59 |
+
useEffect(() => { plasticityRef.current = plasticity; }, [plasticity]);
|
| 60 |
+
useEffect(() => { isDriftingRef.current = isDrifting; }, [isDrifting]);
|
| 61 |
+
|
| 62 |
+
// ── Actions ──
|
| 63 |
+
const generatePattern = useCallback(() => {
|
| 64 |
+
const newGrid = Array.from({ length: CELL_COUNT }, () => Math.random() > 0.8);
|
| 65 |
+
setSignalGrid(newGrid);
|
| 66 |
+
signalRef.current = newGrid;
|
| 67 |
+
setGlitch(true);
|
| 68 |
+
setTimeout(() => setGlitch(false), 150);
|
| 69 |
+
}, []);
|
| 70 |
+
|
| 71 |
+
const clearGrids = useCallback(() => {
|
| 72 |
+
const s = [...EMPTY_BOOLS];
|
| 73 |
+
const m = [...EMPTY_BOOLS];
|
| 74 |
+
const w = [...EMPTY_WEIGHTS];
|
| 75 |
+
setSignalGrid(s);
|
| 76 |
+
setMemoryGrid(m);
|
| 77 |
+
setWeights(w);
|
| 78 |
+
setHistory([...FULL_HISTORY]);
|
| 79 |
+
signalRef.current = s;
|
| 80 |
+
memoryRef.current = m;
|
| 81 |
+
weightsRef.current = w;
|
| 82 |
+
accuracyRef.current = 100;
|
| 83 |
+
}, []);
|
| 84 |
+
|
| 85 |
+
const toggleBlock = useCallback((index) => {
|
| 86 |
+
setSignalGrid(prev => {
|
| 87 |
+
const next = [...prev];
|
| 88 |
+
next[index] = !next[index];
|
| 89 |
+
signalRef.current = next;
|
| 90 |
+
return next;
|
| 91 |
+
});
|
| 92 |
+
}, []);
|
| 93 |
+
|
| 94 |
+
// ── Signal Drift ──
|
| 95 |
+
useEffect(() => {
|
| 96 |
+
const runDrift = () => {
|
| 97 |
+
if (!isDriftingRef.current) return;
|
| 98 |
+
setSignalGrid(prev => {
|
| 99 |
+
const next = [...prev];
|
| 100 |
+
const last = next.pop();
|
| 101 |
+
next.unshift(last);
|
| 102 |
+
signalRef.current = next;
|
| 103 |
+
return next;
|
| 104 |
+
});
|
| 105 |
+
};
|
| 106 |
+
|
| 107 |
+
// Restart drift interval when frequency or drift toggle changes
|
| 108 |
+
clearInterval(driftTimerRef.current);
|
| 109 |
+
if (isDrifting) {
|
| 110 |
+
driftTimerRef.current = setInterval(runDrift, 2000 - frequency * 15);
|
| 111 |
+
}
|
| 112 |
+
return () => clearInterval(driftTimerRef.current);
|
| 113 |
+
}, [isDrifting, frequency]);
|
| 114 |
+
|
| 115 |
+
// ── Main Simulation Loop (runs once on mount) ──
|
| 116 |
+
useEffect(() => {
|
| 117 |
+
const tick = () => {
|
| 118 |
+
const signal = signalRef.current;
|
| 119 |
+
const prevMemory = memoryRef.current;
|
| 120 |
+
const prevWeights = weightsRef.current;
|
| 121 |
+
const str = strengthRef.current;
|
| 122 |
+
const freq = frequencyRef.current;
|
| 123 |
+
const pers = persistenceRef.current;
|
| 124 |
+
const ct = crossTalkRef.current;
|
| 125 |
+
const plast = plasticityRef.current;
|
| 126 |
+
|
| 127 |
+
const baseSnrThreshold = (100 - str) / 100;
|
| 128 |
+
|
| 129 |
+
// Build next memory
|
| 130 |
+
const nextGrid = new Array(CELL_COUNT);
|
| 131 |
+
for (let i = 0; i < CELL_COUNT; i++) {
|
| 132 |
+
const noise = Math.random();
|
| 133 |
+
const shouldUpdate = Math.random() < freq / 100;
|
| 134 |
+
|
| 135 |
+
if (!shouldUpdate) {
|
| 136 |
+
nextGrid[i] = prevMemory[i];
|
| 137 |
+
continue;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
const localWeight = prevWeights[i] * (plast / 100);
|
| 141 |
+
const effectiveThreshold = Math.max(0.01, baseSnrThreshold - localWeight);
|
| 142 |
+
|
| 143 |
+
if (signal[i]) {
|
| 144 |
+
nextGrid[i] = noise > effectiveThreshold;
|
| 145 |
+
} else if (prevMemory[i] && Math.random() < pers / 100) {
|
| 146 |
+
nextGrid[i] = true;
|
| 147 |
+
} else {
|
| 148 |
+
nextGrid[i] = noise < effectiveThreshold / 20;
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
// Spatial cross-talk
|
| 153 |
+
let finalGrid = nextGrid;
|
| 154 |
+
if (ct > 0) {
|
| 155 |
+
finalGrid = new Array(CELL_COUNT);
|
| 156 |
+
for (let i = 0; i < CELL_COUNT; i++) {
|
| 157 |
+
if (nextGrid[i]) {
|
| 158 |
+
finalGrid[i] = true;
|
| 159 |
+
continue;
|
| 160 |
+
}
|
| 161 |
+
const row = (i / GRID_SIZE) | 0;
|
| 162 |
+
const col = i % GRID_SIZE;
|
| 163 |
+
let activeNeighbors = 0;
|
| 164 |
+
for (let r = -1; r <= 1; r++) {
|
| 165 |
+
for (let c = -1; c <= 1; c++) {
|
| 166 |
+
if (r === 0 && c === 0) continue;
|
| 167 |
+
const nr = row + r, nc = col + c;
|
| 168 |
+
if (nr >= 0 && nr < GRID_SIZE && nc >= 0 && nc < GRID_SIZE) {
|
| 169 |
+
if (nextGrid[nr * GRID_SIZE + nc]) activeNeighbors++;
|
| 170 |
+
}
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
finalGrid[i] = Math.random() < activeNeighbors * (ct / 1000);
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
// Hebbian weight update
|
| 178 |
+
const nextWeights = new Array(CELL_COUNT);
|
| 179 |
+
for (let i = 0; i < CELL_COUNT; i++) {
|
| 180 |
+
if (signal[i] && finalGrid[i]) {
|
| 181 |
+
nextWeights[i] = Math.min(1, prevWeights[i] + 0.02);
|
| 182 |
+
} else if (!signal[i] && !finalGrid[i]) {
|
| 183 |
+
nextWeights[i] = prevWeights[i];
|
| 184 |
+
} else {
|
| 185 |
+
nextWeights[i] = Math.max(0, prevWeights[i] - 0.01);
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
// Accuracy
|
| 190 |
+
let matches = 0;
|
| 191 |
+
for (let i = 0; i < CELL_COUNT; i++) {
|
| 192 |
+
if (signal[i] === finalGrid[i]) matches++;
|
| 193 |
+
}
|
| 194 |
+
const acc = Math.round((matches / CELL_COUNT) * 100);
|
| 195 |
+
|
| 196 |
+
// Commit to refs
|
| 197 |
+
memoryRef.current = finalGrid;
|
| 198 |
+
weightsRef.current = nextWeights;
|
| 199 |
+
accuracyRef.current = acc;
|
| 200 |
+
|
| 201 |
+
// Batch state updates
|
| 202 |
+
setMemoryGrid(finalGrid);
|
| 203 |
+
setWeights(nextWeights);
|
| 204 |
+
setAccuracy(acc);
|
| 205 |
+
setHistory(prev => {
|
| 206 |
+
const next = [...prev];
|
| 207 |
+
next.shift();
|
| 208 |
+
next.push(acc);
|
| 209 |
+
return next;
|
| 210 |
+
});
|
| 211 |
+
};
|
| 212 |
+
|
| 213 |
+
// Adaptive tick rate — re-read frequency ref each cycle
|
| 214 |
+
const scheduleNext = () => {
|
| 215 |
+
const intervalTime = Math.max(16, 1000 - frequencyRef.current * 9.8);
|
| 216 |
+
simTimerRef.current = setTimeout(() => {
|
| 217 |
+
tick();
|
| 218 |
+
scheduleNext();
|
| 219 |
+
}, intervalTime);
|
| 220 |
+
};
|
| 221 |
+
|
| 222 |
+
scheduleNext();
|
| 223 |
+
return () => clearTimeout(simTimerRef.current);
|
| 224 |
+
}, []); // mount-only — reads everything from refs
|
| 225 |
+
|
| 226 |
+
// ── Waveform Canvas (ResizeObserver + proper rAF cleanup) ──
|
| 227 |
+
useEffect(() => {
|
| 228 |
+
const canvas = waveCanvasRef.current;
|
| 229 |
+
if (!canvas) return;
|
| 230 |
+
const ctx = canvas.getContext('2d');
|
| 231 |
+
const container = canvas.parentElement;
|
| 232 |
+
|
| 233 |
+
// Sync canvas resolution to container
|
| 234 |
+
const resizeCanvas = () => {
|
| 235 |
+
const rect = container.getBoundingClientRect();
|
| 236 |
+
const dpr = window.devicePixelRatio || 1;
|
| 237 |
+
canvas.width = rect.width * dpr;
|
| 238 |
+
canvas.height = rect.height * dpr;
|
| 239 |
+
ctx.scale(dpr, dpr);
|
| 240 |
+
canvas.style.width = rect.width + 'px';
|
| 241 |
+
canvas.style.height = rect.height + 'px';
|
| 242 |
+
};
|
| 243 |
+
|
| 244 |
+
const ro = new ResizeObserver(resizeCanvas);
|
| 245 |
+
ro.observe(container);
|
| 246 |
+
resizeCanvas();
|
| 247 |
+
|
| 248 |
+
const animate = () => {
|
| 249 |
+
waveFrameCount.current++;
|
| 250 |
+
const frame = waveFrameCount.current;
|
| 251 |
+
const w = canvas.width / (window.devicePixelRatio || 1);
|
| 252 |
+
const h = canvas.height / (window.devicePixelRatio || 1);
|
| 253 |
+
|
| 254 |
+
ctx.clearRect(0, 0, w, h);
|
| 255 |
+
ctx.beginPath();
|
| 256 |
+
ctx.strokeStyle = accuracyRef.current < 50 ? '#ff0055' : '#00ff9f';
|
| 257 |
+
ctx.lineWidth = 2;
|
| 258 |
+
|
| 259 |
+
const freq = frequencyRef.current;
|
| 260 |
+
const str = strengthRef.current;
|
| 261 |
+
const freqMod = freq / 20;
|
| 262 |
+
const ampMod = str / 4;
|
| 263 |
+
const noiseMod = (100 - str) / 5;
|
| 264 |
+
|
| 265 |
+
for (let x = 0; x < w; x++) {
|
| 266 |
+
const y = h / 2 +
|
| 267 |
+
Math.sin(x * 0.05 * freqMod + frame * 0.1) * ampMod +
|
| 268 |
+
Math.sin(x * 0.1 + frame * 0.2) * noiseMod;
|
| 269 |
+
if (x === 0) ctx.moveTo(x, y);
|
| 270 |
+
else ctx.lineTo(x, y);
|
| 271 |
+
}
|
| 272 |
+
ctx.stroke();
|
| 273 |
+
|
| 274 |
+
waveFrameRef.current = requestAnimationFrame(animate);
|
| 275 |
+
};
|
| 276 |
+
|
| 277 |
+
waveFrameRef.current = requestAnimationFrame(animate);
|
| 278 |
+
|
| 279 |
+
return () => {
|
| 280 |
+
cancelAnimationFrame(waveFrameRef.current);
|
| 281 |
+
ro.disconnect();
|
| 282 |
+
};
|
| 283 |
+
}, []);
|
| 284 |
+
|
| 285 |
+
// ── Slider helpers ──
|
| 286 |
+
const sliderHandler = useCallback((setter, ref) => (e) => {
|
| 287 |
+
const v = parseInt(e.target.value);
|
| 288 |
+
setter(v);
|
| 289 |
+
ref.current = v;
|
| 290 |
+
}, []);
|
| 291 |
+
|
| 292 |
+
return (
|
| 293 |
+
<div className={`min-h-screen p-4 md:p-6 font-mono selection:bg-[#00ff9f] selection:text-black transition-colors duration-500 ${accuracy < 25 ? 'bg-[#200] text-[#ff0055]' : 'bg-[#020202] text-[#00ff9f]'}`}>
|
| 294 |
+
{/* Scanline overlay */}
|
| 295 |
+
<div className="fixed inset-0 pointer-events-none z-50 opacity-[0.05] bg-[linear-gradient(rgba(18,16,16,0)_50%,rgba(0,0,0,0.25)_50%),linear-gradient(90deg,rgba(255,0,0,0.06),rgba(0,255,0,0.02),rgba(0,0,255,0.06))] bg-[length:100%_2px,3px_100%]" />
|
| 296 |
+
|
| 297 |
+
<div className="max-w-7xl mx-auto space-y-6">
|
| 298 |
+
{/* Header */}
|
| 299 |
+
<header className="flex flex-col md:flex-row items-start md:items-center justify-between p-6 bg-[#0a0a0a] border-2 border-[#00ff9f]/20 rounded-sm relative overflow-hidden">
|
| 300 |
+
<div className="z-10 flex items-center gap-5">
|
| 301 |
+
<div className={`p-3 rounded-none shadow-lg transition-colors ${accuracy < 25 ? 'bg-[#ff0055] shadow-[#ff0055]' : 'bg-[#00ff9f] shadow-[#00ff9f]'}`}>
|
| 302 |
+
<BrainCircuit size={32} className="text-black" />
|
| 303 |
+
</div>
|
| 304 |
+
<div>
|
| 305 |
+
<h1 className={`text-4xl font-black uppercase tracking-tighter ${glitch || accuracy < 25 ? 'animate-pulse' : ''}`}>
|
| 306 |
+
Synapse_Link <span className="text-white text-2xl font-light opacity-50">HEBB-9</span>
|
| 307 |
+
</h1>
|
| 308 |
+
<div className="flex items-center gap-3 text-[10px] mt-1 font-bold opacity-60 uppercase">
|
| 309 |
+
<span className="flex items-center gap-1"><Dna size={12} /> PLASTICITY: ACTIVE</span>
|
| 310 |
+
<span className={`flex items-center gap-1 ${accuracy < 40 ? 'text-[#ff0055] animate-bounce' : ''}`}>
|
| 311 |
+
<ShieldAlert size={12} /> {accuracy < 25 ? 'CRITICAL DECOHERENCE' : 'SIGNAL STABLE'}
|
| 312 |
+
</span>
|
| 313 |
+
</div>
|
| 314 |
+
</div>
|
| 315 |
+
</div>
|
| 316 |
+
|
| 317 |
+
<div className="mt-4 md:mt-0 flex flex-wrap gap-3">
|
| 318 |
+
<button
|
| 319 |
+
onClick={() => setIsDrifting(d => !d)}
|
| 320 |
+
className={`px-6 py-3 font-black uppercase tracking-widest text-xs transition-all flex items-center gap-2 ${isDrifting ? 'bg-white text-black' : 'border border-[#00ff9f] text-[#00ff9f]'}`}
|
| 321 |
+
>
|
| 322 |
+
<Waves size={16} /> {isDrifting ? 'Stop Drift' : 'Enable Drift'}
|
| 323 |
+
</button>
|
| 324 |
+
<button onClick={generatePattern} className="bg-[#00ff9f] text-black px-6 py-3 font-black uppercase tracking-widest text-xs hover:bg-white transition-all shadow-md flex items-center gap-2">
|
| 325 |
+
<RefreshCw size={16} /> Sync Source
|
| 326 |
+
</button>
|
| 327 |
+
<button onClick={clearGrids} className="border border-[#ff0055] text-[#ff0055] px-6 py-3 font-black uppercase tracking-widest text-xs hover:bg-[#ff0055] hover:text-black transition-all flex items-center gap-2">
|
| 328 |
+
<Trash2 size={16} /> Purge
|
| 329 |
+
</button>
|
| 330 |
+
</div>
|
| 331 |
+
</header>
|
| 332 |
+
|
| 333 |
+
{/* Waveform + Fidelity */}
|
| 334 |
+
<div className="grid lg:grid-cols-4 gap-6">
|
| 335 |
+
<div className="lg:col-span-3 bg-[#0a0a0a] border border-[#00ff9f]/20 p-4 relative h-32" ref={containerRef}>
|
| 336 |
+
<div className="absolute top-2 left-2 text-[8px] opacity-40 uppercase font-black">Bio_Feedback_Wave</div>
|
| 337 |
+
<canvas ref={waveCanvasRef} className="w-full h-full" />
|
| 338 |
+
</div>
|
| 339 |
+
<div className={`bg-[#0a0a0a] border border-[#00ff9f]/20 p-4 flex flex-col justify-center items-center ${accuracy < 25 ? 'border-[#ff0055]' : ''}`}>
|
| 340 |
+
<div className="text-[10px] uppercase opacity-40 font-black mb-1">Engram Fidelity</div>
|
| 341 |
+
<div className={`text-4xl font-black italic tracking-tighter ${accuracy < 60 ? 'text-[#ff0055]' : 'text-white'}`}>
|
| 342 |
+
{accuracy}.0
|
| 343 |
+
</div>
|
| 344 |
+
<div className="w-full bg-[#111] h-1 mt-2">
|
| 345 |
+
<div className="bg-[#00ff9f] h-full transition-all" style={{ width: `${accuracy}%` }} />
|
| 346 |
+
</div>
|
| 347 |
+
</div>
|
| 348 |
+
</div>
|
| 349 |
+
|
| 350 |
+
{/* Grids */}
|
| 351 |
+
<div className="grid lg:grid-cols-2 gap-8">
|
| 352 |
+
{/* Signal Input */}
|
| 353 |
+
<div className="bg-[#0a0a0a] border-l-4 border-[#00ff9f] p-6 relative">
|
| 354 |
+
<div className="flex items-center justify-between mb-6">
|
| 355 |
+
<h3 className="font-black uppercase tracking-widest text-white flex items-center gap-2">
|
| 356 |
+
<Zap size={18} className="text-[#00ff9f]" /> Stimulus Source
|
| 357 |
+
</h3>
|
| 358 |
+
<div className="text-[10px] opacity-40 font-bold">MANUAL_OVERRIDE_ENABLED</div>
|
| 359 |
+
</div>
|
| 360 |
+
<div className="grid grid-cols-8 gap-2 aspect-square bg-black/40 p-3 border border-[#00ff9f]/10" role="grid" aria-label="Stimulus grid">
|
| 361 |
+
{signalGrid.map((active, i) => (
|
| 362 |
+
<GridCell key={i} active={active} variant="signal" weight={0} mismatch={false} onClick={() => toggleBlock(i)} />
|
| 363 |
+
))}
|
| 364 |
+
</div>
|
| 365 |
+
</div>
|
| 366 |
+
|
| 367 |
+
{/* Memory Output */}
|
| 368 |
+
<div className="bg-[#0a0a0a] border-r-4 border-[#ff0055] p-6 relative">
|
| 369 |
+
<div className="flex items-center justify-between mb-6">
|
| 370 |
+
<h3 className="font-black uppercase tracking-widest text-white flex items-center gap-2">
|
| 371 |
+
<BrainCircuit size={18} className="text-[#ff0055]" /> Synaptic Cache
|
| 372 |
+
</h3>
|
| 373 |
+
<div className="flex items-center gap-2 text-[8px] font-bold">
|
| 374 |
+
<div className="w-2 h-2 bg-white rounded-full animate-ping" />
|
| 375 |
+
LEARNING_ACTIVE
|
| 376 |
+
</div>
|
| 377 |
+
</div>
|
| 378 |
+
<div className="grid grid-cols-8 gap-2 aspect-square bg-black/40 p-3 border border-[#ff0055]/10" role="grid" aria-label="Synaptic memory grid">
|
| 379 |
+
{memoryGrid.map((active, i) => (
|
| 380 |
+
<GridCell key={i} active={active} variant="memory" weight={weights[i]} mismatch={signalGrid[i] !== active} />
|
| 381 |
+
))}
|
| 382 |
+
</div>
|
| 383 |
+
</div>
|
| 384 |
+
</div>
|
| 385 |
+
|
| 386 |
+
{/* Sliders */}
|
| 387 |
+
<div className="grid md:grid-cols-3 lg:grid-cols-5 gap-4">
|
| 388 |
+
<div className="bg-[#0a0a0a] p-4 border border-[#00ff9f]/20">
|
| 389 |
+
<label className="flex justify-between text-[10px] font-black uppercase mb-3">
|
| 390 |
+
<span>SNR_Amp</span><span>{strength}%</span>
|
| 391 |
+
</label>
|
| 392 |
+
<input type="range" min="0" max="100" value={strength} onChange={sliderHandler(setStrength, strengthRef)} className="w-full accent-[#00ff9f]" />
|
| 393 |
+
</div>
|
| 394 |
+
<div className="bg-[#0a0a0a] p-4 border border-[#00ff9f]/20">
|
| 395 |
+
<label className="flex justify-between text-[10px] font-black uppercase mb-3">
|
| 396 |
+
<span>Refresh_Hz</span><span>{frequency}%</span>
|
| 397 |
+
</label>
|
| 398 |
+
<input type="range" min="1" max="100" value={frequency} onChange={sliderHandler(setFrequency, frequencyRef)} className="w-full accent-[#00ff9f]" />
|
| 399 |
+
</div>
|
| 400 |
+
<div className="bg-[#0a0a0a] p-4 border border-[#ff0055]/20">
|
| 401 |
+
<label className="flex justify-between text-[10px] font-black uppercase mb-3 text-[#ff0055]">
|
| 402 |
+
<span>Persistence</span><span>{persistence}%</span>
|
| 403 |
+
</label>
|
| 404 |
+
<input type="range" min="0" max="100" value={persistence} onChange={sliderHandler(setPersistence, persistenceRef)} className="w-full accent-[#ff0055]" />
|
| 405 |
+
</div>
|
| 406 |
+
<div className="bg-[#0a0a0a] p-4 border border-[#ff0055]/20">
|
| 407 |
+
<label className="flex justify-between text-[10px] font-black uppercase mb-3 text-[#ff0055]">
|
| 408 |
+
<span>Cross_Talk</span><span>{crossTalk}%</span>
|
| 409 |
+
</label>
|
| 410 |
+
<input type="range" min="0" max="100" value={crossTalk} onChange={sliderHandler(setCrossTalk, crossTalkRef)} className="w-full accent-[#ff0055]" />
|
| 411 |
+
</div>
|
| 412 |
+
<div className="bg-[#111] p-4 border border-white/20">
|
| 413 |
+
<label className="flex justify-between text-[10px] font-black uppercase mb-3 text-white">
|
| 414 |
+
<span>Plasticity</span><span>{plasticity}%</span>
|
| 415 |
+
</label>
|
| 416 |
+
<input type="range" min="0" max="100" value={plasticity} onChange={sliderHandler(setPlasticity, plasticityRef)} className="w-full accent-white" />
|
| 417 |
+
</div>
|
| 418 |
+
</div>
|
| 419 |
+
|
| 420 |
+
{/* Telemetry Graph */}
|
| 421 |
+
<div className="bg-[#0a0a0a] border border-[#00ff9f]/10 p-4 h-20 flex items-end gap-1 relative overflow-hidden" aria-label="Fidelity history">
|
| 422 |
+
<div className="absolute inset-0 bg-gradient-to-t from-[#ff0055]/10 to-transparent pointer-events-none" />
|
| 423 |
+
{history.map((val, i) => (
|
| 424 |
+
<div key={i} className={`flex-1 transition-all ${val < 50 ? 'bg-[#ff0055]' : 'bg-[#00ff9f]/40'}`} style={{ height: `${val}%` }} />
|
| 425 |
+
))}
|
| 426 |
+
</div>
|
| 427 |
+
|
| 428 |
+
{/* Footer */}
|
| 429 |
+
<footer className="p-4 bg-[#0a0a0a] border border-white/5 text-[9px] uppercase tracking-tighter opacity-60 grid grid-cols-2 gap-4">
|
| 430 |
+
<div className="flex gap-2"><Binary size={12} /> DATA_PAGING: STOCHASTIC_GRADIENT_ENABLED</div>
|
| 431 |
+
<div className="text-right">SYNAPTIC_WEIGHTS_NORMALIZED // HEBBIAN_COEFFICIENT: {(plasticity / 100).toFixed(2)}</div>
|
| 432 |
+
</footer>
|
| 433 |
+
</div>
|
| 434 |
+
</div>
|
| 435 |
+
);
|
| 436 |
+
};
|
| 437 |
+
|
| 438 |
+
export default App;
|
src/components/GridCell.jsx
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
const GridCell = React.memo(({ active, mismatch, weight, variant, onClick }) => {
|
| 4 |
+
if (variant === 'signal') {
|
| 5 |
+
return (
|
| 6 |
+
<button
|
| 7 |
+
onClick={onClick}
|
| 8 |
+
aria-label={active ? 'Active neuron' : 'Inactive neuron'}
|
| 9 |
+
className={`w-full h-full transition-all duration-75 border ${
|
| 10 |
+
active
|
| 11 |
+
? 'bg-[#00ff9f] border-white shadow-[0_0_15px_#00ff9f] scale-95'
|
| 12 |
+
: 'bg-transparent border-[#00ff9f]/5 hover:border-[#00ff9f]/40 hover:bg-[#00ff9f]/5'
|
| 13 |
+
}`}
|
| 14 |
+
/>
|
| 15 |
+
);
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
// Memory variant
|
| 19 |
+
const style = weight > 0.5
|
| 20 |
+
? { boxShadow: `0 0 ${weight * 20}px rgba(255,255,255,0.4)`, borderColor: `rgba(255,255,255,${weight})` }
|
| 21 |
+
: { borderColor: `rgba(255,255,255,${weight})` };
|
| 22 |
+
|
| 23 |
+
return (
|
| 24 |
+
<div
|
| 25 |
+
aria-label={active ? 'Remembered neuron' : 'Empty synapse'}
|
| 26 |
+
style={style}
|
| 27 |
+
className={`w-full h-full transition-all duration-300 border ${
|
| 28 |
+
active
|
| 29 |
+
? 'bg-[#ff0055] shadow-[0_0_15px_#ff0055]'
|
| 30 |
+
: 'bg-transparent border-[#ff0055]/5'
|
| 31 |
+
} ${mismatch ? 'opacity-50 border-dotted scale-90' : ''}`}
|
| 32 |
+
/>
|
| 33 |
+
);
|
| 34 |
+
});
|
| 35 |
+
|
| 36 |
+
GridCell.displayName = 'GridCell';
|
| 37 |
+
|
| 38 |
+
export default GridCell;
|
src/index.css
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
src/main.jsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import ReactDOM from 'react-dom/client';
|
| 3 |
+
import App from './App';
|
| 4 |
+
import './index.css';
|
| 5 |
+
|
| 6 |
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
| 7 |
+
<React.StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</React.StrictMode>
|
| 10 |
+
);
|
tailwind.config.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
export default {
|
| 3 |
+
content: ['./index.html', './src/**/*.{js,jsx}'],
|
| 4 |
+
theme: {
|
| 5 |
+
extend: {},
|
| 6 |
+
},
|
| 7 |
+
plugins: [],
|
| 8 |
+
};
|
vite.config.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite';
|
| 2 |
+
import react from '@vitejs/plugin-react';
|
| 3 |
+
|
| 4 |
+
export default defineConfig({
|
| 5 |
+
plugins: [react()],
|
| 6 |
+
server: {
|
| 7 |
+
host: '0.0.0.0',
|
| 8 |
+
port: 7860,
|
| 9 |
+
},
|
| 10 |
+
});
|