From e37f16739e228e67799c8c5b0592715aa3e454f0 Mon Sep 17 00:00:00 2001 From: monkey-w1n5t0n Date: Tue, 21 Jul 2026 12:48:27 +0200 Subject: [PATCH] refactor(nisps): delete dead core/ML mass; keep the legacy feedback modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 group 2 (L27, L26, L28, S21, L13, ST6, S20). - L27: fixed_buffer.hpp + its test + the CMake entry — no consumers. - L26: dislike_multiplier_ and its doubling/halving bookkeeping — upstream InterfaceRL residue that drove nothing. The audit pointed at the wrong test file for the surviving reference; the actual assert was in test_mlp_geo_dislike.cpp:211, removed here. - L28: added copy_weights_to(std::span) to FixedStorage and DynamicStorage and switched feedback.hpp's take_snapshot/push_undo/nudge to it. Drops the permanent whole-net flat_ scratch buffer from FixedStorage and the per-gesture double copy. Behaviour-identical: same source values, same write order, same RNG draw order in nudge(). - S21 + L13: deleted NISPS_AUDIO_MEM / NISPS_APP_SRAM / NISPS_AUDIO_FUNC — zero use sites outside perf.hpp and comments — and rewrote midi_io.hpp's one misshapen NISPS_AUDIO_FUNC use as a plain `inline void`. perf.hpp now documents only the inlining/hotness macros that actually exist, and audio_driver.hpp no longer claims an SRAM discipline the code never had. - ST6: feedback.hpp's header now describes the four current modes and the Geometric default, dropping the retracted "geometric push NOT ported" claim. S20 — OPERATOR DECISION (§7.1): the four legacy feedback behaviours (RandomiseOutputs, RandomiseMlp, AvoidStyle::Diffuse, the RandomiseMlp branch of on_drag) are KEPT, not deleted. They are wanted as building blocks for experimenting with how different instruments feel under different behaviours. Each is now marked at its definition as deliberately-retained research reserve so future audits stop flagging it as dead code. L25 (the 16 KB firmware loss-history buffer) is NOT done here — see the phase report; it turned out to be coupled into the shared mlp.hpp, and its fate belongs with the browser telemetry build (§7.3 / plan §6.5e). Gates: run-all-tests.sh ALL GREEN. --- firmware/MEMLNaut-NISPS/glue/audio_driver.hpp | 2 - firmware/MEMLNaut-NISPS/glue/midi_io.hpp | 4 +- nisps/CMakeLists.txt | 3 +- nisps/core/fixed_buffer.hpp | 77 ------------- nisps/core/perf.hpp | 26 ++--- nisps/ml/dynamic_storage.hpp | 15 +++ nisps/ml/feedback.hpp | 107 +++++++++++------- nisps/ml/storage.hpp | 25 +++- tests/cpp/test_fixed_buffer.cpp | 52 --------- tests/cpp/test_mlp_geo_dislike.cpp | 1 - 10 files changed, 112 insertions(+), 200 deletions(-) delete mode 100644 nisps/core/fixed_buffer.hpp delete mode 100644 tests/cpp/test_fixed_buffer.cpp diff --git a/firmware/MEMLNaut-NISPS/glue/audio_driver.hpp b/firmware/MEMLNaut-NISPS/glue/audio_driver.hpp index 7db54e5..48d7843 100644 --- a/firmware/MEMLNaut-NISPS/glue/audio_driver.hpp +++ b/firmware/MEMLNaut-NISPS/glue/audio_driver.hpp @@ -12,8 +12,6 @@ // The two `stereosample_t` types — the pre-existing firmware POD and the new // nisps namespaced one — have identical memory layout (two floats L,R) so the // bridge does an explicit field-wise copy. No reinterpret_cast. -// -// All hot code lives in flash-resident SRAM via NISPS_AUDIO_FUNC. #pragma once diff --git a/firmware/MEMLNaut-NISPS/glue/midi_io.hpp b/firmware/MEMLNaut-NISPS/glue/midi_io.hpp index e0f850e..d6f753e 100644 --- a/firmware/MEMLNaut-NISPS/glue/midi_io.hpp +++ b/firmware/MEMLNaut-NISPS/glue/midi_io.hpp @@ -26,8 +26,6 @@ #include #include #include - -#include "../src/nisps/core/perf.hpp" #include "../src/nisps/modes/base.hpp" #include "../src/memllib/interface/MIDIInOut.hpp" @@ -69,7 +67,7 @@ inline void bind_midi_input(std::shared_ptr midi, Mode& mode) { // Drain the mode's ControlEvent ring and dispatch each event to MIDI out. // Called at ~1 kHz from loop1(). Non-blocking; no allocations. template -NISPS_AUDIO_FUNC(inline void drain_mode_events(std::shared_ptr midi, Mode& mode)) { +inline void drain_mode_events(std::shared_ptr midi, Mode& mode) { if (!midi) return; std::array<::nisps::ControlEvent, 32u> buf{}; const std::size_t n = mode.pop_control_events(std::span<::nisps::ControlEvent>(buf)); diff --git a/nisps/CMakeLists.txt b/nisps/CMakeLists.txt index 1ed1e19..fed17bd 100644 --- a/nisps/CMakeLists.txt +++ b/nisps/CMakeLists.txt @@ -30,7 +30,7 @@ target_compile_features(nisps_core INTERFACE cxx_std_20) # --------------------------------------------------------------------------- # When building for WASM via emcmake, ${EMSCRIPTEN} is set automatically. We # don't actually emit a WASM binary from this CMakeLists yet — the WASM build -# script lives in stream 7 (playground/build) and assembles its own +# script (scripts/build-wasm.sh) assembles its own # Emscripten link command. Here we just gate the host-only test executable so # `emcmake cmake -S nisps -B build-wasm` configures cleanly. if(EMSCRIPTEN) @@ -47,7 +47,6 @@ if(NOT EMSCRIPTEN) add_executable(nisps_core_tests ${NISPS_TEST_DIR}/test_main.cpp - ${NISPS_TEST_DIR}/test_fixed_buffer.cpp ${NISPS_TEST_DIR}/test_ring_buffer.cpp ${NISPS_TEST_DIR}/test_rng.cpp ${NISPS_TEST_DIR}/test_math.cpp diff --git a/nisps/core/fixed_buffer.hpp b/nisps/core/fixed_buffer.hpp deleted file mode 100644 index e6a0b7e..0000000 --- a/nisps/core/fixed_buffer.hpp +++ /dev/null @@ -1,77 +0,0 @@ -// nisps/core/fixed_buffer.hpp — heap-free dynamic-length array with -// compile-time capacity. Replaces std::vector in hot paths. -// -// API mirrors a tiny subset of std::vector: push_back / clear / size / data / -// operator[] / iterators / front / back. NO reserve, NO resize-with-default, -// NO insert. If you need those, you're probably reaching for the wrong tool. -// -// Bounds checking: push_back returns bool (false ⇒ full, no-op). operator[] -// is unchecked — match std::vector's behavior. - -#pragma once - -#include -#include -#include -#include - -namespace nisps { - -template -class FixedBuffer { - public: - using value_type = T; - using size_type = std::size_t; - using iterator = T*; - using const_iterator = const T*; - - constexpr FixedBuffer() noexcept = default; - - // Trivially copyable when T is. Move/copy semantics fine — backing store - // is std::array, which has correct value-semantics. - - constexpr size_type size() const noexcept { return n_; } - static constexpr size_type capacity() noexcept { return N; } - constexpr bool empty() const noexcept { return n_ == 0u; } - constexpr bool full() const noexcept { return n_ == N; } - - constexpr T* data() noexcept { return buf_.data(); } - constexpr const T* data() const noexcept { return buf_.data(); } - - constexpr T& operator[](size_type i) noexcept { return buf_[i]; } - constexpr const T& operator[](size_type i) const noexcept { return buf_[i]; } - - constexpr T& front() noexcept { return buf_[0]; } - constexpr const T& front() const noexcept { return buf_[0]; } - constexpr T& back() noexcept { return buf_[n_ - 1u]; } - constexpr const T& back() const noexcept { return buf_[n_ - 1u]; } - - constexpr iterator begin() noexcept { return buf_.data(); } - constexpr const_iterator begin() const noexcept { return buf_.data(); } - constexpr iterator end() noexcept { return buf_.data() + n_; } - constexpr const_iterator end() const noexcept { return buf_.data() + n_; } - - constexpr void clear() noexcept { n_ = 0u; } - - // Returns true on success. Does NOT throw or assert when full — callers - // are expected to size the buffer correctly. - constexpr bool push_back(const T& v) noexcept(std::is_nothrow_copy_assignable_v) { - if (n_ >= N) return false; - buf_[n_++] = v; - return true; - } - constexpr bool push_back(T&& v) noexcept(std::is_nothrow_move_assignable_v) { - if (n_ >= N) return false; - buf_[n_++] = std::move(v); - return true; - } - - // pop_back — trivial decrement; does not destroy (T must be cleanup-free). - constexpr void pop_back() noexcept { if (n_ > 0u) --n_; } - - private: - std::array buf_{}; - size_type n_ = 0u; -}; - -} // namespace nisps diff --git a/nisps/core/perf.hpp b/nisps/core/perf.hpp index 1590fbc..cb86f6f 100644 --- a/nisps/core/perf.hpp +++ b/nisps/core/perf.hpp @@ -1,12 +1,13 @@ -// nisps/core/perf.hpp — RP2040/RP2350 memory section + inlining attributes. +// nisps/core/perf.hpp — RP2040/RP2350 inlining/hotness attributes. // -// On firmware builds the macros expand to GCC/Pico-specific section attributes -// so hot code/data lives in SRAM instead of XIP flash. On every other build -// (host tests, Emscripten/WASM) they are inert — the discipline of marking -// audio-critical declarations is preserved syntactically without affecting -// codegen. -// -// See architecture.md §3.4. +// On firmware builds NISPS_FORCE_INLINE/NISPS_HOT/NISPS_NOINLINE expand to +// GCC-specific attributes; on every other build (host tests, Emscripten/WASM) +// NISPS_FORCE_INLINE/NISPS_HOT are inert (NISPS_NOINLINE still applies under +// GCC/Clang host compilers). No SRAM-section placement macros are defined +// here today (the previous NISPS_AUDIO_MEM/NISPS_APP_SRAM/NISPS_AUDIO_FUNC +// regime had zero real call sites — 2026-07 simplification audit S21). If +// flash-vs-SRAM placement is ever measured to matter, add the macro(s) back +// alongside the actual hot declaration(s) that need them. #pragma once @@ -20,19 +21,10 @@ #endif #if defined(ARDUINO_ARCH_RP2040) || defined(ARDUINO_ARCH_RP2350) - // Pico SDK provides __not_in_flash and __not_in_flash_func. - // __not_in_flash takes a section name string; __not_in_flash_func wraps the - // declaration directly. - #define NISPS_AUDIO_MEM __not_in_flash("audio") - #define NISPS_AUDIO_FUNC __not_in_flash_func - #define NISPS_APP_SRAM __not_in_flash("app") #define NISPS_FORCE_INLINE __attribute__((always_inline)) inline #define NISPS_HOT __attribute__((hot)) #define NISPS_NOINLINE __attribute__((noinline)) #else - #define NISPS_AUDIO_MEM - #define NISPS_AUDIO_FUNC(decl) decl - #define NISPS_APP_SRAM #define NISPS_FORCE_INLINE inline #define NISPS_HOT #if defined(__GNUC__) || defined(__clang__) diff --git a/nisps/ml/dynamic_storage.hpp b/nisps/ml/dynamic_storage.hpp index 3f465ce..5aa485f 100644 --- a/nisps/ml/dynamic_storage.hpp +++ b/nisps/ml/dynamic_storage.hpp @@ -163,6 +163,21 @@ class DynamicStorage { std::span loss_hist_buf() noexcept { return {arena_ + off_lh_, max_iter_}; } std::span loss_hist_buf() const noexcept { return {arena_ + off_lh_, max_iter_}; } + // Copies the live weights+biases directly into `dst` in the same flat + // layout as MLPCore::get_weights() (weights layer-major, then biases + // layer-major) — see FixedStorage::copy_weights_to for the rationale. + void copy_weights_to(std::span dst) const noexcept { + std::size_t k = 0u; + for (float v : weights_l<0u>()) dst[k++] = v; + for (float v : weights_l<1u>()) dst[k++] = v; + for (float v : weights_l<2u>()) dst[k++] = v; + for (float v : weights_l<3u>()) dst[k++] = v; + for (float v : biases_l<0u>()) dst[k++] = v; + for (float v : biases_l<1u>()) dst[k++] = v; + for (float v : biases_l<2u>()) dst[k++] = v; + for (float v : biases_l<3u>()) dst[k++] = v; + } + private: void move_from_(DynamicStorage& o) noexcept { for (std::size_t i = 0; i < 5u; ++i) dims_[i] = o.dims_[i]; diff --git a/nisps/ml/feedback.hpp b/nisps/ml/feedback.hpp index a004c31..9dcb9cd 100644 --- a/nisps/ml/feedback.hpp +++ b/nisps/ml/feedback.hpp @@ -4,22 +4,40 @@ // branch feat/feedback-explore-modes) into the shared nisps/ core so the SAME // logic compiles to both WASM (browser) and RP2350 firmware. // -// Three selectable behaviours for the "down" (thumbs-down) gesture: -// * Avoid — delegate to MLP::move_weights (Gaussian perturb). This -// is the new core's "avoid"; the old firmware's k-NN -// geometric centroid push depended on a firmware-only -// ReplayMemory and is intentionally NOT ported (see -// ALIGNMENT.md). The controller owns no Avoid state. -// * RandomiseOutputs — bypass the MLP and hold a static random output vector; -// each subsequent down re-rolls it (focus-aware). Up -// commits the held output as a +1 example at the current -// input, then resumes. -// * RandomiseMlp — snapshot the live weights and randomise the net -// (draw_weights) so the user auditions a random mapping -// by moving the joystick. Down again cancels (restore). -// Up/drag commits the auditioned output as a +1 example -// then restores the original net (the kept example then -// trains the original net toward the audition). +// Four FeedbackMode behaviours for the "down" (thumbs-down) gesture: +// * ExploreAndPlace — the DEFAULT product mode (docs/adr/rl-feedback-design.md). +// Idle→Exploring→Placing→Idle scratchpad lifecycle: down +// snapshots the real net aside and auditions a random +// scratchpad; up freezes the heard output and lets the +// user place it at a new input before committing (+1 +// example, real net restored, trained toward the +// placement). See ExploreState below for the full +// granular API (firmware maps buttons directly to it). +// * Avoid — "down" trains AWAY from the heard action, via one of +// two AvoidStyle sub-modes: +// - Geometric (DEFAULT) — the ported firmware k-NN +// centroid push-away, backed by the controller's +// own ReplayMemory (see dislike_geometric() below). +// Cold-starts with a negative-LR fallback until the +// first positive is stored. The old "geometric push +// not ported" note is stale — it IS ported. +// - Diffuse — the pre-P3 undirected MLP::move_weights +// perturb. Deliberately-retained research reserve, +// not the product default (see its definition below +// and docs/adr/rl-feedback-design.md). +// * RandomiseOutputs — deliberately-retained research reserve (see its +// definition below): bypasses the MLP and holds a +// static random output vector; each subsequent down +// re-rolls it (focus-aware). Up commits the held output +// as a +1 example at the current input, then resumes. +// * RandomiseMlp — deliberately-retained research reserve (see its +// definition below): snapshots the live weights and +// randomises the net (draw_weights) so the user +// auditions a random mapping by moving the joystick. +// Down again cancels (restore). Up/drag commits the +// auditioned output as a +1 example then restores the +// original net (the kept example then trains the +// original net toward the audition). // // STORAGE POLICY (one-core-engine-refactor P2): like MLPCore, the controller // algorithms are written once in `FeedbackControllerCore` against a @@ -58,15 +76,25 @@ namespace nisps::ml { enum class FeedbackMode : std::uint8_t { Avoid = 0, // down → geometric push-away (or legacy Diffuse — see AvoidStyle). + // NOTE (2026-07, S20): RandomiseOutputs and RandomiseMlp have no current + // product caller — firmware/browser both default to ExploreAndPlace. + // They are DELIBERATELY RETAINED as research reserve: building blocks for + // experimenting with how different instruments feel under different + // down-gesture behaviours, not dead code awaiting deletion. Do not re-flag. RandomiseOutputs = 1, // down → bypass MLP, hold static random vector; re-roll each down. RandomiseMlp = 2, // down → snapshot + draw_weights live net; down-again cancels. ExploreAndPlace = 3, // Idle→Exploring→Placing→Idle scratchpad lifecycle (default product mode). }; // How the Avoid mode realises a dislike (rl-feedback-design §2.1). Geometric -// is the ported firmware behaviour (replay-backed k-NN centroid push-away); -// Diffuse is the pre-P3 undirected move_weights, kept reachable as a legacy -// sub-mode for A/B comparison. +// (DEFAULT) is the ported firmware behaviour (replay-backed k-NN centroid +// push-away). +// +// NOTE (2026-07, S20): Diffuse (the pre-P3 undirected move_weights) has no +// current product caller. It is DELIBERATELY RETAINED as research reserve — +// a building block for experimenting with how different instruments feel +// under different down-gesture behaviours, not dead code awaiting deletion. +// Do not re-flag. enum class AvoidStyle : std::uint8_t { Geometric = 0, Diffuse = 1, @@ -327,6 +355,12 @@ class FeedbackControllerCore : public FbStorage { // Drag-store (joystick freeze→reposition→release). In RandomiseMlp this is // the "reposition-commit": the caller has already stored the +1 at the new // input; we just restore the original net and end exploration. + // + // NOTE (2026-07, S20): the RandomiseMlp branch below has no current + // product caller (ExploreAndPlace's own reposition path is + // begin_reposition/commit_reposition, not on_drag). DELIBERATELY RETAINED + // as research reserve alongside RandomiseMlp itself — not dead code + // awaiting deletion. Do not re-flag. template FeedbackAction on_drag(M& mlp) noexcept { if (explore_active_ && mode_ == FeedbackMode::RandomiseMlp) { @@ -367,7 +401,6 @@ class FeedbackControllerCore : public FbStorage { std::size_t replay_size() const noexcept { return replay_count_; } std::size_t positive_count() noexcept { return replay_().positive_count(); } std::size_t negative_count() noexcept { return replay_().negative_count(); } - std::size_t dislike_multiplier() const noexcept { return dislike_multiplier_; } // Store a positive (like) into the replay so the k-NN centroid sees it. // `current_out` may be empty ⇒ the MLP's live output vector is used. The @@ -382,14 +415,12 @@ class FeedbackControllerCore : public FbStorage { // Thumbs-down at the MLP's CURRENT input with heard action `current_out` // (empty ⇒ the MLP's live outputs). Runs the full upstream sequence: - // 1. deepen-or-store the negative (dedup radius 0.05); double the - // dislike multiplier (max 16). + // 1. deepen-or-store the negative (dedup radius 0.05). // 2. cold start (no positives): train AWAY from the heard action at // lr * 0.1 * avgRewardNeg (negative LR — upstream fallback). // 3. else: k-NN(4) positive centroid → push-away target → train toward // it at lr * negLRRatio, gated by the focus/solo mask. - // 4. proportional decay + eviction of expired negatives; halve the - // multiplier per expiry. + // 4. proportional decay + eviction of expired negatives. template FeedbackAction dislike_geometric(M& mlp, std::span current_out, float lr) noexcept { @@ -403,8 +434,6 @@ class FeedbackControllerCore : public FbStorage { // 1. store/deepen the negative (InterfaceRL.cpp:42-66). replay.deepen_or_store_negative(x_neg, a_neg); - dislike_multiplier_ *= 2u; - if (dislike_multiplier_ > 16u) dislike_multiplier_ = 16u; const std::size_t pos_total = replay.positive_count(); const std::size_t neg_total = replay.negative_count(); @@ -433,13 +462,8 @@ class FeedbackControllerCore : public FbStorage { action = FeedbackAction::GeometricPush; } - // 4. decay + evict; halve the multiplier per expiry, reset when no - // negatives remain (InterfaceRL.cpp:752-760). - const std::size_t expired = replay.decay_negatives(); - for (std::size_t i = 0; i < expired; ++i) { - dislike_multiplier_ = (dislike_multiplier_ > 1u) ? dislike_multiplier_ / 2u : 1u; - } - if (expired > 0u && replay.negative_count() == 0u) dislike_multiplier_ = 1u; + // 4. decay + evict expired negatives (InterfaceRL.cpp:752-760). + replay.decay_negatives(); return action; } @@ -495,9 +519,9 @@ class FeedbackControllerCore : public FbStorage { push_undo(mlp); auto scratch = this->scratch_buf(); const std::size_t n_weights = this->n_weights(); - auto w = mlp.get_weights(); + mlp.copy_weights_to(scratch); // single copy — see take_snapshot. for (std::size_t i = 0; i < n_weights; ++i) { - scratch[i] = w[i] + rng_.next_float_gaussian(amount); + scratch[i] += rng_.next_float_gaussian(amount); } mlp.set_weights(std::span(scratch.data(), n_weights)); } @@ -648,10 +672,10 @@ class FeedbackControllerCore : public FbStorage { template void take_snapshot(M& mlp) noexcept { - auto snap = this->snapshot(); - auto w = mlp.get_weights(); // flat snapshot (size == n_weights) - const std::size_t n = this->n_weights(); - for (std::size_t i = 0; i < n; ++i) snap[i] = w[i]; + // copy_weights_to writes the live flat weights+biases straight into + // the snapshot slot — a single copy (no intermediate hop through + // get_weights()'s flat_ scratch buffer; see storage.hpp L28). + mlp.copy_weights_to(this->snapshot()); } template @@ -720,9 +744,7 @@ class FeedbackControllerCore : public FbStorage { template void push_undo(M& mlp) noexcept { auto slot = this->undo_slot(undo_head_); - auto w = mlp.get_weights(); - const std::size_t n = this->n_weights(); - for (std::size_t i = 0; i < n; ++i) slot[i] = w[i]; + mlp.copy_weights_to(slot); // single copy — see take_snapshot. const std::size_t cap = this->undo_cap(); undo_head_ = (undo_head_ + 1u) % cap; if (undo_count_ < cap) ++undo_count_; @@ -764,7 +786,6 @@ class FeedbackControllerCore : public FbStorage { // ---- Geometric dislike state --------------------------------------------- std::size_t replay_count_ = 0u; - std::size_t dislike_multiplier_ = 1u; float geo_lr_ = 0.001f; // upstream InterfaceRL.hpp:312 // ---- ExploreAndPlace state ---------------------------------------------- diff --git a/nisps/ml/storage.hpp b/nisps/ml/storage.hpp index c453dae..2828254 100644 --- a/nisps/ml/storage.hpp +++ b/nisps/ml/storage.hpp @@ -5,8 +5,8 @@ // buffer. Two models exist: // // * `FixedStorage` -// (this file) — all buffers are template-sized `std::array`, zero heap, -// `NISPS_AUDIO_MEM`-able. This is the firmware model; the classic +// (this file) — all buffers are template-sized `std::array`, zero heap. +// This is the firmware model; the classic // `MLP<...>` template is an alias over it and its compile-time constants // (`kInput`, `kHidden1..3`, `kOutput`, `weight_count()`) are preserved. // @@ -23,7 +23,7 @@ // sized fan_in(L)], eval_act_l() [const-eval scratch, // sized fan_out(L), mutable] // global: input_buf(), output_buf(), ds_features(), ds_labels(), -// flat_buf(), loss_hist_buf() +// flat_buf(), loss_hist_buf(), copy_weights_to(dst) // // For `FixedStorage` every dim accessor is constexpr-foldable, so the // algorithms compile to the same fully-unrolled/constant-bound code the old @@ -156,6 +156,25 @@ class FixedStorage { NISPS_FORCE_INLINE std::span loss_hist_buf() noexcept { return lh_; } NISPS_FORCE_INLINE std::span loss_hist_buf() const noexcept { return lh_; } + // Copies the live weights+biases directly into `dst` in the same flat + // layout as MLPCore::get_weights() (weights layer-major, then biases + // layer-major) — but writes straight from the layer buffers, with no + // intermediate flat_/flat_buf() hop. `dst` must be at least + // weight_count() long. Lets a caller that only needs a transient copy + // (feedback.hpp's snapshot/undo/nudge ops) take a single copy instead of + // double-copying through get_weights()'s scratch buffer. + void copy_weights_to(std::span dst) const noexcept { + std::size_t k = 0u; + for (float v : weights_l<0u>()) dst[k++] = v; + for (float v : weights_l<1u>()) dst[k++] = v; + for (float v : weights_l<2u>()) dst[k++] = v; + for (float v : weights_l<3u>()) dst[k++] = v; + for (float v : biases_l<0u>()) dst[k++] = v; + for (float v : biases_l<1u>()) dst[k++] = v; + for (float v : biases_l<2u>()) dst[k++] = v; + for (float v : biases_l<3u>()) dst[k++] = v; + } + private: std::array w0_{}; std::array w1_{}; diff --git a/tests/cpp/test_fixed_buffer.cpp b/tests/cpp/test_fixed_buffer.cpp deleted file mode 100644 index 7948e67..0000000 --- a/tests/cpp/test_fixed_buffer.cpp +++ /dev/null @@ -1,52 +0,0 @@ -// tests/cpp/test_fixed_buffer.cpp — exercises FixedBuffer's cursor semantics -// and capacity guard. - -#include "test_helpers.hpp" -#include "../../nisps/core/fixed_buffer.hpp" - -NISPS_TEST(fixed_buffer_starts_empty) { - nisps::FixedBuffer b; - NISPS_EXPECT(b.size() == 0u); - NISPS_EXPECT(b.empty()); - NISPS_EXPECT(!b.full()); - NISPS_EXPECT(b.capacity() == 8u); -} - -NISPS_TEST(fixed_buffer_push_and_index) { - nisps::FixedBuffer b; - NISPS_EXPECT(b.push_back(10)); - NISPS_EXPECT(b.push_back(20)); - NISPS_EXPECT(b.push_back(30)); - NISPS_EXPECT(b.size() == 3u); - NISPS_EXPECT(b[0] == 10); - NISPS_EXPECT(b[1] == 20); - NISPS_EXPECT(b[2] == 30); - NISPS_EXPECT(b.front() == 10); - NISPS_EXPECT(b.back() == 30); -} - -NISPS_TEST(fixed_buffer_refuses_when_full) { - nisps::FixedBuffer b; - NISPS_EXPECT(b.push_back(1)); - NISPS_EXPECT(b.push_back(2)); - NISPS_EXPECT(b.full()); - NISPS_EXPECT(!b.push_back(3)); // refused - NISPS_EXPECT(b.size() == 2u); // unchanged -} - -NISPS_TEST(fixed_buffer_clear_resets) { - nisps::FixedBuffer b; - b.push_back(1); b.push_back(2); - b.clear(); - NISPS_EXPECT(b.empty()); - NISPS_EXPECT(b.push_back(99)); - NISPS_EXPECT(b[0] == 99); -} - -NISPS_TEST(fixed_buffer_iteration) { - nisps::FixedBuffer b; - for (int i = 0; i < 5; ++i) b.push_back(i * 2); - int sum = 0; - for (int v : b) sum += v; - NISPS_EXPECT(sum == 0 + 2 + 4 + 6 + 8); -} diff --git a/tests/cpp/test_mlp_geo_dislike.cpp b/tests/cpp/test_mlp_geo_dislike.cpp index 82994f5..9e42852 100644 --- a/tests/cpp/test_mlp_geo_dislike.cpp +++ b/tests/cpp/test_mlp_geo_dislike.cpp @@ -208,7 +208,6 @@ NISPS_TEST(geo_dislike_cold_start_then_push) { const FeedbackAction a1 = fb.on_down(m, {}, 0.1f, 0.5f, {}); NISPS_EXPECT(a1 == FeedbackAction::GeometricColdStart); NISPS_EXPECT(fb.negative_count() == 1u); - NISPS_EXPECT(fb.dislike_multiplier() == 2u); { auto after = m.get_weights(); for (std::size_t i = 0; i < snap.size(); ++i) {