Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .devin/blueprint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Devin environment blueprint for synaptic-mesh.
#
# This file is a version-controlled REFERENCE ONLY. Devin blueprints are not
# auto-read from the repo — copy this content into Devin's
# Settings > Environment > Blueprints > synaptic-mesh UI to apply it.
# Keep this file in sync whenever the blueprint is changed in the UI.

initialize: |
rustup toolchain install stable --component clippy --component rustfmt
rustup default stable

maintenance: |
cargo fetch --locked

knowledge:
- name: format-check
contents: cargo fmt --check
- name: test
contents: cargo test --locked
- name: lint
contents: cargo clippy --all-features -- -D warnings
- name: review-gate
contents: |
Before opening or updating a PR, run every command in REVIEW.md
(format check, locked test matrix, clippy, regression guards,
diff hygiene, origin hygiene). All must pass; REVIEW.md's
"Pass criteria" section is the merge bar.
- name: repo-map
contents: See AGENTS.md for module map, entry order, and design principles.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
.air/
.opencode/
.cline/
.ai/
.junie/

# Isolated Git Worktrees
.worktrees/
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,17 @@ let decision = router.route(&[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0]);
- **Neuron State Snapshots** — Per-neuron adaptation and error tracking for dynamic routing decisions.
- **Routing Policies** — Configurable scoring equations that balance spike activity, adaptation penalties, and error bonuses.

## Crate Boundary

`synaptic-mesh` does **not** depend on the separate [`neuromod`](https://github.com/Limen-Neural/neuromod) crate. The two crates are kept independent so each can evolve without coupling:

| Crate | Owns |
|-------|------|
| **neuromod** | Canonical neuron models — LIF, Izhikevich, Hodgkin-Huxley, GIF, FitzHugh-Nagumo, Lapicque |
| **synaptic-mesh** | Topology, wiring, delay infrastructure, CSR sparse maps, `ChannelRouter`, and its router-internal `NeuromodNeuron` (NIF) integration primitive |

`NeuromodNeuron` lives in [`router`](src/router.rs) — it's a router-internal integration primitive, not a general-purpose neuron model.

## Architecture

```text
Expand Down
6 changes: 3 additions & 3 deletions REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ After any "security" or dependency PR, confirm core product APIs still exist:
```bash
# Check for key structs and modules
rg -n 'pub struct SynapticMesh' src/mesh.rs
rg -n 'pub struct NeuromodNeuron' src/neuromod.rs
rg -n 'pub struct NeuromodNeuron' src/router.rs
rg -n 'pub struct SynapticGraph' src/topology/graph.rs
rg -n 'pub mod topology' src/lib.rs
rg -n 'pub mod neuromod' src/lib.rs
rg -n 'pub use router::\{[^}]*NeuromodNeuron' src/lib.rs # confirms the router re-export (not a removed `neuromod` module)
```

## Diff hygiene
Expand All @@ -58,7 +58,7 @@ git check-ignore -v .worktrees .idea target

## Do not merge if

- `src/mesh.rs` or `src/neuromod.rs` are unexpectedly altered or removed.
- `src/mesh.rs` or `src/router.rs` are unexpectedly altered or removed.
- `git diff origin/main` shows unexpected public-API removals.

## Pass criteria
Expand Down
20 changes: 13 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,17 @@
//! | [`delay`] | Temporal delay infrastructure — ring-buffer spike queues for tick-aligned delivery with configurable axonal propagation delays |
//! | [`mesh`] | [`SynapticMesh`] orchestrator — the top-level struct owning topology + delays, provides `propagate()` for spike → current conversion |
//! | [`sparse`] | Compressed Sparse Row (CSR) synaptic maps for GPU-optimized weight matrices |
//! | [`router`] | Generic multi-channel SNN router using neuromodulatory neurons |
//! | [`neuromod`] | Neuromodulatory (NIF) neuron model with gain control |
//! | [`router`] | Generic multi-channel SNN router using [`NeuromodNeuron`], a router-internal NIF integration primitive |
//!
//! ## Crate boundary
//!
//! `synaptic-mesh` does **not** depend on the separate `neuromod` crate. The
//! two crates are kept independent so each can evolve without coupling:
//!
//! | Crate | Owns |
//! |-------|------|
//! | **neuromod** | Canonical neuron models — LIF, Izhikevich, Hodgkin-Huxley, GIF, FitzHugh-Nagumo, Lapicque |
//! | **synaptic-mesh** | Topology, wiring, delay infrastructure, CSR sparse maps, [`router::ChannelRouter`], and its router-internal [`NeuromodNeuron`] integration primitive |
//!
//! ## Quick start
//!
Expand Down Expand Up @@ -90,7 +99,6 @@
pub mod delay;
pub mod error;
pub mod mesh;
pub mod neuromod;
pub mod topology;
pub mod types;

Expand All @@ -109,10 +117,8 @@ pub use types::{
ConnectionModel, DelayModel, DelayTicks, NeuronId, Polarity, SynapseDescriptor, TopologyConfig,
};

pub use neuromod::NeuromodNeuron;

// Generic router exports
pub use router::{ChannelRouter, NeuromodState, RouterConfig, RoutingDecision};
// Generic router exports (NeuromodNeuron is a router-internal NIF primitive; see "Crate boundary" above)
pub use router::{ChannelRouter, NeuromodNeuron, NeuromodState, RouterConfig, RoutingDecision};

// Backward-compatible router exports (deprecated)
pub use router::{AHL_NUM_CHANNELS, AhlRouter};
Expand Down
83 changes: 0 additions & 83 deletions src/neuromod.rs

This file was deleted.

82 changes: 81 additions & 1 deletion src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
//! neuromodulatory routing — channels strengthen with use (dopamine-gated)
//! and weaken when idle (use-it-or-lose-it plasticity).

use crate::neuromod::NeuromodNeuron;
use serde::{Deserialize, Serialize};

/// Number of input channels for the default 3-channel router (backward compatible).
Expand All @@ -21,6 +20,87 @@ const ROUTING_TIMESTEPS: usize = 16;
/// Minimum firing rate (spikes / `ROUTING_TIMESTEPS`) to activate a channel.
const MIN_FIRE_RATE: f32 = 0.1875;

/// Neuromodulatory Integrative Fixed-threshold (NIF) neuron.
///
/// This is a **router-internal integration primitive** for [`ChannelRouter`],
/// not a general-purpose neuron model — canonical neuron models (LIF,
/// Izhikevich, Hodgkin-Huxley, GIF, FitzHugh-Nagumo, Lapicque) live in the
/// separate `neuromod` crate, which `synaptic-mesh` intentionally does not
/// depend on. See the crate-level docs for the full boundary rationale.
///
/// $V_{t+1} = V_t + (G \cdot I_{syn}) - \lambda(V_t - V_{rest})$
/// where $G$ is the modulation gain and $\lambda$ is the leak rate.
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(default)]
pub struct NeuromodNeuron {
Comment thread
rmems marked this conversation as resolved.
/// Current membrane potential.
pub v: f32,
/// Resting membrane potential.
pub v_rest: f32,
/// Reset potential after a spike.
pub v_reset: f32,
/// Passive leak rate per timestep.
pub leak: f32,
/// Firing threshold.
pub threshold: f32,

/// Neuromodulatory gain (scales incoming stimulus).
pub gain: f32,

/// Synaptic weights — one per input channel.
pub weights: Vec<f32>,
/// Whether the neuron fired in the last timestep.
pub last_spike: bool,
}

impl Default for NeuromodNeuron {
fn default() -> Self {
Self {
v: 0.0,
v_rest: 0.0,
v_reset: 0.0,
leak: 0.12,
threshold: 0.25,
gain: 1.0,
weights: Vec::new(),
last_spike: false,
}
}
}

impl NeuromodNeuron {
pub fn new() -> Self {
Self::default()
}

/// Advance neuron dynamics by one timestep.
///
/// The `stimulus` is scaled by the neuron's current `gain`.
pub fn integrate(&mut self, stimulus: f32) {
// Apply modulated integration
self.v += stimulus * self.gain;
// Apply leak towards resting potential
self.v -= (self.v - self.v_rest) * self.leak;
}

/// Check if the neuron spikes. Resets V on fire.
pub fn check_fire(&mut self) -> Option<f32> {
if self.v >= self.threshold {
let peak = self.v;
self.v = self.v_reset;
self.last_spike = true;
return Some(peak);
}
self.last_spike = false;
None
}

/// Update the modulation gain.
pub fn set_gain(&mut self, new_gain: f32) {
self.gain = new_gain;
}
}

/// Configuration for a generic channel router.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RouterConfig {
Expand Down
27 changes: 25 additions & 2 deletions src/tests.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: MIT OR Apache-2.0

use crate::neuromod::NeuromodNeuron;
use crate::router::{ChannelRouter, NeuromodState, RouterConfig};
use crate::router::{ChannelRouter, NeuromodNeuron, NeuromodState, RouterConfig};

#[test]
fn channel_0_pulse_activates_channel_0() {
Expand Down Expand Up @@ -243,6 +242,30 @@ fn deserialized_router_with_empty_inner_baseline_weights_recovers() {
);
}

#[test]
fn deserialized_router_with_missing_neuron_fields_recovers() {
// Regression test: older or hand-authored payloads may omit fields from a
// NeuromodNeuron. Deserialization must use the neuron's defaults so the
// router's existing lazy state repair can run on the first route.
let router = ChannelRouter::new();
let mut json: serde_json::Value =
serde_json::to_value(&router).expect("Fresh router must serialize");
json["neurons"][0]
.as_object_mut()
.unwrap()
.remove("weights");

let mut router: ChannelRouter = serde_json::from_value(json)
.expect("Missing neuron fields should deserialize using defaults");

let result = router.route_modulated([0.5, 0.0, 0.0], &NeuromodState::balanced());
assert!(
result.is_ok(),
"Router with missing neuron fields must self-heal on first route: {:?}",
result.err()
);
}

#[test]
fn apply_feedback_on_deserialized_router_with_malformed_baseline_does_not_panic() {
// Regression test for chatgpt-codex P2 thread #NyuBl / devin-ai BUG #NytR0.
Expand Down
Loading