refactor(nisps): delete dead core/ML mass; keep the legacy feedback modes

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<float>) 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.
This commit is contained in:
monkey-w1n5t0n 2026-07-21 12:48:27 +02:00
parent abb569b287
commit e37f16739e
10 changed files with 112 additions and 200 deletions

View file

@ -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

View file

@ -26,8 +26,6 @@
#include <cstddef>
#include <cstdint>
#include <memory>
#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<MIDIInOut> 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 <typename Mode>
NISPS_AUDIO_FUNC(inline void drain_mode_events(std::shared_ptr<MIDIInOut> midi, Mode& mode)) {
inline void drain_mode_events(std::shared_ptr<MIDIInOut> 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));

View file

@ -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

View file

@ -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 <array>
#include <cstddef>
#include <type_traits>
#include <utility>
namespace nisps {
template <typename T, std::size_t N>
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<T>) {
if (n_ >= N) return false;
buf_[n_++] = v;
return true;
}
constexpr bool push_back(T&& v) noexcept(std::is_nothrow_move_assignable_v<T>) {
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<T, N> buf_{};
size_type n_ = 0u;
};
} // namespace nisps

View file

@ -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__)

View file

@ -163,6 +163,21 @@ class DynamicStorage {
std::span<float> loss_hist_buf() noexcept { return {arena_ + off_lh_, max_iter_}; }
std::span<const float> 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<float> 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];

View file

@ -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<FbStorage>` 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 <typename M>
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 <typename M>
FeedbackAction dislike_geometric(M& mlp, std::span<const float> 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<const float>(scratch.data(), n_weights));
}
@ -648,10 +672,10 @@ class FeedbackControllerCore : public FbStorage {
template <typename M>
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 <typename M>
@ -720,9 +744,7 @@ class FeedbackControllerCore : public FbStorage {
template <typename M>
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 ----------------------------------------------

View file

@ -5,8 +5,8 @@
// buffer. Two models exist:
//
// * `FixedStorage<NIn, NH1, NH2, NH3, NOut, NMaxExamples, NMaxIterTrain>`
// (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<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<float> loss_hist_buf() noexcept { return lh_; }
NISPS_FORCE_INLINE std::span<const float> 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<float> 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<float, NIn * NHidden1> w0_{};
std::array<float, NHidden1 * NHidden2> w1_{};

View file

@ -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<int, 8> 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<int, 4> 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<int, 2> 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<int, 4> 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<int, 8> 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);
}

View file

@ -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) {