Solução do Problema CSP202109-3: Rede Neural de Pulsos

  1. Problema ===========

Aqui é necessário implementar um simulador de SNN (spiking neural network, rede neural de pulsos). Uma SNN é composta pelas seguintes partes:

  1. Neurônios: atualizam o estado interno seguindo fórmulas, recebem pulsos e podem disparar pulsos.
  2. Fontes de pulso: disparam pulsos em momentos específicos.
  3. Sinapses: conectam neurônio-neurônio ou fonte de pulso-neurônio, transmitindo pulsos.

Descrição do Problema

Os neurônios atualizam seu estado interno seguindo regras. O tempo é discretizado com um intervalo \(\Delta t\). Consideramos apenas instentes \(t = k\Delta t\) (\(k\in\mathbb{Z}^+\)). As variáveis no instante \(k\) são calculadas a partir do instante \(k-1\) conforme:

\[v_k = v_{k-1} + \Delta t(0.04 v_{k-1}^2 + 5 v_{k-1} + 140 - u_{k-1}) + I_k\]

\[u_k = u_{k-1} + \Delta t \, a (b v_{k-1} - u_{k-1})\]

Onde \(v\) e \(u\) são variáveis internas que evoluem no tempo; \(a\) e \(b\) são constantes fixas. \(I_k\) é a soma das intensidades de todos os pulsos recebidos pelo neurônio no instante \(k\). Se não houver pulsos, \(I_k = 0\). Após o cálculo, se \(v_k \ge 30\), o neurônio dispara um pulso que se propaga pelas sinapses; então \(v_k\) é reajustado para \(c\) e \(u_k\) para \(u_k + d\), onde \(c\) e \(d\) também são constantes.

As sinapses representam conexões entre neurônios ou fontes de pulso e neurônios, incluindo auto-laços e arestas múltiplas. Quando um nó de entrada (neurônio ou fonte) dispara um pulso no instante \(k\), após um atraso \(D > 0\) (no instante \(k+D\)), o nó de saída (neurônio) recebe um pulso de intensidade \(w\).

As fontes de pulso disparam com uma probabilidade a cada instante. Cada fonte possui um parâmetro \(0 < r < 32767\). A função pseudoaleatória padrão é:

static unsigned long semente = 1;
int meu_rand(void) {
   semente = semente * 1103515245 + 12345;
   return (unsigned)(semente/65536) % 32768;
}

A cada instante, cada fonte de pulso (em ordem crescente de índice) chama a função. Se \(r > \text{meu_rand}()\), a fonte dispara um pulso naquele instante, que se propaga pelas sinapses.

Dado o estado inicial dos neurônios no instante 0, simula-se do instante 1 até \(T\). Ao final, imprime-se o valor mínimo e máximo de \(v\) entre todos os neurônios no instante \(T\), e o número mínimo e máximo de disparos realizados durante toda a simulação.

Os nós são numerados: \([0, N-1]\) para neurônios, \([N, N+P-1]\) para fontes de pulso.

Use ponto flutuante de precisão dupla.

Formato de Entrada

Da entrada padrão. Primeira linha: quatro enteiros \(N\), \(S\), \(P\), \(T\). Segunda linha: um real positivo \(\Delta t\). Linhas seguintes: para cada bloco de neurônios com mesmos parâmetros, um inteiro \(R_N\) seguido de seis reais \(v, u, a, b, c, d\). A soma dos \(R_N\) é \(N\). Depois: \(P\) linhas, cada uma com um inteiro \(r\) (parâmetro da fonte). Depois: \(S\) linhas, cada uma com dois inteiros \(s\) (nó de entrada) e \(t\) (nó de saída), um real \(w>0\), e um inteiro \(D\) (atraso).

Formato de Saída

Duas linhas. Primeira: dois reais com 3 casas decimais (mínimo e máximo de \(v\) no instante \(T\)). Segunda: dois inteiros (mínimo e máximo de disparos).

Exemplo 1

Entrada:

1 1 1 10
0.1
1 -70.0 -14.0 0.02 0.2 -65.0 2.0
30000
1 0 30.0 2

Saída:

-35.608 -35.608
2 2

Exemplo 2

Entrada:

2 4 2 10
0.1
1 -70.0 -14.0 0.02 0.2 -65.0 2.0
1 -69.0 -13.0 0.04 0.1 -60.0 1.0
30000
20000
2 0 15.0 1
3 1 20.0 1
1 0 10.0 2
0 1 40.0 3

Saída:

-60.000 -22.092
1 2
  1. Abordagem ============

Este é um problema de simulação que também envolve teoria dos grafos para gerenciar os pulsos. A cada instante, as fontes geram pulsos que são enviados para as sinapses com atraso. Podemos armazenar os pulsos futuros em uma estrutura indexada pelo tempo de chegada.

A estratégia é manter um vetor de filas (ou vetor de vetores) para cada instante de tempo, registrando todos os pulsos que chegarão naquele instante. Durante a simulação, processamos os pulsos do instante atual e, para cada disparo, agendamos pulsos futuros.

  1. Código =========

Implementação em C++ moderno, reestruturada com classes e melhor organização:

#include <bits/stdc++.h>
using namespace std;

// Semente para o gerador pseudoaleatório
unsigned long seed = 1;
int myrand() {
   seed = seed * 1103515245 + 12345;
   return (unsigned)(seed / 65536) % 32768;
}

// Estrutura de um pulso agendado
struct PulseEvent {
   int target;    // índice do neurônio de destino
   double weight; // intensidade do pulso
};

// Informações de uma sinapse
struct Synapse {
   int target;    // neurônio de destino
   double weight; // peso
   int delay;     // atraso em unidades de tempo
};

int main() {
   ios::sync_with_stdio(false);
   cin.tie(nullptr);

   int N, S, P, T;
   double dt;
   cin >> N >> S >> P >> T;
   cin >> dt;

   // Parâmetros dos neurônios (índices 0..N-1)
   vector<double> v(N), u(N), a(N), b(N), c(N), d(N);
   int idx = 0;
   while (idx < N) {
       int rn;
       double vi, ui, ai, bi, ci, di;
       cin >> rn >> vi >> ui >> ai >> bi >> ci >> di;
       for (int i = 0; i < rn; ++i) {
           v[idx] = vi;
           u[idx] = ui;
           a[idx] = ai;
           b[idx] = bi;
           c[idx] = ci;
           d[idx] = di;
           ++idx;
       }
   }

   // Parâmetros das fontes (índices N..N+P-1)
   vector<double> r(P);
   for (int i = 0; i < P; ++i) {
       cin >> r[i];
   }

   // Grafo de sinapses: lista de adjacência para cada nó (neurônio ou fonte)
   // O grafo será indexado de 0 a N+P-1 para os nós de origem.
   vector<vector<Synapse>> graph(N + P);
   for (int i = 0; i < S; ++i) {
       int s, t;
       double w;
       int D;
       cin >> s >> t >> w >> D;
       graph[s].push_back({t, w, D});
   }

   // Eventos futuros: para cada instante de tempo, um vetor de pulsos que chegam
   // Tamanho T+2 para garantir acesso seguro até T+1
   vector<vector<PulseEvent>> futurePulses(T + 2);

   // Contagem de disparos de cada neurônio
   vector<int> spikeCount(N, 0);

   // corrente de entrada I para cada neurônio (zera a cada passo)
   vector<double> I(N, 0.0);

   // Simulação para t = 1 até T
   for (int time = 1; time <= T; ++time) {
       // --- 1. Fontes de pulso disparam ---
       for (int i = 0; i < P; ++i) {
           if (r[i] > myrand()) {
               // Fonte N+i dispara
               for (const Synapse& syn : graph[N + i]) {
                   int arrival = time + syn.delay;
                   if (arrival <= T) {
                       futurePulses[arrival].push_back({syn.target, syn.weight});
                   }
               }
           }
       }

       // --- 2. Processar pulsos que chegam neste instante ---
       fill(I.begin(), I.end(), 0.0);
       for (const PulseEvent& ev : futurePulses[time]) {
           I[ev.target] += ev.weight;
       }
       // Limpar a lista de pulsos futuros para este instante (libera memória)
       futurePulses[time].clear();
       futurePulses[time].shrink_to_fit();

       // --- 3. Atualizar neurônios ---
       for (int i = 0; i < N; ++i) {
           double old_v = v[i];
           double old_u = u[i];
           double new_v = old_v + dt * (0.04 * old_v * old_v + 5 * old_v + 140 - old_u) + I[i];
           double new_u = old_u + dt * a[i] * (b[i] * old_v - old_u);
           v[i] = new_v;
           u[i] = new_u;

           // Verificar disparo
           if (new_v >= 30.0) {
               spikeCount[i]++;
               v[i] = c[i];
               u[i] += d[i];

               // Agendar pulsos disparados por este neurônio
               for (const Synapse& syn : graph[i]) {
                   int arrival = time + syn.delay;
                   if (arrival <= T) {
                       futurePulses[arrival].push_back({syn.target, syn.weight});
                   }
               }
           }
       }
   }

   // Resultados finais
   double min_v = *min_element(v.begin(), v.end());
   double max_v = *max_element(v.begin(), v.end());
   int min_spikes = *min_element(spikeCount.begin(), spikeCount.end());
   int max_spikes = *max_element(spikeCount.begin(), spikeCount.end());

   cout << fixed << setprecision(3);
   cout << min_v << " " << max_v << "\n";
   cout << min_spikes << " " << max_spikes << "\n";

   return 0;
}

Tags: CSP rede neural de pulsos simulação C++ algoritmo

Publicado em 7-14 19:03