# זבוב משחק מלך הפלאפל סימולציה של מוח זבוב הפירות, בנויה מהקונקטום האמיתי של **FlyWire**, שמפעילה דוכן פלאפל. 4,798 נוירונים אמיתיים ו-116,960 סינפסות אמיתיות רצים כרשת leaky integrate-and-fire בדפדפן, ומזיזים את היד בין המגשים. הלקוחות הם חברי כנסת מהקואליציה. הם מאבדים סבלנות. **האתר החי:** https://eli-harshefer.github.io/fly-brain-falafel/ --- ## להריץ מקומית ```bash npm install npm run dev ``` `npm run build` בונה ל-`dist/`, `npm run preview` מגיש את הבנייה על פורט 4173. כל הנתונים כבר בריפו תחת `public/data/`, אז אין צורך להריץ את החילוץ מחדש. כדי לבנות אותם מאפס: `pip install -r requirements.txt` ואז `npm run extract` (מוריד כ-60MB מה-bucket הציבורי של FlyWire), ו-`npm run verify` לבדיקת רגרסיה. --- ## מה באמת עושה פה המוח המשחק מפריד במפורש בין מה שהקונקטום מחשב לבין מה שלוגיקת המשחק מחליטה, וה-UI מסמן את זה: תג ירוק "קונקטום אמיתי" מול תג אפור "לוגיקת משחק". **נוירונלי — כל אלה עוברים דרך המעגל האמיתי:** | מסלול | סינפסות | מה הוא עושה | |---|---|---| | LC10a → AOTU019 | 2,845 | מזהה את המגש ומדווח שהוא **במרכז** | | LC10a → AOTU025 | 2,772 | מזהה שהמגש **בפריפריה** | | AOTU019 → DNa02 | 333 (GABA, נגדי) | מעכב סיבוב כשמכוונים נכון | | AOTU025 → DNa02 | 321 (ACH, אותו צד) | מניע סיבוב כשהמטרה בצד | | EPG → Δ7 → PFL3 | 2,369 / 2,192 | טבעת הכיוון והשוואה מול המטרה | | PFL3 → DNa02 | 474 (נגדי) | הטיית היגוי — פעיל, אך לא נחוץ (ראו למטה) | | LPLC2 → DNp01 | 822 | לומינג → Giant Fiber → סטירה לזבוב | **לא נוירונלי, ומוצהר ככזה:** קריאת שלט ההזמנה ובחירת המגש הבא. אין מעגל מתועד בזבוב שקורא הזמנות. מרגע שנבחרה מטרה, כל השאר — לכוון, להחליט מתי לתפוס, ולסטור לזבובים — נעשה על ידי החיווט. **מה עוד רץ אבל לא נחוץ:** הקומפלקס המרכזי. הוא פעיל, יורה, ונראה בראסטר ובתלת-ממד — אבל נמדד שאינו תורם למשימה הזו. פירוט בתגלית 3. ### שלוש תגליות מהנתונים **1. הפרדה נקייה שנפלה מהמעגל עצמו.** סריקת הכיול (`tools/calibrate.mjs`) הראתה ש-AOTU019 יורה הכי חזק (140–157Hz) בדיוק כשהמטרה **במרכז** ונדעך בפריפריה, בעוד AOTU025 עושה בדיוק ההפך. מכיוון ש-AOTU019 מעכב את DNa02 ו-AOTU025 מעורר אותו, זהו זוג push-pull אמיתי: מטרה בצד מניעה סיבוב, מטרה במרכז עוצרת אותו. ל-DNa02 יש אזור מת טבעי של ±20°, שהוא מה שמאפשר ליד להתייצב במקום לרדוף סביב המגש. **2. חוק דייל תיקן שגיאה בנתונים.** התחזית לנוירוטרנסמיטר ברמת הקשת מתייגת את `LPLC2→DNp01` כגלוטמט, כלומר **מעכב** — מה שהיה גורם ללומינג להשתיק את ה-Giant Fiber ולבטל לגמרי את רפלקס הבריחה. אכיפת חוק דייל (נוירוטרנסמיטר אחד לנוירון, לפי רוב משוקלל על כל הפלט שלו) מחזירה את התשובה הנכונה: LPLC2 כולינרגי ומעורר. `tools/verify_circuit.py` נועל את זה עם הערה. **3. הקומפלקס המרכזי לא נחוץ למשימה הזו — וזו תוצאה, לא באג.** התכנון המקורי הניח שהקומפלקס המרכזי (EPG → Δ7 → PFL3) יחזיק את המטרה וינווט אליה. `tools/cxsweep.mjs` מדד את התרומה שלו מקצה לקצה, וגילה שאין כזו: | עוצמת PFL3 | תוך 15° | שגיאה | |---|---|---| | 0.00 (כבוי) | 14/14 | 3.0° | | 0.50 | 14/14 | 2.9° | | 1.00 | 10/14 | 39.6° | | 1.50 | 3/14 | 91.7° | מעגל המרדף פותר את המשימה לבדו, והטיית PFL3 חזקה רק מתחרה בו. לכן הקומפלקס המרכזי רץ ב-0.5 — פעיל ונראה בראסטר ובתלת-ממד, אבל לא מנווט. בדרך לשם תוקן גם באג אמיתי: `setGoal` הזין את PFL3_L ו-PFL3_R **באותה פאזה**, מה שייצר פלט סימטרי לחלוטין וכלום היגוי. שתי אוכלוסיות PFL3 דוגמות את המטרה בהיסטים הפוכים, וזה מה שהופך הפרש כיוון-מול-מטרה לפקודת פנייה. ניסיתי גם את החלופה הנקייה יותר — להזין ל-LC10a את **כל** התחנות ולתת לקומפלקס המרכזי לבחור. זה נכשל: AOTU019 מסמן "אובייקט במרכז", לא "האובייקט הנכון במרכז", אז הזבוב תפס במגש הראשון שהגיע אליו (1/14). לגזור את שער התפיסה מ-PFL3 היה מעגלי — קריאה חזרה של הקלט שאני עצמי הזרקתי. לכן המטרה נשארת קלט מוצהר ולא-נוירונלי. ### למה זה בכלל עובד — ומה זה אומר על מוח הזבוב **שום דבר כאן לא לומד.** המשקלים הם ספירות הסינפסות מהקונקטום, קבועות. הזבוב לא התאמן על מלך הפלאפל ולא השתפר תוך כדי משחק. זו בדיוק הנקודה המעניינת: הוא כבר היה מחווט לעשות משהו בעל אותו מבנה, ואנחנו רק כיוונו אותו לדוכן פלאפל. האבולוציה כאן היא אבולוציה ביולוגית, לא אימון. שלושת המעגלים שמפעילים את המשחק התפתחו לשלוש משימות אחרות לגמרי: | מעגל | למה הוא התפתח | מה הוא עושה כאן | |---|---|---| | `LC10a → AOTU → DNa02` | **חיזור.** LC10a הוא גלאי אובייקט קטן ונע — זה המעגל שמאפשר לזכר לנעול מבט על נקבה ולרדוף אחריה בלי להרפות. | נועל על מגש ומסובב את הזבוב אליו. מבחינת המוח, מגש חומוס הוא עוד מטרה קטנה שזזה בשדה הראייה. | | `LPLC2 → DNp01` | **בריחה.** LPLC2 מזהה התרחבות זוויתית מהירה — צל שמתקרב, ציפור, כף יד. ה-Giant Fiber מקפיץ את הזבוב לאוויר תוך מילישניות. | המעגל שהתפתח כדי *לברוח* ממכת זבובים הוא זה שסוטר לזבוב שהתיישב על האוכל. | | `EPG → Δ7 → PFL3` | **ניווט.** תאי EPG מחזיקים את כיוון הראש כמו מחוג מצפן שמסתובב עם הזבוב, ומאפשרים לשמור על קו ישר או לחזור למקור מזון. | רץ ונראה — אבל נמדד שאינו נחוץ למשימה הזו (תגלית 3). | זה גם מה שהופך את הדיסוציאציה הכפולה למשמעותית: כשמשתיקים את DNa02 הזבוב מאבד את פקודת הפנייה לגוף ולא מצליח לכוון, וכשמשתיקים את AOTU019 הוא מכוון מצוין אבל לא יודע מתי הוא *על* המטרה — בדיוק כמו שהיה קורה לזבוב אמיתי באמצע מרדף. ### איך עובד המודל עצמו כל נוירון הוא **דלי מתח דולף**, וזה כל המודל: 1. **נצבר** — מטען שמגיע מנוירונים אחרים מעלה את המתח. 2. **דולף** — המתח חוזר למנוחה (−52mV) בקבוע זמן של 20ms. 3. **יורה** — חצה את הסף (−45mV)? ספייק. 4. **מתאפס** — חוזר למנוחה, וחירש 2.2ms. הספייק מגיע ליעדיו 1.8ms אחר כך לאורך הסינפסות האמיתיות, במשקל של **מספר הסינפסות × סימן המוליך העצבי**. אין כאן למידה בשום שלב. בעמוד יש אוסילוסקופ חי של נוירון בודד ב-0.2ms רזולוציה — אפשר לראות את המתח נצבר, חוצה את הסף, יורה ומתאפס, וגם נדחף מתחת למנוחה כשעיכוב פעיל עליו. לוחצים על נוירון בתצוגה התלת-ממדית כדי למדוד אותו. ### מה הזבוב חושב שקורה מבחוץ זה נראה כמו זבוב שמכין מנה. מבפנים אין דוכן, אין הזמנה, אין לקוח ואין עבודה. יש **כתם כהה קטן איפשהו בשדה הראייה, ודחף להסתובב עד שהוא באמצע**. ואז יש כתם אחר. הפאנל בעמוד מראה את אותו רגע בשתי עמודות — מה שאנחנו רואים מול מה שהוא חווה — וכל שורה מסומנת לפי המקור שלה. ההבחנה שהוא קיים בשבילה: - **ההליכה למשהו היא אינסטינקט אמיתי.** זבוב שרואה משהו קטן שזז ננעל עליו ומסתובב אחריו, וזה בדיוק המעגל שרץ פה. מסומן "אינסטינקט". - **המעבר מרכיב לרכיב הוא לא הוא.** אין שום דבר בזבוב שרוצה סלט עכשיו. את ההחלטה הזו לוגיקת המשחק מקבלת בשבילו. מסומן "לא הוא". - **הסטירה היא רפלקס בריחה.** מסומן "רפלקס". הזבוב אף פעם לא מגלה שיש לו עבודה. מבחינתו זו שרשרת אינסופית של "יש משהו — ללכת אליו — הגעתי", בלי מושג שזה מצטרף למנה ושמנות מצטרפות למשמרת. ### האם הוא משתפר? לא. זו השאלה הכי נפוצה, והתשובה הכנה היא לא. הזבוב הוא בדיוק אותו זבוב בשנייה הראשונה ובדקה השביעית — החיווט לא משתנה, ואין בסימולציה הזו שום למידה. מה שכן קורה זה שני דברים אמיתיים, ושניהם מוצגים בעמוד: **צעד אחר צעד.** כל מנה היא רשימה קצרה: פיתה, ואז כל מה שההזמנה מבקשת, ואז להגיש. הזבוב עושה צעד, בודק מה עוד חסר, ועושה את הבא. **אין לו תוכנית ארוכה מזה** — אין תכנון, אין זיכרון של מנות קודמות. **הקשת של הסשן.** הגרף מראה עמודה לכל מנה שהוגשה, בגובה של כמה זמן היא לקחה, מול קו קצב ההזמנות שעולה. העמודות נשארות בערך באותו גובה לאורך כל המשחק. מה שעולה זה הקו האדום. **בסוף התור פשוט רץ מהר יותר מזבוב שעובד בדיוק כמו קודם.** `tools/survival.mjs` מודד את זה: הוא מחזיק כ-450 שניות ואז נשבר. ### נקודת המבט של הזבוב עין של זבוב היא לא מצלמה. יש בה כ-750 **אומטידיות**, כל אחת עדשה נפרדת בכיוון משלה, במרווח של כ-**5°** — בערך פי 100 גס יותר מהראייה שלנו. מצד שני היא הרבה יותר מהירה: זבובים מבחינים בהבהוב עד בערך **200Hz** ואנחנו מוותרים סביב 60. הפאנל "מה הזבוב באמת רואה" מצייר את הדוכן כפי שהסריג הזה דוגם אותו — משושה אחד לכל אומטידיה, בהיטל אמיתי של גיאומטריית המשחק. **וזה מסביר את ההתנהגות:** ברזולוציה של 5° אין שום פרט לעבוד איתו, מגש הוא כמה משושים כהים. הדבר היחיד ששווה לחשב הוא *איפה* הכתם, וזה בדיוק מה ש-LC10a מדווח. הזבוב לא "מזהה חומוס" — הוא נועל על כתם קטן בשדה הראייה ומסתובב עד שהוא במרכז. ### מה הזבוב רואה הפאנל "מה הזבוב רואה" הוא לא איור. העקומה היא אוכלוסיית LC10a האמיתית, מצוירת לפי המיקום הרטינוטופי של כל תא: תאי עין שמאל באזימוט שלילי, עין ימין בחיובי, והאקסצנטריות נגזרת מיחס הדחיפה של התא ל-AOTU019 מול AOTU025. כשהזבוב מסתובב הגבשושית זזה, כי תאים אחרים יורים. **הגבשושית הזו היא התשובה של המוח לשאלה "איפה המטרה"**, והאיזון שמאל/ימין שנגזר ממנה הוא התשובה ל"לאן לפנות". ### שלוש דרכים לראות את אותו דבר הממשק מראה את אותה פעילות בשלוש רמות, כי כל אחת עונה על שאלה אחרת: - **המוח התלת-ממדי** — *איפה*. כל נוירון במיקום ה-FlyWire האמיתי שלו, מהבהב כשהוא יורה. הקשתות עצמן מאירות לפי הפעילות של הנוירון שממנו הן יוצאות, כך שרואים את האות נוסע לאורך החיבורים ולא רק קצוות מהבהבים. אפשר להצביע על כל נוירון ולקבל את סוג התא, הצד, המוליך העצבי, הקצב הנוכחי ומספר הקשרים הנכנסים והיוצאים — לחיצה נועצת אותו. 104 סוגי תאים שונים ניתנים לאיתור כך. - **הראסטר והעקבות** — *מתי*. ספייקים לפי קבוצה, וקצבי הנוירונים היורדים לאורך זמן. - **תרשים המסלול** — *למה*. המעגל עצמו: עובי הקו הוא מספר הסינפסות האמיתי, הצבע הוא סימן הסינפסה, והפולסים נעים לפי קצב הירי. כאן רואים במו העיניים ש-AOTU019 מעכב את DNa02 הנגדי בזמן ש-AOTU025 מעורר את זה שבאותו צד. כל המספרים בתרשים נקראים מהמעגל המחולץ בזמן הטעינה — אין שם ערכים קשיחים. ### נגעים — ההוכחה שזה לא תפאורה `node tools/closedloop.mjs`, 14 ניסויים לכל תנאי, בעמדות התחנות האמיתיות: | תנאי | תוך 15° | שגיאה | מה זה אומר | |---|---|---|---| | **קונקטום שלם** | **14/14** | **3.0°** | — | | חיווט מעורבב | 0/14 | 84.8° | החיווט הספציפי הוא הכל | | DNa02 מושתק | 2/14 | 62.4° | תופס, לא מסוגל לכוון | | AOTU019 מושתק | 0/14 | 2.2° | מכוון מצוין, לא תופס לעולם | | קומפלקס מרכזי מושתק | ללא שינוי | — | לא נחוץ למשימה הזו | שתי השורות האמצעיות הן דיסוציאציה כפולה: השתקת DNa02 שוברת את ההיגוי אבל לא את התפיסה, והשתקת AOTU019 שוברת את התפיסה אבל לא את ההיגוי. השורה האחרונה היא תוצאה שלילית שנשארת מסומנת ככזו — בקרה שתמיד מאשרת חשיבות איננה בקרה. אפשר להפעיל את כולן חי מלוח "ניסויים". ### שרידות — משחקים שלמים עד הסוף `node tools/survival.mjs`, ממוצע 3 משחקים לכל תנאי: | תנאי | שרד | ניקוד | מנות | |---|---|---|---| | **קונקטום שלם** | **443 שנ׳** | 5,162 | 167 | | חיווט מעורבב | 33 שנ׳ | 12 | 0 | | DNa02 מושתק | 33 שנ׳ | 13 | 0 | | LPLC2 מושתק | 286 שנ׳ | 2,946 | 102 | הזבוב השלם שורד כ-7.5 דקות ואז נשבר מול קצב ההזמנות העולה — כלומר מצב ההפסד אכן ניתן להגעה, ולא קישוט. השתקת LPLC2 היא אפקט מדורג ולא בינארי: הזבוב עדיין משחק, אבל מאבד 35% מזמן השרידות כי מגשים מתקלקלים. --- ## הרצה ```bash npm install python tools/extract_circuit.py # פעם אחת, מוריד ~60MB מ-FlyWire npm run dev ``` `extract_circuit.py` משתמש רק בספריית התקן של פייתון (3.9+). הוא מוריד את ה-snapshot הציבורי של FlyWire v783 — בלי התחברות — ל-`data/raw/`, ומייצר `public/data/` (circuit.bin ~1MB, cloud.bin ~1.8MB). ### כלים ```bash npm test # 40 בדיקות יחידה: מנוע ה-LIF, קידוד/פענוח, לוגיקת המשחק npm run verify # בודק שספירות הסינפסות תואמות ל-FlyWire v783 npm run bench # ביצועי מנוע ה-LIF npm run calibrate # סריקת אזימוט; מדפיס קבועים ל-decode.js npm run closedloop # בנצ'מרק רכישת מטרה: שלם מול מעורבב מול נגעים npm run cxsweep # מודד את תרומת הקומפלקס המרכזי לפי עוצמת PFL3 npm run survival # משחקים שלמים עד הפסד: שלם מול נגעים ``` אם משנים את החילוץ או את `SYN_GAIN`, מריצים `npm run calibrate` ומעתיקים את הקבועים שהוא מדפיס ל-`src/decode.js`. **אל תנחשו את הסימנים** — החיווט חוצה את קו האמצע בכמה מקומות (גם PFL3 וגם AOTU019 מקרינים לצד הנגדי אל DNa02). --- ## מבנה ``` tools/extract_circuit.py חילוץ המעגל מ-FlyWire → בינארי דחוס tools/verify_circuit.py בדיקת רגרסיה על ספירות הסינפסות tools/calibrate.mjs סריקת אזימוט → קבועי הפענוח tools/closedloop.mjs בנצ'מרק + בקרות נגעים tools/cxsweep.mjs מדידת תרומת הקומפלקס המרכזי tools/survival.mjs משחקים שלמים עד הפסד tools/symmetry.mjs בדיקת סימטריה שמאל/ימין של המעגל tools/test.mjs בדיקות יחידה src/lif.js מנוע LIF (משותף לדפדפן ול-Node) src/encode.js מצב משחק → קצבי Poisson ל-LC10a / LPLC2 / EPG src/decode.js קצבי DN → היגוי, תפיסה, סטירה src/controller.js הלולאה הסגורה src/brain3d.js three.js: ענן 139K + המעגל + bloom src/game/ מצב המשחק, ציור, אמנות, רוסטר חברי הכנסת src/ui/ ראסטר, עקבות, תרשים המסלול, בקרות נגעים ``` ### הערות מימוש **המנוע רץ ב-thread הראשי, לא ב-Web Worker.** הוא נמדד ב-~2.1ms למסגרת מתוך תקציב של 16.67ms (`npm run bench`, ~7.7× מזמן אמת), והתצוגה התלת-ממדית צריכה את מאגר הספייקים בכל מסגרת — העברתו דרך `postMessage` 60 פעמים בשנייה הייתה עולה יותר מהסימולציה עצמה. משחק שלם עם כל הרינדור נמדד ב-~4ms למסגרת. **ההתפשטות מונעת-אירועים והאינטגרטור עובר על קבוצה פעילה.** רק נוירונים שיורים דוחפים מטען, ורק נוירונים שעושים משהו מתעדכנים. זה מה שהפך 0.78× מזמן אמת ל-~7.7×. **ניפוי שגיאות בדפדפן.** `window.__fly` חושף את `brain`, `controller`, `game`, `view` ו-`tick(dtMs)`. אפשר להריץ `for (let i=0;i<600;i++) __fly.tick(16.7)` בקונסול כדי לקדם 10 שניות של משחק בלי להמתין — שימושי גם כשהלשונית מוסתרת ו- `requestAnimationFrame` מושהה. **`dt` הוא 0.2ms ולא 0.1ms**, ו-`SYN_GAIN` אחיד של 0.11 מכייל את הרשת. משקלי קונקטום גולמיים מכניסים אותה לרוויה — LPLC2 לבדו מספק 822 סינפסות × 0.275mV = 226mV ל-Giant Fiber. ה-gain הוא מספר אחד שמוכפל בכל המשקולות, כך שכל היחסים שהקונקטום קובע נשמרים במדויק. --- ## קרדיטים ורישוי **נתוני קונקטום:** [FlyWire](https://codex.flywire.ai) FAFB v783, רישיון **CC BY-NC 4.0** (שימוש לא-מסחרי, חובת ייחוס). Dorkenwald et al. 2024, *Nature*; Schlegel et al. 2024, *Nature*. **פרמטרי LIF:** Shiu et al. 2024, *Nature* — מנוחה −52mV, סף −45mV, τ_mem 20ms, τ_syn 5ms, refractory 2.2ms, עיכוב 1.8ms, 0.275mV לסינפסה. **המשחק:** מחווה ל**מלך הפלאפל** (2002) מאת דובי גרמייז, נדב גל ורפי ברקוביץ׳. כל הקוד והאיורים כאן מקוריים; לא נעשה שימוש בנכסים מהמשחק המקורי. זהו פרויקט לא-מסחרי, ללא קשר ליוצרי המשחק. **הלקוחות:** קריקטורות של חברי כנסת מהקואליציה של הממשלה ה-37, בשפה הרגילה של קריקטורה פוליטית — תסרוקת, משקפיים, כיסוי ראש, חליפה. המשפטים שהם אומרים מומצאים ועוסקים בפלאפל; אין כאן ציטוטים אמיתיים. הרוסטר נמצא ב-`src/game/mks.js` וניתן לעריכה חופשית.