назад в блог

Эмоциональный мозг для LLM: провал

TL;DR: я реализовал систему, в которой спайкинговая нейронная сеть на Brian2 симулировала пять базовых эмоций по Экману, а результаты этой симуляции управляли temperature и top_p при вызовах LLM через LangChain. Это было увлекательно, красиво и совершенно бесполезно для продакшна.

Это продолжение статьи про настроение у языковой модели. Тогда идея выглядела как рабочий инженерный путь: взять эмоциональный контекст, прогнать через нейронную динамику и получить параметры генерации. Здесь — честный разбор того, почему эта архитектура не переживает встречу с реальностью.


1. Откуда взялась идея

Вопрос звучит невинно: а что если LLM будет «чувствовать» контекст? Не метафорически, а буквально — через биологически правдоподобную модель нейронной активности, которая в реальном времени регулирует параметры генерации.

Отправная точка — теория базовых эмоций Пола Экмана[4]: пять универсальных состояний — joy, sadness, fear, anger, disgust — которые можно закодировать в числа. Параллельно — модель Рассела[5] с осями valence/arousal, позволяющая перевести аффективное состояние в вектор.

Техническая реализация легла на Brian2[1] — Python-фреймворк для симуляции биологически реалистичных нейронных сетей. Управление LLM — через LangChain[3].


2. Архитектура системы

2.1 Нейронная сеть — Leaky Integrate-and-Fire

Каждая эмоция представлена пулом из 100 LIF-нейронов — Leaky Integrate-and-Fire. Мембранный потенциал описывается уравнением:

τ · dv/dt = (V_rest − v) + I_ext + ξ · σ · √τ

Когда потенциал превышает порог V_thresh, нейрон генерирует спайк и сбрасывается в V_reset. Частота разрядов пула — прямая мера «интенсивности» эмоции.

# Уравнение LIF-нейрона в Brian2
eqs = """
dv/dt = ((v_rest - v) + I_ext + xi * sigma_n * sqrt(tau)) / tau : volt (unless refractory)
I_ext : volt (shared)
sigma_n : volt (shared)
"""

pool = NeuronGroup(
    100,
    eqs,
    threshold="v > v_thresh",
    reset="v = v_reset",
    refractory=2*ms,
    method="euler",
)

2.2 Латеральное торможение — конкуренция между эмоциями

Пять пулов соединены тормозными синапсами: когда пул A разряжается, он снижает I_ext пула B. Это создаёт winner-take-most динамику — аналог работы базальных ганглиев.

# Двунаправленное латеральное торможение
inhib_joy_to_fear = Synapses(pool_joy, pool_fear, on_pre="I_ext_post -= 1.5*mV")
inhib_fear_to_joy = Synapses(pool_fear, pool_joy, on_pre="I_ext_post -= 1.5*mV")

inhib_joy_to_fear.connect(p=0.25)
inhib_fear_to_joy.connect(p=0.25)

2.3 Декодирование — маппинг спайков в параметры LLM

Доминирующая эмоция определяет диапазон temperature, top_p и системный промпт:

Эмоция temperature top_p Системный промпт
Joy1.1–1.30.95enthusiastic, warm
Sadness0.5–0.70.80thoughtful, gentle
Fear0.3–0.50.70cautious, precise
Anger0.9–1.10.85direct, assertive
Disgust0.6–0.80.75critical, detached

2.4 Полный pipeline

# Полный pipeline: стимул -> SNN -> параметры LLM
stimulus = Stimulus(joy=16.0, sadness=5.0, fear=8.0, anger=4.0, disgust=3.0)

# 1. Запускаем SNN: 300 мс симуляции
cfg = NetworkConfig(n_neurons=100, duration_ms=300, noise_sigma_mV=1.5)
net = EmotionalNetwork(cfg)
result = net.run(stimulus)

# 2. Декодируем в параметры
modulator = LLMModulator()
state = modulator.modulate(result)

# 3. Вызываем LLM
llm = ChatOpenAI(
    temperature=state.temperature,
    model_kwargs={"top_p": state.top_p},
)
chain = LLMChain(llm=llm, prompt=state.system_prompt)
response = chain.run(user_input)

3. Почему это не работает в продакшне

3.1 Проблема латентности

Одна итерация net.run(stimulus) — это полноценная нейронная симуляция: 300 мс биологического времени с численным интегрированием дифференциальных уравнений методом Эйлера по 500 нейронам. На CPU это занимает 150–400 мс реального времени при каждом запросе.

Для сравнения: inference на Claude Haiku занимает 200–600 мс на первый токен. Мы добавляем сопоставимый overhead ради вычисления двух float-чисел.

3.2 Архитектурная несостоятельность маппинга

Фундаментальная проблема: temperature и top_p — это параметры распределения следующего токена[10]. Они не «эмоции модели». Никакой биологической или вычислительной связи между частотой разрядов LIF-нейронов и семантикой генерируемого текста не существует.

Тот же результат — joy -> high temperature — достигается одной строкой:

temperature = emotion_scores["joy"] / 15.0

Brian2 здесь — вычислительно дорогой random number generator, замаскированный под нейробиологию.

3.3 Детерминизм и воспроизводимость

Brian2 использует стохастические уравнения: xi * sigma * sqrt(tau), ланжевеновский шум. Одинаковый стимул даёт разные firing rates, разные параметры и разные ответы LLM. Это не «биологическая реалистичность», а недетерминированный баг, который невозможно воспроизвести при отладке.

3.4 Stateful-симуляция в stateless-API

LLM API stateless. Каждый запрос независим. Но SNN имеет внутреннее состояние: мембранные потенциалы, рефрактерные периоды, синаптическая динамика. Если сохранять состояние между запросами — получаем проблему concurrency, потому что один объект Network Brian2 не thread-safe. Если не сохранять — теряется весь смысл «эмоциональной памяти».

3.5 Синаптическое торможение ломает production-логику

В эксперименте синапс on_pre="I_ext_post -= 1.5*mV" работает корректно при 300 мс симуляции. Но при последовательных вызовах накапливается синаптическая депрессия: через N запросов I_ext всех пулов уходит в отрицательную зону, все нейроны замолкают, temperature = 0. LLM начинает генерировать один и тот же детерминированный токен.

3.6 Зависимости и деплой

Brian2 требует C++ компилятора для JIT-компиляции нейронных уравнений, sympy, cython и специфичной версии numpy. В продакшне это означает:


4. Что реально работает

4.1 Прямой маппинг без симуляции

Если нужна эмоциональная модуляция параметров — сделайте простой маппинг. Он детерминирован, быстр и даёт идентичный результат:

def emotion_to_params(emotion_scores):
    dominant = max(emotion_scores, key=emotion_scores.get)
    intensity = emotion_scores[dominant] / sum(emotion_scores.values())

    base_params = {
        "joy": {"temperature": 1.2, "top_p": 0.95},
        "sadness": {"temperature": 0.6, "top_p": 0.80},
        "fear": {"temperature": 0.4, "top_p": 0.70},
        "anger": {"temperature": 1.0, "top_p": 0.85},
        "disgust": {"temperature": 0.7, "top_p": 0.75},
    }

    params = base_params[dominant]
    return {k: v * intensity for k, v in params.items()}

4.2 Системный промпт — реальный инструмент

Значимо влияет не temperature, а системный промпт. «Отвечай как человек в состоянии тревоги: кратко, осторожно, без лишних деталей» — это реально меняет тон. Маппинг emotion → system prompt через LangChain PromptTemplate работает хорошо и без Brian2.

# Рабочий подход: эмоция -> системный промпт
EMOTION_PROMPTS = {
    "joy": "Be enthusiastic, warm, and encouraging.",
    "fear": "Be cautious, precise, and avoid overconfidence.",
    "sadness": "Be gentle, thoughtful, and empathetic.",
    "anger": "Be direct, concise, and solution-focused.",
    "disgust": "Be critical, analytical, and objective.",
}

template = ChatPromptTemplate.from_messages([
    ("system", EMOTION_PROMPTS[dominant_emotion]),
    ("human", "{input}"),
])

5. Почему эксперимент всё равно worth it

Исследовательская ценность полностью оправдывает затраты. Вот что реально дал этот эксперимент:

В neuromorphic AI SNN действительно имеют смысл: Loihi 2[7], SpiNNaker[8], BrainScaleS[9] — там речь идёт о hardware-accelerated inference. Но переносить ту же метафору в обычный LangChain-пайплайн ради двух параметров генерации — инженерно невыгодно.


6. Полный исполняемый код

Ниже — полный код эксперимента, который можно запустить через jupyter notebook emotional_brain.ipynb. Потребуются brian2, numpy, matplotlib и ipywidgets.

Секция 1: настройка окружения

# Install: first run only
# !pip install brian2 numpy matplotlib ipywidgets

import numpy as np
import matplotlib.pyplot as plt
from brian2 import (
    NeuronGroup,
    Synapses,
    SpikeMonitor,
    StateMonitor,
    Network,
    start_scope,
    ms,
    mV,
    second,
)

from src.encoder import StimulusEncoder, Stimulus, EMOTIONS, CIRCUMPLEX
from src.network import EmotionalNetwork, NetworkConfig
from src.decoder import LLMModulator
from src.visualizer import (
    Oscilloscope,
    RasterPlot,
    ISIHistogram,
    EmotionBarChart,
    EmotionDashboard,
    COLORS,
)

Секция 2: LIF-нейрон — осциллоскоп

start_scope()

TAU = 10 * ms
V_REST = -70 * mV
V_THRESH = -55 * mV
V_RESET = -75 * mV

eqs = """
dv/dt = (v_rest - v + I_ext) / tau : volt (unless refractory)
I_ext : volt
"""

neuron = NeuronGroup(
    1,
    eqs,
    threshold="v > v_thresh",
    reset="v = v_reset",
    refractory=2*ms,
    method="euler",
    namespace={
        "tau": TAU,
        "v_rest": V_REST,
        "v_thresh": V_THRESH,
        "v_reset": V_RESET,
    },
)

neuron.v = V_REST
neuron.I_ext = 16 * mV

state_mon = StateMonitor(neuron, "v", record=True)
spike_mon = SpikeMonitor(neuron)
run(200 * ms)

Секция 3: пул 100 нейронов

start_scope()

N = 100
DURATION = 300 * ms

eqs_pool = """
dv/dt = ((v_rest - v) + I_ext + xi * sigma_n * sqrt(tau)) / tau : volt (unless refractory)
I_ext : volt (shared)
sigma_n : volt (shared)
"""

pool = NeuronGroup(
    N,
    eqs_pool,
    threshold="v > v_thresh",
    reset="v = v_reset",
    refractory=2*ms,
    method="euler",
    namespace={
        "tau": TAU,
        "v_rest": V_REST,
        "v_thresh": V_THRESH,
        "v_reset": V_RESET,
    },
)

pool.v = "v_rest + rand() * (v_thresh - v_rest) * 0.3"
pool.I_ext = 14 * mV
pool.sigma_n = 1.5 * mV

spk_mon = SpikeMonitor(pool)
run(DURATION)

rate_hz = spk_mon.num_spikes / (N * (DURATION / second))
print(f"Pool firing rate: {rate_hz:.1f} Hz")

Секция 4: латеральное торможение

start_scope()

# joy получает более сильный стимул и должна доминировать
pool_joy = NeuronGroup(100, eqs_pool, ...)
pool_fear = NeuronGroup(100, eqs_pool, ...)

pool_joy.I_ext = 16 * mV
pool_fear.I_ext = 12 * mV

inhib_joy_to_fear = Synapses(pool_joy, pool_fear, on_pre="I_ext_post -= 1.5*mV")
inhib_fear_to_joy = Synapses(pool_fear, pool_joy, on_pre="I_ext_post -= 1.5*mV")

inhib_joy_to_fear.connect(p=0.25)
inhib_fear_to_joy.connect(p=0.25)

run(300 * ms)

Секция 5: полная система + LLM-модуляция

stimulus = Stimulus(
    joy=16.0,
    sadness=5.0,
    fear=8.0,
    anger=4.0,
    disgust=3.0,
)

cfg = NetworkConfig(n_neurons=100, duration_ms=300, noise_sigma_mV=1.5)
net = EmotionalNetwork(cfg)
result = net.run(stimulus)

modulator = LLMModulator()
state = modulator.modulate(result)

print(state.summary())

import json
print(json.dumps(state.llm_params(), indent=2))

# Вывод:
# {
#   "temperature": 1.15,
#   "top_p": 0.94,
#   "system_prompt": "Be enthusiastic and warm...",
#   "dominant_emotion": "joy"
# }

Секция 6: сравнение сценариев

scenarios = [
    ("Pure Joy", Stimulus(joy=18, sadness=1, fear=1, anger=1, disgust=1)),
    ("Pure Fear", Stimulus(joy=1, sadness=1, fear=18, anger=1, disgust=1)),
    ("Pure Anger", Stimulus(joy=1, sadness=1, fear=1, anger=18, disgust=1)),
    ("Pure Sadness", Stimulus(joy=1, sadness=18, fear=1, anger=1, disgust=1)),
    ("Mixed", Stimulus(joy=10, sadness=8, fear=12, anger=6, disgust=4)),
    ("Neutral", Stimulus(joy=5, sadness=5, fear=5, anger=5, disgust=5)),
]

for name, stim in scenarios:
    r = net.run(stim)
    s = modulator.modulate(r)
    print(f"{name:15s} | temp={s.temperature:.2f} | top_p={s.top_p:.2f} | dominant={s.dominant}")

7. Выводы

Что получили: красивую демонстрацию биологически правдоподобной нейронной конкуренции, хорошо работающую как учебный материал по SNN.

Что не получили: работающий инструмент для production. Latency overhead, недетерминизм, проблемы с деплоем и главное — отсутствие реальной связи между firing rates и качеством генерации делают систему нежизнеспособной.

Главный урок: биологическая метафора не должна диктовать архитектуру. Если цель — менять tone of voice LLM в зависимости от эмоционального контекста, это решается системным промптом за 10 строк кода. Если цель — изучить SNN, Brian2 — отличный инструмент. Не надо смешивать.

// Код эксперимента доступен в репозитории. Проект — часть моего исследования GLaDOS и эмоциональной модуляции LLM.

// Источники и ссылки

  1. Brian2 documentation — brian2.readthedocs.io
  2. Stimberg M., Brette R., Goodman D.F.M. (2019). Brian 2, an intuitive and efficient neural simulator. eLife 8:e47314.
  3. LangChain documentation — python.langchain.com
  4. Paul Ekman — universal facial expressions
  5. Russell's circumplex model of affect — PMC
  6. Gerstner W., Kistler W.M., Naud R., Paninski L. Neuronal Dynamics.
  7. Intel Neuromorphic Research — Loihi 2
  8. SpiNNaker — University of Manchester
  9. BrainScaleS — Heidelberg
  10. OpenAI API — temperature and top_p
  11. Brian2 Emotional Model — github.com/gotogrub/Brian2-Emotional-Model

TL;DR: I built a system where a spiking neural network in Brian2 simulated five basic Ekman emotions, and the result controlled temperature and top_p for LLM calls through LangChain. It was fascinating, beautiful, and completely useless for production.

This is a follow-up to the article about mood in a language model. Back then, the idea looked like a plausible engineering path: take emotional context, pass it through neural dynamics, and get generation parameters. This piece is the honest postmortem on why that architecture does not survive contact with reality.


1. Where the Idea Came From

The question sounds innocent: what if an LLM could "feel" context? Not metaphorically, but literally — through a biologically plausible model of neural activity that adjusts generation parameters in real time.

The starting point was Paul Ekman's theory of basic emotions[4]: five universal states — joy, sadness, fear, anger, disgust — that can be encoded numerically. In parallel, Russell's circumplex model[5] provides valence/arousal axes for translating affective state into a vector.

The implementation used Brian2[1], a Python framework for biologically realistic neural-network simulation. LLM control went through LangChain[3].


2. System Architecture

2.1 Neural Network — Leaky Integrate-and-Fire

Each emotion is represented by a pool of 100 LIF neurons — Leaky Integrate-and-Fire. The membrane potential is described by this equation:

τ · dv/dt = (V_rest − v) + I_ext + ξ · σ · √τ

When the potential crosses V_thresh, the neuron emits a spike and resets to V_reset. The pool's firing rate becomes a direct measure of emotion "intensity".

# LIF neuron equation in Brian2
eqs = """
dv/dt = ((v_rest - v) + I_ext + xi * sigma_n * sqrt(tau)) / tau : volt (unless refractory)
I_ext : volt (shared)
sigma_n : volt (shared)
"""

pool = NeuronGroup(
    100,
    eqs,
    threshold="v > v_thresh",
    reset="v = v_reset",
    refractory=2*ms,
    method="euler",
)

2.2 Lateral Inhibition — Competition Between Emotions

The five pools are connected with inhibitory synapses: when pool A fires, it lowers I_ext in pool B. This creates a winner-take-most dynamic, loosely analogous to basal ganglia behavior.

# Bidirectional lateral inhibition
inhib_joy_to_fear = Synapses(pool_joy, pool_fear, on_pre="I_ext_post -= 1.5*mV")
inhib_fear_to_joy = Synapses(pool_fear, pool_joy, on_pre="I_ext_post -= 1.5*mV")

inhib_joy_to_fear.connect(p=0.25)
inhib_fear_to_joy.connect(p=0.25)

2.3 Decoding — Mapping Spikes to LLM Parameters

The dominant emotion determines the temperature, top_p, and system prompt:

Emotion temperature top_p System prompt
Joy1.1–1.30.95enthusiastic, warm
Sadness0.5–0.70.80thoughtful, gentle
Fear0.3–0.50.70cautious, precise
Anger0.9–1.10.85direct, assertive
Disgust0.6–0.80.75critical, detached

2.4 Full Pipeline

# Full pipeline: stimulus -> SNN -> LLM parameters
stimulus = Stimulus(joy=16.0, sadness=5.0, fear=8.0, anger=4.0, disgust=3.0)

# 1. Run the SNN: 300 ms of simulation
cfg = NetworkConfig(n_neurons=100, duration_ms=300, noise_sigma_mV=1.5)
net = EmotionalNetwork(cfg)
result = net.run(stimulus)

# 2. Decode into parameters
modulator = LLMModulator()
state = modulator.modulate(result)

# 3. Call the LLM
llm = ChatOpenAI(
    temperature=state.temperature,
    model_kwargs={"top_p": state.top_p},
)
chain = LLMChain(llm=llm, prompt=state.system_prompt)
response = chain.run(user_input)

3. Why It Fails in Production

3.1 Latency

A single net.run(stimulus) call is a full neural simulation: 300 ms of biological time, numerical Euler integration of differential equations, and 500 neurons in total. On CPU, this takes 150–400 ms of real time on every request.

For comparison, inference on Claude Haiku can take 200–600 ms to first token. We are adding comparable overhead just to compute two floats.

3.2 The Mapping Is Architecturally Unsound

The fundamental problem: temperature and top_p are next-token distribution parameters[10]. They are not "the model's emotions." There is no biological or computational connection between LIF firing rates and the semantics of generated text.

The same result — joy -> high temperature — can be achieved with one line:

temperature = emotion_scores["joy"] / 15.0

In this setup, Brian2 is a computationally expensive random number generator disguised as neuroscience.

3.3 Determinism and Reproducibility

Brian2 uses stochastic equations: xi * sigma * sqrt(tau), Langevin noise. The same stimulus leads to different firing rates, different parameters, and different LLM outputs. That is not "biological realism"; it is a non-deterministic bug that cannot be reproduced during debugging.

3.4 Stateful Simulation Inside a Stateless API

LLM APIs are stateless. Every request is independent. But an SNN has internal state: membrane potentials, refractory periods, synaptic dynamics. If you keep state between requests, you get concurrency problems because a single Brian2 Network object is not thread-safe. If you do not keep state, the whole idea of "emotional memory" disappears.

3.5 Synaptic Inhibition Breaks Production Logic

In the experiment, the synapse on_pre="I_ext_post -= 1.5*mV" works correctly for a 300 ms simulation. But across sequential calls, synaptic depression accumulates: after N requests, I_ext for every pool drifts into the negative range, all neurons go silent, and temperature = 0. The LLM starts generating the same deterministic token again and again.

3.6 Dependencies and Deployment

Brian2 requires a C++ compiler for JIT-compiling neural equations, plus sympy, cython, and a specific numpy version. In production, that means:


4. What Actually Works

4.1 Direct Mapping Without Simulation

If you need emotional modulation of generation parameters, use a simple mapping. It is deterministic, fast, and repeatable:

def emotion_to_params(emotion_scores):
    dominant = max(emotion_scores, key=emotion_scores.get)
    intensity = emotion_scores[dominant] / sum(emotion_scores.values())

    base_params = {
        "joy": {"temperature": 1.2, "top_p": 0.95},
        "sadness": {"temperature": 0.6, "top_p": 0.80},
        "fear": {"temperature": 0.4, "top_p": 0.70},
        "anger": {"temperature": 1.0, "top_p": 0.85},
        "disgust": {"temperature": 0.7, "top_p": 0.75},
    }

    params = base_params[dominant]
    return {k: v * intensity for k, v in params.items()}

4.2 The System Prompt Is the Real Lever

The meaningful effect does not come from temperature; it comes from the system prompt. "Respond like a person in a state of anxiety: brief, careful, without unnecessary detail" genuinely changes the tone. Mapping emotion → system prompt through LangChain PromptTemplate works well without Brian2.

# Working approach: emotion -> system prompt
EMOTION_PROMPTS = {
    "joy": "Be enthusiastic, warm, and encouraging.",
    "fear": "Be cautious, precise, and avoid overconfidence.",
    "sadness": "Be gentle, thoughtful, and empathetic.",
    "anger": "Be direct, concise, and solution-focused.",
    "disgust": "Be critical, analytical, and objective.",
}

template = ChatPromptTemplate.from_messages([
    ("system", EMOTION_PROMPTS[dominant_emotion]),
    ("human", "{input}"),
])

5. Why the Experiment Was Still Worth It

The research value fully justified the cost. Here is what the experiment actually gave me:

In neuromorphic AI, SNNs really do make sense: Loihi 2[7], SpiNNaker[8], BrainScaleS[9] — that is where hardware-accelerated inference enters the picture. But moving the same metaphor into a normal LangChain pipeline just to compute two generation parameters is not an engineering win.


6. Full Runnable Code

Below is the full experiment code, runnable through jupyter notebook emotional_brain.ipynb. It requires brian2, numpy, matplotlib, and ipywidgets.

Section 1: Environment Setup

# Install: first run only
# !pip install brian2 numpy matplotlib ipywidgets

import numpy as np
import matplotlib.pyplot as plt
from brian2 import (
    NeuronGroup,
    Synapses,
    SpikeMonitor,
    StateMonitor,
    Network,
    start_scope,
    ms,
    mV,
    second,
)

from src.encoder import StimulusEncoder, Stimulus, EMOTIONS, CIRCUMPLEX
from src.network import EmotionalNetwork, NetworkConfig
from src.decoder import LLMModulator
from src.visualizer import (
    Oscilloscope,
    RasterPlot,
    ISIHistogram,
    EmotionBarChart,
    EmotionDashboard,
    COLORS,
)

Section 2: LIF Neuron — Oscilloscope

start_scope()

TAU = 10 * ms
V_REST = -70 * mV
V_THRESH = -55 * mV
V_RESET = -75 * mV

eqs = """
dv/dt = (v_rest - v + I_ext) / tau : volt (unless refractory)
I_ext : volt
"""

neuron = NeuronGroup(
    1,
    eqs,
    threshold="v > v_thresh",
    reset="v = v_reset",
    refractory=2*ms,
    method="euler",
    namespace={
        "tau": TAU,
        "v_rest": V_REST,
        "v_thresh": V_THRESH,
        "v_reset": V_RESET,
    },
)

neuron.v = V_REST
neuron.I_ext = 16 * mV

state_mon = StateMonitor(neuron, "v", record=True)
spike_mon = SpikeMonitor(neuron)
run(200 * ms)

Section 3: A Pool of 100 Neurons

start_scope()

N = 100
DURATION = 300 * ms

eqs_pool = """
dv/dt = ((v_rest - v) + I_ext + xi * sigma_n * sqrt(tau)) / tau : volt (unless refractory)
I_ext : volt (shared)
sigma_n : volt (shared)
"""

pool = NeuronGroup(
    N,
    eqs_pool,
    threshold="v > v_thresh",
    reset="v = v_reset",
    refractory=2*ms,
    method="euler",
    namespace={
        "tau": TAU,
        "v_rest": V_REST,
        "v_thresh": V_THRESH,
        "v_reset": V_RESET,
    },
)

pool.v = "v_rest + rand() * (v_thresh - v_rest) * 0.3"
pool.I_ext = 14 * mV
pool.sigma_n = 1.5 * mV

spk_mon = SpikeMonitor(pool)
run(DURATION)

rate_hz = spk_mon.num_spikes / (N * (DURATION / second))
print(f"Pool firing rate: {rate_hz:.1f} Hz")

Section 4: Lateral Inhibition

start_scope()

# joy receives a stronger stimulus and should dominate
pool_joy = NeuronGroup(100, eqs_pool, ...)
pool_fear = NeuronGroup(100, eqs_pool, ...)

pool_joy.I_ext = 16 * mV
pool_fear.I_ext = 12 * mV

inhib_joy_to_fear = Synapses(pool_joy, pool_fear, on_pre="I_ext_post -= 1.5*mV")
inhib_fear_to_joy = Synapses(pool_fear, pool_joy, on_pre="I_ext_post -= 1.5*mV")

inhib_joy_to_fear.connect(p=0.25)
inhib_fear_to_joy.connect(p=0.25)

run(300 * ms)

Section 5: Full System + LLM Modulation

stimulus = Stimulus(
    joy=16.0,
    sadness=5.0,
    fear=8.0,
    anger=4.0,
    disgust=3.0,
)

cfg = NetworkConfig(n_neurons=100, duration_ms=300, noise_sigma_mV=1.5)
net = EmotionalNetwork(cfg)
result = net.run(stimulus)

modulator = LLMModulator()
state = modulator.modulate(result)

print(state.summary())

import json
print(json.dumps(state.llm_params(), indent=2))

# Output:
# {
#   "temperature": 1.15,
#   "top_p": 0.94,
#   "system_prompt": "Be enthusiastic and warm...",
#   "dominant_emotion": "joy"
# }

Section 6: Scenario Comparison

scenarios = [
    ("Pure Joy", Stimulus(joy=18, sadness=1, fear=1, anger=1, disgust=1)),
    ("Pure Fear", Stimulus(joy=1, sadness=1, fear=18, anger=1, disgust=1)),
    ("Pure Anger", Stimulus(joy=1, sadness=1, fear=1, anger=18, disgust=1)),
    ("Pure Sadness", Stimulus(joy=1, sadness=18, fear=1, anger=1, disgust=1)),
    ("Mixed", Stimulus(joy=10, sadness=8, fear=12, anger=6, disgust=4)),
    ("Neutral", Stimulus(joy=5, sadness=5, fear=5, anger=5, disgust=5)),
]

for name, stim in scenarios:
    r = net.run(stim)
    s = modulator.modulate(r)
    print(f"{name:15s} | temp={s.temperature:.2f} | top_p={s.top_p:.2f} | dominant={s.dominant}")

7. Conclusions

What we got: a beautiful demonstration of biologically plausible neural competition that works well as learning material for SNNs.

What we did not get: a production-ready tool. Latency overhead, non-determinism, deployment problems, and most importantly the lack of a real connection between firing rates and generation quality make the system non-viable.

The main lesson: a biological metaphor should not dictate architecture. If the goal is to change an LLM's tone of voice based on emotional context, a system prompt solves it in 10 lines of code. If the goal is to study SNNs, Brian2 is an excellent tool. Do not mix the two.

// The experiment code is available in the repository. This project is part of my GLaDOS research and LLM emotional modulation experiments.

// Sources and Links

  1. Brian2 documentation — brian2.readthedocs.io
  2. Stimberg M., Brette R., Goodman D.F.M. (2019). Brian 2, an intuitive and efficient neural simulator. eLife 8:e47314.
  3. LangChain documentation — python.langchain.com
  4. Paul Ekman — universal facial expressions
  5. Russell's circumplex model of affect — PMC
  6. Gerstner W., Kistler W.M., Naud R., Paninski L. Neuronal Dynamics.
  7. Intel Neuromorphic Research — Loihi 2
  8. SpiNNaker — University of Manchester
  9. BrainScaleS — Heidelberg
  10. OpenAI API — temperature and top_p
  11. Brian2 Emotional Model — github.com/gotogrub/Brian2-Emotional-Model