Spaces:
Sleeping
Sleeping
Download src/core/neuron.py from ThomasHeisig/MHRN-Space: direct link, hf CLI and curl.
- Browser
- Download file 22.8 kB
-
https://huggingface.co/spaces/ThomasHeisig/MHRN-Space/resolve/main/src/core/neuron.py
- Command line
-
hf download hf://spaces/ThomasHeisig/MHRN-Space/src/core/neuron.py
-
curl -L -o neuron.py https://huggingface.co/spaces/ThomasHeisig/MHRN-Space/resolve/main/src/core/neuron.py
22.8 kB
| """Deterministic, versioned single-neuron primitive used by the MHRN core. | |
| The historical Izhikevich implementation remains the canonical default. The | |
| membrane-dynamics model is explicit and provenance-bearing, so alternative | |
| models can be compared without silently changing the meaning of an experiment. | |
| Secondary mechanisms can be disabled for an isolated Stage-0 reference cell. | |
| """ | |
| from __future__ import annotations | |
| import math | |
| from dataclasses import dataclass, field, fields, replace | |
| from enum import Enum, auto | |
| from typing import Any, Callable, cast | |
| from .neuron_models import ( | |
| NeuronModel, | |
| apply_spike_reset, | |
| available_neuron_models, | |
| coerce_neuron_model, | |
| get_model_descriptor, | |
| integrate_membrane, | |
| spike_threshold, | |
| ) | |
| class NeuronType(Enum): | |
| """Functional neuron labels with established Izhikevich parameter sets.""" | |
| REGULAR_SPIKING = auto() | |
| FAST_SPIKING = auto() | |
| INTRINSICALLY_BURSTING = auto() | |
| CHATTERING = auto() | |
| LOW_THRESHOLD_SPIKING = auto() | |
| RESONATOR = auto() | |
| SENSORY = auto() | |
| MOTOR = auto() | |
| def default_params(self) -> tuple[float, float, float, float]: | |
| params = { | |
| NeuronType.REGULAR_SPIKING: (0.02, 0.2, -65.0, 8.0), | |
| NeuronType.FAST_SPIKING: (0.1, 0.2, -65.0, 2.0), | |
| NeuronType.INTRINSICALLY_BURSTING: (0.02, 0.2, -55.0, 4.0), | |
| NeuronType.CHATTERING: (0.02, 0.2, -50.0, 2.0), | |
| NeuronType.LOW_THRESHOLD_SPIKING: (0.02, 0.25, -65.0, 2.0), | |
| NeuronType.RESONATOR: (0.1, 0.26, -65.0, 2.0), | |
| NeuronType.SENSORY: (0.02, 0.2, -65.0, 8.0), | |
| NeuronType.MOTOR: (0.02, 0.2, -65.0, 8.0), | |
| } | |
| return params.get(self, (0.02, 0.2, -65.0, 8.0)) | |
| class NeuronConfig: | |
| """Configuration for membrane dynamics and optional secondary mechanisms.""" | |
| model: NeuronModel | str = NeuronModel.IZHIKEVICH | |
| dt_ms: float = 1.0 | |
| a: float = 0.02 | |
| b: float = 0.2 | |
| c: float = -65.0 | |
| d: float = 8.0 | |
| initial_v: float = -65.0 | |
| initial_u: float = -13.0 | |
| izhikevich_threshold: float = 30.0 | |
| lif_resting_potential: float = -65.0 | |
| lif_tau_m_ms: float = 20.0 | |
| lif_resistance: float = 1.0 | |
| lif_threshold: float = -50.0 | |
| lif_reset: float = -65.0 | |
| refractory_ticks: int = 0 | |
| threshold_adaptation_rate: float = 0.01 | |
| threshold_adaptation_decay: float = 0.999 | |
| spike_cost: float = 0.001 | |
| resting_energy: float = 1.0 | |
| energy_recovery_rate: float = 0.0001 | |
| trace_decay: float = 0.95 | |
| trace_increment: float = 1.0 | |
| target_rate_hz: float = 10.0 | |
| firing_rate_tau_ms: float = 1000.0 | |
| homeostasis_learning_rate: float = 0.001 | |
| enable_threshold_adaptation: bool = True | |
| enable_energy_dynamics: bool = True | |
| enable_traces: bool = True | |
| enable_homeostasis: bool = True | |
| def __post_init__(self) -> None: | |
| object.__setattr__(self, "model", coerce_neuron_model(self.model)) | |
| if self.dt_ms <= 0.0: | |
| raise ValueError("dt_ms must be > 0") | |
| if self.firing_rate_tau_ms <= 0.0: | |
| raise ValueError("firing_rate_tau_ms must be > 0") | |
| if self.lif_tau_m_ms <= 0.0: | |
| raise ValueError("lif_tau_m_ms must be > 0") | |
| if self.refractory_ticks < 0: | |
| raise ValueError("refractory_ticks must be >= 0") | |
| def isolated_reference( | |
| cls, | |
| model: NeuronModel | str = NeuronModel.IZHIKEVICH, | |
| **overrides: Any, | |
| ) -> "NeuronConfig": | |
| values: dict[str, Any] = { | |
| "model": model, | |
| "enable_threshold_adaptation": False, | |
| "enable_energy_dynamics": False, | |
| "enable_traces": False, | |
| "enable_homeostasis": False, | |
| } | |
| values.update(overrides) | |
| return cls(**values) | |
| def to_dict(self) -> dict[str, Any]: | |
| payload = {item.name: getattr(self, item.name) for item in fields(self)} | |
| payload["model"] = coerce_neuron_model(self.model).value | |
| return payload | |
| def from_dict(cls, data: dict[str, Any]) -> "NeuronConfig": | |
| names = {item.name for item in fields(cls)} | |
| return cls(**{key: value for key, value in data.items() if key in names}) | |
| class Neuron: | |
| """One deterministic spiking neuron with an explicit dynamics model.""" | |
| neuron_id: int | |
| a: float = 0.02 | |
| b: float = 0.2 | |
| c: float = -65.0 | |
| d: float = 8.0 | |
| v: float = -65.0 | |
| u: float = -13.0 | |
| energy: float = 1.0 | |
| spike_cost: float = 0.001 | |
| spike_counter: int = 0 | |
| last_spike_tick: int = -1 | |
| threshold_adaptation: float = 0.0 | |
| last_external_current: float = 0.0 | |
| last_synaptic_current: float = 0.0 | |
| neuron_type: NeuronType = NeuronType.REGULAR_SPIKING | |
| model: NeuronModel | str = NeuronModel.IZHIKEVICH | |
| pre_trace: float = 0.0 | |
| post_trace: float = 0.0 | |
| firing_rate_estimate: float = 0.0 | |
| model_switch_count: int = 0 | |
| last_model_switch_tick: int = -1 | |
| _config: NeuronConfig | None = field(default=None, repr=False, init=False) | |
| _enabled: bool = field(default=True, repr=False, init=False) | |
| _spike_count_window: int = 0 | |
| _last_update_tick: int = 0 | |
| _refractory_until_tick: int = -1 | |
| _has_stepped: bool = False | |
| _dirty_callback: Callable[[], None] | None = field( | |
| default=None, repr=False, init=False | |
| ) | |
| def __post_init__(self) -> None: | |
| self.model = coerce_neuron_model(self.model) | |
| if self._config is None: | |
| self._config = NeuronConfig(model=self.model) | |
| if ( | |
| self.model is NeuronModel.IZHIKEVICH | |
| and self.neuron_type is not NeuronType.REGULAR_SPIKING | |
| ): | |
| params = self.neuron_type.default_params | |
| if self.a == 0.02 and self.b == 0.2 and self.c == -65.0 and self.d == 8.0: | |
| self.a, self.b, self.c, self.d = params | |
| def set_dirty_callback(self, callback: Callable[[], None] | None) -> None: | |
| self._dirty_callback = callback | |
| def mark_dirty(self) -> None: | |
| if self._dirty_callback is not None: | |
| self._dirty_callback() | |
| def is_inhibitory(self) -> bool: | |
| return self.neuron_type is NeuronType.FAST_SPIKING | |
| def config(self) -> NeuronConfig: | |
| if self._config is None: | |
| self._config = NeuronConfig(model=self.model) | |
| return self._config | |
| def set_config(self, config: NeuronConfig) -> None: | |
| target = coerce_neuron_model(config.model) | |
| if target is not self.model: | |
| if self._has_stepped: | |
| raise ValueError( | |
| "Cannot change neuron model through set_config after execution; " | |
| "use switch_model(..., tick=...) so provenance is recorded" | |
| ) | |
| self.model = target | |
| self._config = config | |
| def model_provenance(self) -> dict[str, object]: | |
| descriptor = get_model_descriptor(self.model).to_dict() | |
| descriptor["parameters"] = self.config.to_dict() | |
| descriptor["switch_count"] = self.model_switch_count | |
| descriptor["last_switch_tick"] = self.last_model_switch_tick | |
| return descriptor | |
| def current_threshold(self) -> float: | |
| base = spike_threshold( | |
| self.model, | |
| izhikevich_threshold=self.config.izhikevich_threshold, | |
| lif_threshold=self.config.lif_threshold, | |
| ) | |
| return ( | |
| base + self.threshold_adaptation | |
| if self.config.enable_threshold_adaptation | |
| else base | |
| ) | |
| def switch_model( | |
| self, | |
| model: NeuronModel | str, | |
| *, | |
| tick: int, | |
| config: NeuronConfig | None = None, | |
| reset_state: bool = True, | |
| ) -> None: | |
| target = coerce_neuron_model(model) | |
| if tick < self._last_update_tick: | |
| raise ValueError( | |
| "model switch tick cannot precede the current neuron state" | |
| ) | |
| next_config = config or replace(self.config, model=target) | |
| if coerce_neuron_model(next_config.model) is not target: | |
| raise ValueError("switch_model config.model must match target model") | |
| if target is self.model and next_config == self.config: | |
| return | |
| self.model = target | |
| self._config = next_config | |
| self.model_switch_count += 1 | |
| self.last_model_switch_tick = tick | |
| self._refractory_until_tick = -1 | |
| if reset_state: | |
| self.v = ( | |
| next_config.lif_resting_potential | |
| if target is NeuronModel.LEAKY_INTEGRATE_AND_FIRE | |
| else next_config.initial_v | |
| ) | |
| self.u = next_config.initial_u | |
| self.threshold_adaptation = 0.0 | |
| self.firing_rate_estimate = 0.0 | |
| self._spike_count_window = 0 | |
| self.reset_traces() | |
| self.mark_dirty() | |
| def _record_currents( | |
| self, | |
| input_current: float, | |
| external_current: float | None, | |
| synaptic_current: float | None, | |
| ) -> None: | |
| if external_current is not None or synaptic_current is not None: | |
| ext = 0.0 if external_current is None else float(external_current) | |
| syn = 0.0 if synaptic_current is None else float(synaptic_current) | |
| if not math.isclose(ext + syn, input_current, rel_tol=1e-12, abs_tol=1e-12): | |
| raise ValueError( | |
| "external_current + synaptic_current must equal input_current" | |
| ) | |
| self.last_external_current = ext | |
| self.last_synaptic_current = syn | |
| return | |
| observed_total = self.last_external_current + self.last_synaptic_current | |
| if not math.isclose( | |
| observed_total, input_current, rel_tol=1e-12, abs_tol=1e-12 | |
| ): | |
| self.last_external_current = float(input_current) | |
| self.last_synaptic_current = 0.0 | |
| def _reset_potential(self) -> float: | |
| return ( | |
| self.config.lif_reset | |
| if self.model is NeuronModel.LEAKY_INTEGRATE_AND_FIRE | |
| else self.c | |
| ) | |
| def _finish_tick(self, tick: int) -> None: | |
| if self.config.enable_traces: | |
| self._decay_traces() | |
| if self.config.enable_energy_dynamics: | |
| self.energy = min(1.0, self.energy + self.config.energy_recovery_rate) | |
| if self.config.enable_threshold_adaptation: | |
| self.threshold_adaptation *= self.config.threshold_adaptation_decay | |
| if self.config.enable_homeostasis: | |
| self._apply_homeostasis() | |
| self._last_update_tick = tick | |
| self._has_stepped = True | |
| self.mark_dirty() | |
| def step( | |
| self, | |
| input_current: float, | |
| tick: int, | |
| *, | |
| external_current: float | None = None, | |
| synaptic_current: float | None = None, | |
| ) -> bool: | |
| if not self._enabled: | |
| return False | |
| self._record_currents(input_current, external_current, synaptic_current) | |
| if tick <= self._refractory_until_tick: | |
| self.v = self._reset_potential() | |
| self._update_firing_rate(spiked=False, tick=tick) | |
| self._finish_tick(tick) | |
| return False | |
| self.v, self.u = integrate_membrane( | |
| self.model, | |
| v=self.v, | |
| u=self.u, | |
| input_current=input_current, | |
| dt_ms=self.config.dt_ms, | |
| a=self.a, | |
| b=self.b, | |
| lif_resting_potential=self.config.lif_resting_potential, | |
| lif_tau_m_ms=self.config.lif_tau_m_ms, | |
| lif_resistance=self.config.lif_resistance, | |
| ) | |
| spiked = self.v >= self.current_threshold | |
| if spiked: | |
| self.v, self.u = apply_spike_reset( | |
| self.model, | |
| v=self.v, | |
| u=self.u, | |
| c=self.c, | |
| d=self.d, | |
| lif_reset=self.config.lif_reset, | |
| ) | |
| self.spike_counter += 1 | |
| self.last_spike_tick = tick | |
| if self.config.enable_energy_dynamics: | |
| self.energy = max(0.0, self.energy - self.spike_cost) | |
| if self.config.enable_threshold_adaptation: | |
| self.threshold_adaptation += self.config.threshold_adaptation_rate | |
| if self.config.enable_traces: | |
| self.pre_trace += self.config.trace_increment | |
| self.post_trace += self.config.trace_increment | |
| if self.config.refractory_ticks > 0: | |
| self._refractory_until_tick = tick + self.config.refractory_ticks | |
| self._update_firing_rate(spiked=spiked, tick=tick) | |
| self._finish_tick(tick) | |
| return spiked | |
| def _decay_traces(self) -> None: | |
| self.pre_trace *= self.config.trace_decay | |
| self.post_trace *= self.config.trace_decay | |
| def get_pre_trace(self) -> float: | |
| return self.pre_trace | |
| def get_post_trace(self) -> float: | |
| return self.post_trace | |
| def reset_traces(self) -> None: | |
| self.pre_trace = 0.0 | |
| self.post_trace = 0.0 | |
| def _update_firing_rate(self, *, spiked: bool, tick: int) -> None: | |
| """Update a low-pass firing-rate estimate in Hz once per simulated tick.""" | |
| elapsed_ticks = ( | |
| max(1, tick - self._last_update_tick) if self._has_stepped else 1 | |
| ) | |
| elapsed_ms = elapsed_ticks * self.config.dt_ms | |
| alpha = 1.0 - math.exp(-elapsed_ms / self.config.firing_rate_tau_ms) | |
| instantaneous_rate_hz = 1000.0 / elapsed_ms if spiked else 0.0 | |
| self.firing_rate_estimate = ( | |
| 1.0 - alpha | |
| ) * self.firing_rate_estimate + alpha * instantaneous_rate_hz | |
| window_ticks = max(1, int(round(1000.0 / self.config.dt_ms))) | |
| if tick % window_ticks == 0: | |
| self._spike_count_window = 0 | |
| if spiked: | |
| self._spike_count_window += 1 | |
| def _apply_homeostasis(self) -> None: | |
| error = self.firing_rate_estimate - self.config.target_rate_hz | |
| self.threshold_adaptation += self.config.homeostasis_learning_rate * error | |
| self.threshold_adaptation = max(-10.0, min(10.0, self.threshold_adaptation)) | |
| def drain_energy(self, amount: float) -> float: | |
| drained = min(amount, self.energy) | |
| self.energy -= drained | |
| return drained | |
| def restore_energy(self, amount: float) -> float: | |
| restored = min(amount, 1.0 - self.energy) | |
| self.energy += restored | |
| return restored | |
| def enable(self) -> None: | |
| self._enabled = True | |
| def disable(self) -> None: | |
| self._enabled = False | |
| def is_enabled(self) -> bool: | |
| return self._enabled | |
| def reset_state(self, reset_v: bool = True) -> None: | |
| if reset_v: | |
| self.v = ( | |
| self.config.lif_resting_potential | |
| if self.model is NeuronModel.LEAKY_INTEGRATE_AND_FIRE | |
| else self.config.initial_v | |
| ) | |
| self.u = self.config.initial_u | |
| self.threshold_adaptation = 0.0 | |
| self.energy = self.config.resting_energy | |
| self.firing_rate_estimate = 0.0 | |
| self._spike_count_window = 0 | |
| self._last_update_tick = 0 | |
| self._refractory_until_tick = -1 | |
| self._has_stepped = False | |
| self.reset_traces() | |
| self.mark_dirty() | |
| def reset_full(self) -> None: | |
| self.reset_state(reset_v=True) | |
| self.spike_counter = 0 | |
| self.last_spike_tick = -1 | |
| def to_dict(self) -> dict[str, Any]: | |
| return { | |
| "schema_version": 2, | |
| "neuron_id": self.neuron_id, | |
| "model": coerce_neuron_model(self.model).value, | |
| "model_version": get_model_descriptor(self.model).version, | |
| "config": self.config.to_dict(), | |
| "a": self.a, | |
| "b": self.b, | |
| "c": self.c, | |
| "d": self.d, | |
| "v": self.v, | |
| "u": self.u, | |
| "energy": self.energy, | |
| "spike_cost": self.spike_cost, | |
| "spike_counter": self.spike_counter, | |
| "last_spike_tick": self.last_spike_tick, | |
| "threshold_adaptation": self.threshold_adaptation, | |
| "last_external_current": self.last_external_current, | |
| "last_synaptic_current": self.last_synaptic_current, | |
| "neuron_type": self.neuron_type.name, | |
| "pre_trace": self.pre_trace, | |
| "post_trace": self.post_trace, | |
| "firing_rate_estimate": self.firing_rate_estimate, | |
| "model_switch_count": self.model_switch_count, | |
| "last_model_switch_tick": self.last_model_switch_tick, | |
| "enabled": self._enabled, | |
| "spike_count_window": self._spike_count_window, | |
| "last_update_tick": self._last_update_tick, | |
| "refractory_until_tick": self._refractory_until_tick, | |
| "has_stepped": self._has_stepped, | |
| } | |
| def from_dict(cls, data: dict[str, Any]) -> "Neuron": | |
| neuron_type = NeuronType[data.get("neuron_type", "REGULAR_SPIKING")] | |
| config_data = data.get("config") | |
| if isinstance(config_data, dict): | |
| config = NeuronConfig.from_dict(cast(dict[str, Any], config_data)) | |
| else: | |
| config = NeuronConfig( | |
| model=data.get("model", NeuronModel.IZHIKEVICH.value), | |
| a=float(data.get("a", 0.02)), | |
| b=float(data.get("b", 0.2)), | |
| c=float(data.get("c", -65.0)), | |
| d=float(data.get("d", 8.0)), | |
| spike_cost=float(data.get("spike_cost", 0.001)), | |
| ) | |
| model = coerce_neuron_model(data.get("model", config.model)) | |
| neuron = cls( | |
| neuron_id=int(data["neuron_id"]), | |
| a=float(data.get("a", config.a)), | |
| b=float(data.get("b", config.b)), | |
| c=float(data.get("c", config.c)), | |
| d=float(data.get("d", config.d)), | |
| v=float(data.get("v", config.initial_v)), | |
| u=float(data.get("u", config.initial_u)), | |
| energy=float(data.get("energy", config.resting_energy)), | |
| spike_cost=float(data.get("spike_cost", config.spike_cost)), | |
| spike_counter=int(data.get("spike_counter", 0)), | |
| last_spike_tick=int(data.get("last_spike_tick", -1)), | |
| threshold_adaptation=float(data.get("threshold_adaptation", 0.0)), | |
| last_external_current=float(data.get("last_external_current", 0.0)), | |
| last_synaptic_current=float(data.get("last_synaptic_current", 0.0)), | |
| neuron_type=neuron_type, | |
| model=model, | |
| pre_trace=float(data.get("pre_trace", 0.0)), | |
| post_trace=float(data.get("post_trace", 0.0)), | |
| firing_rate_estimate=float(data.get("firing_rate_estimate", 0.0)), | |
| model_switch_count=int(data.get("model_switch_count", 0)), | |
| last_model_switch_tick=int(data.get("last_model_switch_tick", -1)), | |
| ) | |
| neuron.set_config(config) | |
| neuron._enabled = bool(data.get("enabled", True)) | |
| neuron._spike_count_window = int(data.get("spike_count_window", 0)) | |
| neuron._last_update_tick = int(data.get("last_update_tick", 0)) | |
| neuron._refractory_until_tick = int(data.get("refractory_until_tick", -1)) | |
| neuron._has_stepped = bool( | |
| data.get( | |
| "has_stepped", neuron.spike_counter > 0 or neuron._last_update_tick != 0 | |
| ) | |
| ) | |
| return neuron | |
| def __str__(self) -> str: | |
| return ( | |
| f"Neuron(id={self.neuron_id}, type={self.neuron_type.name}, " | |
| f"model={coerce_neuron_model(self.model).value}, v={self.v:.1f}mV, " | |
| f"spikes={self.spike_counter}, rate={self.firing_rate_estimate:.1f}Hz)" | |
| ) | |
| def __repr__(self) -> str: | |
| return self.__str__() | |
| def create_neuron( | |
| neuron_id: int, | |
| neuron_type: NeuronType = NeuronType.REGULAR_SPIKING, | |
| config: NeuronConfig | None = None, | |
| **kwargs: Any, | |
| ) -> Neuron: | |
| params = neuron_type.default_params | |
| config_a = config.a if config is not None else params[0] | |
| config_b = config.b if config is not None else params[1] | |
| config_c = config.c if config is not None else params[2] | |
| config_d = config.d if config is not None else params[3] | |
| initial_v = config.initial_v if config is not None else -65.0 | |
| initial_u = config.initial_u if config is not None else -13.0 | |
| initial_energy = config.resting_energy if config is not None else 1.0 | |
| spike_cost = config.spike_cost if config is not None else 0.001 | |
| configured_model = config.model if config is not None else NeuronModel.IZHIKEVICH | |
| model = coerce_neuron_model(kwargs.get("model", configured_model)) | |
| if config is not None and model is not coerce_neuron_model(config.model): | |
| raise ValueError("Explicit model must match config.model") | |
| neuron = Neuron( | |
| neuron_id=neuron_id, | |
| a=kwargs.get("a", config_a), | |
| b=kwargs.get("b", config_b), | |
| c=kwargs.get("c", config_c), | |
| d=kwargs.get("d", config_d), | |
| v=kwargs.get("v", initial_v), | |
| u=kwargs.get("u", initial_u), | |
| energy=kwargs.get("energy", initial_energy), | |
| spike_cost=kwargs.get("spike_cost", spike_cost), | |
| spike_counter=kwargs.get("spike_counter", 0), | |
| last_spike_tick=kwargs.get("last_spike_tick", -1), | |
| threshold_adaptation=kwargs.get("threshold_adaptation", 0.0), | |
| neuron_type=neuron_type, | |
| model=model, | |
| pre_trace=kwargs.get("pre_trace", 0.0), | |
| post_trace=kwargs.get("post_trace", 0.0), | |
| firing_rate_estimate=kwargs.get("firing_rate_estimate", 0.0), | |
| ) | |
| if config is not None: | |
| neuron.set_config(config) | |
| return neuron | |
| def create_random_neuron( | |
| neuron_id: int, | |
| rng: Any, | |
| neuron_type: NeuronType | None = None, | |
| ) -> Neuron: | |
| if neuron_type is None: | |
| neuron_type = rng.choice(list(NeuronType)) | |
| assert neuron_type is not None | |
| params = neuron_type.default_params | |
| a = max(0.01, min(0.2, params[0] * (1.0 + rng.uniform(-0.1, 0.1)))) | |
| b = max(0.1, min(0.5, params[1] * (1.0 + rng.uniform(-0.1, 0.1)))) | |
| c = max(-80.0, min(-40.0, params[2] + rng.uniform(-5.0, 5.0))) | |
| d = max(1.0, min(15.0, params[3] + rng.uniform(-1.0, 1.0))) | |
| return create_neuron( | |
| neuron_id, | |
| neuron_type, | |
| a=a, | |
| b=b, | |
| c=c, | |
| d=d, | |
| v=rng.uniform(-70.0, -50.0), | |
| u=rng.uniform(-20.0, -5.0), | |
| energy=rng.uniform(0.8, 1.0), | |
| ) | |
| __all__ = [ | |
| "Neuron", | |
| "NeuronConfig", | |
| "NeuronModel", | |
| "NeuronType", | |
| "available_neuron_models", | |
| "create_neuron", | |
| "create_random_neuron", | |
| ] | |