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
23 changes: 17 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `## Role and boundary matrix` documentation in `README.md`.
- Local `StimulusSource` / `SpikeSink` traits + `IngressPacket` / `SpikeEvent` (owned by this crate).
- `BackendPair` + `BackendPair::stub()` for pluggable I/O.
- In-crate stub backend (`StubStimulusSource`, `NoopSpikeSink`, `CollectingSpikeSink` under `#[cfg(test)]` for our own tests; not re-exported for downstream test use).
- In-crate stub backend (`StubStimulusSource`, `NoopSpikeSink`) plus a public
`CollectingSpikeSink` for tests and the Thalamic integration smoke harness.
- CPU-only Thalamic → `corpus-ipc` → Brainstem smoke (`tests/thalamic_brainstem_smoke.rs`,
`required-features = ["corpus-ipc"]`). A Thalamic fixture with no `SpikingNetwork`
publishes typed `IpcMessage::Stimuli(StimulusBatch)` JSON; Brainstem loads a Distill
sidecar, validates schema/width/freshness/`valid_mask`, and ticks through
`BrainstemDaemon::run_for_ticks`.
- Typed `corpus-ipc` ingress (`accept_ipc_json`): schema token
`corpus-ipc.stimulus.v1`, channel-width check (or unspecified-width when
`expected_channels = 0`), future-timestamp and max-age rejection, session_id
passthrough, and `RuntimeStats` accepted/rejected counters. Rejected frames
still advance the network. Modulation-only frames are drained in the same tick.
- `BrainstemDaemon::with_backend(cfg, pair)` constructor for tests and custom backends.
- Test coverage for the non-`corpus-ipc` (stub) path that runs under `--no-default-features`.
- Graceful `SIGTERM` handling alongside the existing `SIGINT` (Ctrl-C): the tick loop now
Expand All @@ -61,11 +72,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- Switch optional `corpus-ipc` from a git pin to crates.io `0.1.0`
(`features = ["zmq"]`). ZMQ ingress uses published `ZmqIpcBackend` /
`IpcBackend::process_batch`; egress publishes unversioned
`IpcMessage::Spikes` JSON. The binary sets `CORPUS_IPC_ZMQ_READOUT_IPC`
(what 0.1 reads) and still sets `SPIKENAUT_ZMQ_READOUT_IPC` for older
tooling.
(`features = ["zmq"]`). ZMQ SUB ingress decodes unversioned JSON
`IpcMessage` (`Stimuli` / `Neuromodulators`) with crates.io types; egress
publishes unversioned `IpcMessage::Spikes` JSON. The binary sets
`CORPUS_IPC_ZMQ_READOUT_IPC` and still sets `SPIKENAUT_ZMQ_READOUT_IPC` for
older tooling.
- Upgrade `neuromod` from 0.4.0 to crates.io **0.6.0** (pre-1.0 range
`>=0.6.0, <0.7.0`). Ingress modulators map to dopamine / serotonin /
acetylcholine / norepinephrine; `cortisol`, `tempo`, and `aux_dopamine`
Expand Down
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ corpus-ipc = ["dep:corpus-ipc", "dep:zmq"]
name = "brainstem-daemon"
path = "src/bin/brainstem_daemon.rs"

[[test]]
name = "thalamic_brainstem_smoke"
required-features = ["corpus-ipc"]

# Cargo profiles (dev / release / release-with-debug / test / bench).
# Keep settings conservative — no maximum-optimization knobs that over-promise
# for a headless inference daemon. Aligned with the `neuromod` profile pattern.
Expand Down
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ Default Cargo features are empty (`default = []` in `Cargo.toml`). That path use

Enabling the feature does **not** change `BrainstemDaemon::new()` or `try_new()`. Those always inject `BackendPair::stub()`. Only `src/bin/brainstem_daemon.rs` constructs `ZmqStimulusSource` + `ZmqSpikeSink` when `corpus-ipc` is on.

Library users who want live ZMQ must build that pair themselves under `#[cfg(feature = "corpus-ipc")]` and pass it to `with_backend` / `try_with_backend`. `BrainstemDaemon::run` / `run_with_restored_network` is the sole caller of `StimulusSource::initialize` (the binary no longer initializes first, so the pinned ZMQ backend is not reconnected). A failing `initialize` marks health **fatal** and never becomes ready.
Library users who want live ZMQ must build that pair themselves under `#[cfg(feature = "corpus-ipc")]` and pass it to `with_backend` / `try_with_backend`. `BrainstemDaemon::run` / `run_with_restored_network` / `run_for_ticks` call `StimulusSource::initialize` (the binary no longer initializes first, so the pinned ZMQ backend is not reconnected). A failing `initialize` marks health **fatal** and never becomes ready.

Health snapshots, probe paths, and the transition table live in [`docs/health.md`](docs/health.md).

Expand All @@ -166,21 +166,23 @@ Health snapshots, probe paths, and the transition table live in [`docs/health.md
| `ingress` | used (bounded class queues in the tick loop; health reports aggregate fill) | used (same queues wrap backend packets before the network step) |
| `spine_sub_port` | parsed, **no-op** | sets `CORPUS_IPC_ZMQ_READOUT_IPC` to `tcp://127.0.0.1:<port>` (also sets legacy `SPIKENAUT_ZMQ_READOUT_IPC` for compatibility) |
| `spine_pub_port` | parsed, **no-op** | binds ZMQ PUB `tcp://*:<port>` |
| `model_path` | used in **live** mode (sidecar JSON); ignored in **simulation** (`StubStimulusSource::initialize` still ignores it) | same live/simulation gate, then passed literally to `initialize` (no `~` expansion); published `ZmqIpcBackend` currently ignores `_model_path` |
| `model_path` | used in **live** mode (sidecar JSON); ignored in **simulation** (`StubStimulusSource::initialize` still ignores it) | same live/simulation gate, then passed literally to `initialize` (no `~` expansion); the ZMQ SUB source connects and ignores `_model_path` |

**Settings that only take effect with `corpus-ipc`** (the `brainstem-daemon` binary built `--features corpus-ipc`):

- `spine_sub_port` (drives `CORPUS_IPC_ZMQ_READOUT_IPC`)
- `spine_pub_port`
- `CORPUS_IPC_ZMQ_READOUT_IPC` (const `CORPUS_IPC_READOUT_ENV`; this is what published `ZmqIpcBackend::initialize` reads)
- `CORPUS_IPC_ZMQ_READOUT_IPC` (const `CORPUS_IPC_READOUT_ENV`; this is what `ZmqStimulusSource::initialize` reads when no explicit `connect` endpoint is set)

**Passed through / set, but currently unused by published `corpus-ipc` 0.1:**
**Passed through / set, but unused by the ZMQ SUB source after connect:**

- `model_path` (literal filesystem path; `~` is not expanded; passed to `initialize`, which names the argument `_model_path` and does not consume it; live-mode restore consumes it before that handshake)
- `SPIKENAUT_ZMQ_READOUT_IPC` (const `LEGACY_SPIKENAUT_READOUT_ENV`; the binary still sets this alongside `CORPUS_IPC_ZMQ_READOUT_IPC` for older tooling; published `corpus-ipc` does not read it)
- `SPIKENAUT_ZMQ_READOUT_IPC` (const `LEGACY_SPIKENAUT_READOUT_ENV`; the binary still sets this alongside `CORPUS_IPC_ZMQ_READOUT_IPC` for older tooling)

Under stub those ZMQ TOML keys are still parsed. The env vars are unset by the default binary. Nothing in this crate reads them without the `corpus-ipc` feature.

ZMQ SUB ingress decodes unversioned JSON `IpcMessage` frames (`Stimuli` / `Neuromodulators`) through crates.io `corpus-ipc` 0.1 types. Width, schema token `corpus-ipc.stimulus.v1`, freshness, and future timestamps are rejected without stopping the tick loop. Modulation-only frames are drained in the same tick so they do not consume a sensory period.

### Runtime modes: simulation vs loaded Spikenaut

`runtime_mode` is independent of the stub vs ZMQ **backend**. Backends move stimuli and spikes. The network itself is restored **before** the tick loop:
Expand Down Expand Up @@ -216,6 +218,16 @@ The allowed software artifact is the Distill sidecar JSON published as Hugging F

Live restore copies LIF weights, membrane, `last_spike`, `decay_rate`, and `threshold` (also seeding `base_threshold`) and sets `RmStdpConfig.reward_lr = 0` so dopamine-gated R-STDP cannot retrain the Distill matrix. `neuromod` 0.6.0 `SpikingNetwork::step` still assigns `decay_rate` from acetylcholine, blends `threshold` toward `0.05..=0.50`, and L1-renormalizes rows whose weights already sum above `1e-6`. Those are engine contracts; this crate does not fork `step`.

### Thalamic → corpus-ipc → Brainstem smoke

CPU-only integration coverage (no GPU) lives in `tests/thalamic_brainstem_smoke.rs` and is gated on `--features corpus-ipc` so default stub tests never need `libzmq`.

The Thalamic fixture (`tests/fixtures/thalamic_producer.rs`) produces `IpcMessage::Stimuli(StimulusBatch)` from simulated telemetry. It does not import `neuromod` or own a `SpikingNetwork`. Brainstem restores a Distill sidecar JSON checkpoint before ticking, preserves `valid_mask` and `session_id` across the wire, and rejects incompatible schema/JSON loudly. A separate assertion keeps the fixture's safety flag healthy when Brainstem/transport is absent, and a later publish cannot clobber a thermal fault.

```bash
CC=gcc CXX=g++ cargo test --locked --features corpus-ipc --test thalamic_brainstem_smoke
```

Simulation is the deliberate test/dev path for a blank network. Do not use it as a stand-in for production Spikenaut.

```toml
Expand Down
4 changes: 3 additions & 1 deletion docs/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ All jobs install **Rust 1.98.1** — the same string as `Cargo.toml`
| `corpus-ipc` | `ubuntu-latest` | `--all-features` (the `corpus-ipc` feature, which enables the optional `zmq` dependency) | clippy, build, test |

Default features are empty. Stub jobs do **not** install `libzmq` and do
not pass `--features corpus-ipc`.
not pass `--features corpus-ipc`. The Thalamic → corpus-ipc → Brainstem
integration smoke (`tests/thalamic_brainstem_smoke.rs`) is compiled only in
the Linux `corpus-ipc` job (`required-features = ["corpus-ipc"]`).

## Skips

Expand Down
Loading
Loading