Source code for mercurial.core.pattern_completion

"""Pattern completion network for olfactory/gustatory modalities with empirical parameters."""

from typing import List, Optional

import numpy as np

from mercurial.params.empirical import HebbianParams


[docs] class HopfieldPatternCompletion: """ Hopfield‑style attractor network for pattern completion, parameterised by empirical olfactory bulb and piriform cortex data. """
[docs] def __init__( self, n_neurons: int = 8000, # approximate human glomeruli count stored_patterns: Optional[List[np.ndarray]] = None, tau_a: float = 0.020, # 20 ms neural time constant tau_f: float = 1.0, # fatigue time constant (s) beta_f: float = 0.5, # fatigue scaling noise_amp: float = 0.01, beta_sigmoid: float = 10.0, hebb_params: Optional[HebbianParams] = None, ): """ Parameters ---------- n_neurons : int Number of units (default 8000, approximate human olfactory bulb glomeruli). stored_patterns : list of np.ndarray, optional Binary patterns {-1,1} or continuous [0,1] to store. tau_a, tau_f : float Time constants for activity and fatigue (s). beta_f : float Fatigue scaling. noise_amp : float Stochastic noise amplitude. beta_sigmoid : float Gain of the sigmoid (high for binary attractors). hebb_params : HebbianParams, optional Empirical Hebbian learning parameters. """ self.N = n_neurons self.tau_a = tau_a self.tau_f = tau_f self.beta_f = beta_f self.noise_amp = noise_amp self.beta_sigmoid = beta_sigmoid self.rng = np.random.default_rng() if hebb_params is None: hebb_params = HebbianParams() self.eta = hebb_params.learning_rate self.gamma = hebb_params.decay self.r = np.zeros(n_neurons) self.f = np.zeros(n_neurons) self.W = np.zeros((n_neurons, n_neurons)) if stored_patterns is not None: self.store_patterns(stored_patterns)
[docs] def store_patterns(self, patterns: List[np.ndarray]) -> None: pats = [] for p in patterns: if np.max(p) <= 1.0 and np.min(p) >= 0.0: pats.append(2 * p - 1) else: pats.append(p) self.W = np.zeros((self.N, self.N)) for mu in range(len(pats)): xi = pats[mu] self.W += np.outer(xi, xi) self.W /= self.N np.fill_diagonal(self.W, 0.0)
[docs] def sigmoid(self, x: float) -> float: arg = self.beta_sigmoid * x if arg > 500: return 1.0 if arg < -500: return 0.0 return 1.0 / (1.0 + np.exp(-arg))
[docs] def step(self, dt: float, external_input: np.ndarray) -> np.ndarray: h_eff = external_input - self.f total_input = self.W @ self.r + h_eff r_target = np.array([self.sigmoid(x) for x in total_input]) dr = (-self.r + r_target) / self.tau_a noise = self.noise_amp * np.sqrt(dt) * self.rng.normal(size=self.N) self.r += dr * dt + noise self.r = np.clip(self.r, 0.0, 1.0) df = (-self.f + self.beta_f * self.r) / self.tau_f self.f += df * dt self.f = np.clip(self.f, 0.0, 1.0) return self.r.copy()
[docs] def get_overlap(self, pattern: np.ndarray) -> float: if np.max(pattern) <= 1.0 and np.min(pattern) >= 0.0: pat = 2 * pattern - 1 else: pat = pattern r_bipolar = 2 * self.r - 1 return np.dot(r_bipolar, pat) / self.N
[docs] def reset(self): self.r.fill(0.0) self.f.fill(0.0)