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:
parent
abb569b287
commit
e37f16739e
10 changed files with 112 additions and 200 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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__)
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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 ----------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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_{};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue