docs(firmware): vendor InterfaceRL as read-only reference

InterfaceRL.{hpp,cpp,tpp} + InterfaceRLFileFormat.hpp are the upstream
reference implementation of the entire NISPS feedback subsystem —
nisps/ml/{geo_push,replay,feedback,jolt,ou_noise}.hpp are all ports of it,
several still carrying `// upstream InterfaceRL.hpp:NNN` line references.
The Phase-4 vendoring dropped examples/ because nothing compiled it. That
was correct for the build and wrong for the codebase: with the source of
truth out of tree, upstream redesigned the geometric dislike (deleted the
/(1+len) taper, doubled kGeometricPushScale, tripled the negative-LR base,
moved to batch training over all negatives every tick) and we did not notice
for months.

Copied verbatim from memllib @ e291192 — the same commit the rest of the
vendored tree pins — into lib/memllib/reference/, which sits OUTSIDE src/
and is therefore never compiled: PlatformIO's LDF only recursively builds an
Arduino-format library's src/ folder. Verified: slpworkshop still builds
(RAM 28.8%, flash 2.1%).

reference/README.md states the two rules (never compiled, never edited — a
divergence from upstream is a recorded decision, not an edit here) and
VENDORED.md's "what was dropped" section now tells the truth.

Resolves ALIGNMENT defect 6c.
This commit is contained in:
monkey-w1n5t0n 2026-07-25 11:14:35 +02:00
parent f57cddc278
commit 1f0eecfe78
8 changed files with 1578 additions and 12 deletions

View file

@ -105,14 +105,6 @@ still not a design.
(`ml_bench` U4 sweeps dose; U1 sweeps upstream's soft-target alpha, where alpha=1 is (`ml_bench` U4 sweeps dose; U1 sweeps upstream's soft-target alpha, where alpha=1 is
NISPS today). It wants a matched-N head-to-head, not a guess. NISPS today). It wants a matched-N head-to-head, not a guess.
### 6c. `InterfaceRL` — the reference implementation — is not in the tree (2026-07-25)
**What.** It lives in `memllib/examples/`, and the vendoring dropped `examples/`
(`VENDORED.md`). So the source of truth for our most contested subsystem is absent, and
the divergences in 6b went unnoticed for months. Either vendor
`examples/InterfaceRL.{hpp,cpp,tpp}` read-only alongside the rest, or record its pinned
commit and a fetch recipe in `VENDORED.md`.
## Open mission questions ## Open mission questions
### Q1: Per-mode MLP architectures or one shared shape? (2026-04-29) ### Q1: Per-mode MLP architectures or one shared shape? (2026-04-29)
@ -137,6 +129,10 @@ Legacy a-immersive was mobile-first; Manifold is desktop-first. Defer until user
## Recently resolved (delete after a few weeks) ## Recently resolved (delete after a few weeks)
- 2026-07-25: **`InterfaceRL` is back in the tree (defect 6c).** Vendored verbatim from
memllib `e291192` at `firmware/MEMLNaut-NISPS/lib/memllib/reference/` — outside `src/`,
so PlatformIO never compiles it. Upstream drift in the feedback subsystem is a `diff`
again rather than an archaeology session.
- 2026-07-25: **The optimiser mismatch (defect 6) is fixed.** `nisps/ml/training.hpp` was - 2026-07-25: **The optimiser mismatch (defect 6) is fixed.** `nisps/ml/training.hpp` was
SGD-only while upstream `memlp` (`ea777502`) applies **RMSProp everywhere**, so every SGD-only while upstream `memlp` (`ea777502`) applies **RMSProp everywhere**, so every
learning rate we ported landed in an optimiser that reads it differently — an RMSProp learning rate we ported landed in an optimiser that reads it differently — an RMSProp

2
MAP.md
View file

@ -27,7 +27,7 @@ MEMLNaut-NISPS — Neural Interactive Shaping of Parameter Spaces. One C++20 cod
- `selftest.hpp` — standalone guided hardware self-test rig (`SelfTest` variant; no engine/ML). Step-driven state machine on a `SelfTestView`: TFT prompts the operator through every control, auto-advances on detection, encoder-press skips. Ends with optional L/R/BOTH sine-sweep headphone check (core 1 block callback) + MIDI loopback-cable test. Lives firmware-side (touches TFT + raw pins) so it stays out of platform-agnostic `nisps/`. - `selftest.hpp` — standalone guided hardware self-test rig (`SelfTest` variant; no engine/ML). Step-driven state machine on a `SelfTestView`: TFT prompts the operator through every control, auto-advances on detection, encoder-press skips. Ends with optional L/R/BOTH sine-sweep headphone check (core 1 block callback) + MIDI loopback-cable test. Lives firmware-side (touches TFT + raw pins) so it stays out of platform-agnostic `nisps/`.
- `output_router.hpp` — top-level `drain_outputs()` entry point. (Inputs are wired directly by `peripherals.hpp`'s `bind_peripherals()`.) - `output_router.hpp` — top-level `drain_outputs()` entry point. (Inputs are wired directly by `peripherals.hpp`'s `bind_peripherals()`.)
- `settings_view.hpp``wire_settings(mode)`: adds on-device settings views to the MEMLNaut display carousel (TFT + rotary encoder). Joystick Dual/Single toggle for the 4-input ("two 2-D joystick") modes — "Single" pins ML input channels 2,3 to neutral via `ModeBase::set_input_pinned` (no net rebuild). Registered in the `.ino` after `addSystemInfoView()`. - `settings_view.hpp``wire_settings(mode)`: adds on-device settings views to the MEMLNaut display carousel (TFT + rotary encoder). Joystick Dual/Single toggle for the 4-input ("two 2-D joystick") modes — "Single" pins ML input channels 2,3 to neutral via `ModeBase::set_input_pinned` (no net rebuild). Registered in the `.ino` after `addSystemInfoView()`.
- `firmware/MEMLNaut-NISPS/lib/memllib/`**vendored** memllib (was the `src/memllib` submodule): hardware abstraction (audio driver, TFT display, MIDI, peripherals), ~1.9 MB / 100 files, `examples/` dropped. `VENDORED.md` records the upstream commit and the re-sync procedure; `LICENSE` is MPL-2.0, copied verbatim. **Sources must sit under `lib/memllib/src/`** — PlatformIO's library builder falls back to a flat root-only scan without it and silently compiles nothing while still linking (see VENDORED.md). - `firmware/MEMLNaut-NISPS/lib/memllib/`**vendored** memllib (was the `src/memllib` submodule): hardware abstraction (audio driver, TFT display, MIDI, peripherals), ~1.9 MB / 100 files, `examples/` dropped **except** `reference/InterfaceRL.{hpp,cpp,tpp}` + `InterfaceRLFileFormat.hpp` — upstream's reference implementation of the feedback subsystem `nisps/ml/{geo_push,replay,feedback,jolt,ou_noise}.hpp` were ported from, kept verbatim and NEVER compiled (`reference/` sits outside `src/`, which is the only thing PlatformIO builds). It is there so upstream drift is a `diff`; losing it is how the e291192 geometric-dislike redesign went unnoticed for months. `VENDORED.md` records the upstream commit and the re-sync procedure; `LICENSE` is MPL-2.0, copied verbatim. **Sources must sit under `lib/memllib/src/`** — PlatformIO's library builder falls back to a flat root-only scan without it and silently compiles nothing while still linking (see VENDORED.md).
- `firmware/README.md` — structure + build instructions. - `firmware/README.md` — structure + build instructions.
- `firmware/useq-celium/` — standalone RP2040 firmware (PlatformIO, Arduino-Pico core) that turns a uSEQ module + CV expander into a USB→CV/gate converter driven by the manifold `cvgate` backend. `shared/protocol.h` is the v2 wire-protocol single source of truth (mirrored by `manifold/src/backends/useq-protocol.ts`); `main/` (USB serial → CV13 + GATE13, I2C → expander) and `expander/` (I2C slave → CV411). Wire spec: `docs/specs/useq-cv-protocol.md`. Restored from the April-2026 "uSEQ-Celium" mode. - `firmware/useq-celium/` — standalone RP2040 firmware (PlatformIO, Arduino-Pico core) that turns a uSEQ module + CV expander into a USB→CV/gate converter driven by the manifold `cvgate` backend. `shared/protocol.h` is the v2 wire-protocol single source of truth (mirrored by `manifold/src/backends/useq-protocol.ts`); `main/` (USB serial → CV13 + GATE13, I2C → expander) and `expander/` (I2C slave → CV411). Wire spec: `docs/specs/useq-cv-protocol.md`. Restored from the April-2026 "uSEQ-Celium" mode.

View file

@ -23,9 +23,15 @@ migration) — only the wrapping `src/` folder and the `library.properties` mani
new, both required for PlatformIO to discover and recursively compile this tree (see new, both required for PlatformIO to discover and recursively compile this tree (see
below). below).
Dropped: `examples/` (17 files — never compiled; the firmware never referenced it, and Dropped: `examples/` (never compiled; the firmware never referenced it, and its content
its content that mattered was already ported into `nisps/ml/{jolt,ou_noise,feedback, that mattered was already ported into `nisps/ml/{jolt,ou_noise,feedback,geo_push}.hpp`
geo_push}.hpp` per the pre-Phase-4 submodule-bump decision), `.git` (submodule gitlink), per the pre-Phase-4 submodule-bump decision) — **except** `InterfaceRL.{hpp,cpp,tpp}` and
`InterfaceRLFileFormat.hpp`, which were added back on 2026-07-25 under `reference/`.
Dropping them was correct for the build and wrong for the codebase: `InterfaceRL` is the
source of truth for the whole feedback subsystem we ported, and with it out of tree we
missed upstream's redesign of the geometric dislike for months. `reference/` sits outside
`src/`, so PlatformIO does not compile it; see `reference/README.md`. Also dropped:
`.git` (submodule gitlink),
`.gitignore` (build-artifact patterns, meaningless once vendored — this repo's own `.gitignore` (build-artifact patterns, meaningless once vendored — this repo's own
`.gitignore` covers it), `README.md` (described the old Arduino-IDE TFT_eSPI `.gitignore` covers it), `README.md` (described the old Arduino-IDE TFT_eSPI
`User_Setup_Select.h` copy-paste workflow, which PlatformIO replaces with `User_Setup_Select.h` copy-paste workflow, which PlatformIO replaces with

View file

@ -0,0 +1,6 @@
// InterfaceRL is now a class template (InterfaceRL<N_OUTPUTS>); its definitions
// live in InterfaceRL.tpp, included from InterfaceRL.hpp so each per-mode
// instantiation is available at its use site. This translation unit is
// intentionally empty (kept so existing build scripts that reference it still
// find a file).
#include "InterfaceRL.hpp"

View file

@ -0,0 +1,492 @@
#ifndef INTERFACERL_HPP
#define INTERFACERL_HPP
#include "../interface/InterfaceBase.hpp"
#include "../../memlp/StaticMLP.h"
#include "../../memlp/ReplayMemory.hpp"
#include "../../memlp/OrnsteinUhlenbeckNoise.h"
#include <memory>
#include "../utils/sharedMem.hpp"
#include "../PicoDefs.hpp"
//#include "../hardware/memlnaut/display.hpp"
#include "../hardware/memlnaut/display/MessageView.hpp"
#include "../interface/UARTInput.hpp"
#include "../interface/MIDIInOut.hpp"
#include "../hardware/memlnaut/display/MessageView.hpp"
#include "../hardware/memlnaut/display/BarGraphView.hpp"
#include "../hardware/memlnaut/display/RLView.hpp"
#include "../hardware/memlnaut/display/BlockSelectView.hpp"
#include "../hardware/memlnaut/display/SingleSelectView.hpp"
#include "../hardware/memlnaut/display/RotarySelectView.hpp"
#include "../hardware/memlnaut/display/NameInputView.hpp"
#include "../hardware/memlnaut/display/CCSelectView.hpp"
#include "InterfaceRLFileFormat.hpp"
#define RL_MEM __not_in_flash("rlmem")
struct trainStatelessRLItem {
std::vector<float> input ;
std::vector<float> action;
float reward;
};
// Non-template base holding the nested types + constants that callers reference
// without a template argument (e.g. InterfaceRLBase::INPUT_MODES in the modes
// and the .ino). Because this (and InterfaceBase) are *non-dependent* bases of
// InterfaceRL<N_OUTPUTS>, the template sees every inherited member by ordinary
// lookup — no this->/using-declarations required.
class InterfaceRLBase : public InterfaceBase
{
public:
using OnMIDICtrlCallback = std::function<void(uint8_t)>;
// Training data type (independent of the network's output width).
using training_pair_t = std::pair<std::vector<std::vector<float>>,
std::vector<std::vector<float>>>;
static constexpr size_t kMaxNNInputs = 10;
enum class INPUT_MODES {
JOYSTICK,
MACHINE_LISTENING,
JOYSTICK_AND_MACHINE_LISTENING,
SERIAL_INPUT
};
enum class INPUT_SOURCE : uint8_t {
JOYSTICK_3D = 0,
JOYSTICK_4D,
MACHINE_LISTENING,
MIDI_1CC,
MIDI_3CC,
MIDI_8CC,
COMBINED,
COUNT
};
enum class MEMORY_STORE_MODES {
ADD,
REPLACE_5_PERCENT,
REPLACE_10_PERCENT,
REPLACE_15_PERCENT,
REWARD_DECAY_10_PERCENT,
REWARD_DECAY_20_PERCENT
};
// Input-source state is independent of the network's output width, so it
// lives in the base: this lets N-agnostic helpers (e.g. MachineListeningMixin)
// hold an InterfaceRLBase* and still query/configure the input source.
INPUT_SOURCE getInputSource() const { return input_source_; }
void setHasMachineListening(bool v) { hasMachineListening_ = v; }
protected:
INPUT_SOURCE input_source_ = INPUT_SOURCE::JOYSTICK_3D;
bool hasMachineListening_ = false;
};
// The RL interface. N_OUTPUTS (the active mode's parameter count) is fixed at
// compile time, so the synth-mapping network is a static-memory StaticMLP with
// no heap allocation. The mode declares e.g. InterfaceRL<MyApp::kN_Params>.
template<size_t N_OUTPUTS>
class InterfaceRL : public InterfaceRLBase
{
public:
// Compile-time mapping network: kMaxNNInputs -> 16 -> 16 -> N_OUTPUTS.
using SynthMLP = smlp::StaticMLP<float,
smlp::Layout<kMaxNNInputs, 16, 16, N_OUTPUTS>,
smlp::Activations<RELU, RELU, HARDSIGMOID>,
loss::LOSS_FUNCTIONS::LOSS_MSE>;
InterfaceRL() : InterfaceRLBase()
// , ou_noise(0.02f, 0.0f, 0.2f, 0.001f, 0.0f)
{
}
void setup(size_t n_inputs, size_t n_outputs, bool addMessageView = true);
void optimise();
inline void setState(const size_t index, float value) {
controlInput[index] = value;
newInput = true;
}
// Force the next loop to regenerate + re-send the action, even if no input changed.
// Use when an output-stage parameter (e.g. a fade/home value) changes.
inline void markInputDirty() { newInput = true; }
void readAnalysisParameters(std::vector<float> params) override;
void generateAction(bool donthesitate=false);
inline void optimiseSometimes() {
if (optimiseCounter>=optimiseDivisor) {
optimise();
optimiseCounter=0;
newInput = true;
}else{
optimiseCounter++;
}
}
void storeExperience(float reward, std::vector<float> &experienceState, std::vector<float> &experienceAction );
#define randomWeightVariance 1.f
inline void randomiseTheNetwork()
{
synthMapping.RandomiseWeightsAndBiasesLin(-0.9f,1.1f, -0.9f, 0.3f);
newInput = true;
resetMinMaxFlag = true;
}
inline void setOptimiseDivisor(size_t newDiv) {
optimiseDivisor = newDiv;
}
void setOptimiseDivisorInterf(float value);
inline void forgetMemory() {
replayMem.clear();
}
inline void setRewardScale(float scale) {
rewardScale = scale;
}
inline void setLRScale(const float scale) {
learningRateScaled = learningRate * scale; // knob at 0 -> LR 0 -> training off (intended)
String msg = "LR scale: " + String(scale);
if (msgView) msgView->post(msg);
}
void setRewardScaleInterf(float value);
inline void setNoiseLevel(float level) {
// Knob [0,1] -> roaming amplitude (the OU walk's stationary std) in param space.
// theta/dt (set in setup) fix the smoothness; this only sets how far each param
// drifts from the mapping output. Low = gentle local wander; full = slow sweeps
// across the whole [0,1] range. kMaxAmplitude sets the "depth": higher reaches
// deeper into the param space (and interacts more with the [0,1] rails via the
// gentle reflection in generateAction), lower keeps it shallower/more local.
constexpr float kMaxAmplitude = 0.65f;
float amplitude = level * kMaxAmplitude;
if (amplitude < 0.01f) {
amplitude = 0.f;
if (msgView) msgView->post("Noise off");
} else {
String msg = "Explore amount: " + String(amplitude, 3);
if (msgView) msgView->post(msg);
}
for(auto& ou_noise: ou_noises) {
ou_noise->setStationaryStd(amplitude);
}
// Learning stays active during exploration on purpose: likes/dislikes given while
// the noise roams are what steer the network toward sounds the player wants.
if (nnOutputsGraphView) nnOutputsGraphView->setNoiseActive(amplitude > 0.f);
}
// Jolt = permanent weight modulation (B2, held). At press, pick a random subset of
// weights scattered across the net and roll a bounded random target for each. While
// held, EMA-glide each toward its target (stepJolt, called per loop); release just
// freezes them, so the change persists. Bounded by construction (interpolation toward
// targets in the weight-init range can't run away) and smooth (no per-tick jitter).
// Runs on the main loop / under the mlpActive lock, so touching the MLP here is safe.
inline float randomJoltTarget() const {
return kJoltWeightMin + (static_cast<float>(rand()) / RAND_MAX) * (kJoltWeightMax - kJoltWeightMin);
}
inline void startJolt() {
joltActive_ = true;
joltWeightLoc_.clear();
joltTarget_.clear();
// StaticMLP exposes a flat view over all weights (layer 0 first); pick
// random global indices to modulate.
const size_t total = SynthMLP::TotalWeights();
if (total == 0) return;
for (size_t i = 0; i < kJoltNumWeights; i++) {
joltWeightLoc_.push_back(rand() % total);
joltTarget_.push_back(randomJoltTarget());
}
if (nnOutputsGraphView) nnOutputsGraphView->setLastAction("jolt");
if (msgView) msgView->post("Jolt: morphing weights");
}
inline void stepJolt() {
const size_t total = SynthMLP::TotalWeights();
for (size_t i = 0; i < joltWeightLoc_.size(); i++) {
const size_t idx = joltWeightLoc_[i];
if (idx >= total) continue; // stale after a model load
float* wp = synthMapping.WeightPtrAt(idx);
if (!wp) continue;
float& w = *wp;
w += kJoltMorphRate * (joltTarget_[i] - w);
// Reached this target (EMA only asymptotes, so use a threshold) -> roll a new
// one, keeping the weight in motion for as long as the button is held.
float gap = joltTarget_[i] - w;
if (gap < 0.f) gap = -gap;
if (gap < kJoltTargetEpsilon) joltTarget_[i] = randomJoltTarget();
}
markInputDirty(); // weights changed -> regenerate + re-send the action
}
inline void stopJolt() {
joltActive_ = false; // weights stay where they morphed to (permanent)
joltLRRamp_ = 0.f; // resume learning from 0, ramping back to full over ~5s
}
void bind_RL_interface(INPUT_MODES input_mode = INPUT_MODES::JOYSTICK, bool joystick4D = false);
void bindInterface(INPUT_MODES input_mode = INPUT_MODES::JOYSTICK,bool joystick4D = false) {
bind_RL_interface(input_mode, joystick4D);
}
void bindInterface(bool disable_joystick=false, bool joystick4D = false) {
bind_RL_interface(disable_joystick ? INPUT_MODES::MACHINE_LISTENING : INPUT_MODES::JOYSTICK, joystick4D);
}
void bindUARTInput(std::shared_ptr<UARTInput> uart_input,
const std::vector<size_t>& kUARTListenInputs)
{
uart_input->SetCallback([this](size_t channel, float value) {
// Serial.println("UART input: " + String(channel) + " value: " + String(value));
if (channel < controlInput.size()) {
setState(channel, value);
}
});
}
void bindMIDI(std::shared_ptr<MIDIInOut> midi_interf, bool enableFootcontroller=false);
void setModeInfo(const String& modeRoot, const String& modeTag);
using ExtraSaveDataFn = std::function<std::vector<uint8_t>()>;
using ExtraLoadDataFn = std::function<void(const uint8_t*, uint16_t, uint16_t)>;
void setExtraSaveCallback(ExtraSaveDataFn fn) { _extraSaveFn = fn; }
void setExtraLoadCallback(ExtraLoadDataFn fn) { _extraLoadFn = fn; }
using RVCallback = std::function<void(float)>;
void setRVX1Override(RVCallback fn) { rvX1Override = std::move(fn); }
void setRVY1Override(RVCallback fn) { rvY1Override = std::move(fn); }
void setRVZ1Override(RVCallback fn) { rvZ1Override = std::move(fn); }
void setActiveDims(std::vector<bool> dims) { activeDims_ = std::move(dims); }
std::function<void(std::vector<float>&)> inputInjectionHook;
void trigger_like();
void trigger_dislike();
inline void getAction(std::vector<float> &out_action) {
out_action = action;
}
size_t getActiveInputCount() const {
switch (input_source_) {
case INPUT_SOURCE::JOYSTICK_3D: return 3;
case INPUT_SOURCE::JOYSTICK_4D: return 4;
case INPUT_SOURCE::MACHINE_LISTENING: return 6;
case INPUT_SOURCE::MIDI_1CC: return 1;
case INPUT_SOURCE::MIDI_3CC: return 3;
case INPUT_SOURCE::MIDI_8CC: return 8;
case INPUT_SOURCE::COMBINED: return kMaxNNInputs;
default: return kMaxNNInputs;
}
}
const std::vector<float>& getControlInput() const { return controlInput; }
// Recompute the unused-input pad value. Done only when the input mode changes (not per
// frame). Scales as 1.1/n_unused so the total constant injected into layer 1 stays
// bounded regardless of how many dims are unused — avoids over-driving the net.
void updateUnusedInputDefault() {
const size_t used = getActiveInputCount();
const size_t unused = (used < kMaxNNInputs) ? (kMaxNNInputs - used) : 0;
unusedInputDefault_ = (unused > 0) ? (1.1f / static_cast<float>(unused)) : 0.f;
}
// persist=false applies the change in memory + updates the bar graph without writing
// flash. A flash write stalls XIP execution on the RP2040 (blanking the display), so
// the rotary-driven path applies immediately but debounces the save (see loopCallback).
void setInputSource(INPUT_SOURCE src, bool persist = true) {
input_source_ = src;
updateUnusedInputDefault();
if (persist) saveInputSource();
if (nnInputsGraphView) nnInputsGraphView->setNumDisplayBars(getActiveInputCount());
}
// ISR-safe entry point (the rotary-encoder dispatch runs in interrupt context).
// setInputSource() does heap allocation (bar-graph resize), SPI (fillRect) and flash
// file IO — all unsafe in an ISR — so only record the request here and let the main
// loop apply it via the pendingInputSourceChange_ handler in bind_RL_interface().
void requestInputSource(INPUT_SOURCE src) {
pendingInputSource_ = src;
pendingInputSourceChange_ = true;
}
void addInputSourceView(bool includeCCSelect = true);
void SetMIDI5Callback(OnMIDICtrlCallback _cb_) {
midi5cb = _cb_;
}
void SetMIDI6Callback(OnMIDICtrlCallback _cb_) {
midi6cb = _cb_;
}
// Display views
std::shared_ptr<MessageView> msgView;
std::shared_ptr<BlockSelectView> fileSaveView;
std::shared_ptr<BlockSelectView> fileLoadView;
std::shared_ptr<NameInputView> nameInputView;
std::shared_ptr<BarGraphView> nnInputsGraphView;
std::shared_ptr<RLView> nnOutputsGraphView;
std::shared_ptr<SingleSelectView> memoryStoreModeView;
std::shared_ptr<CCSelectView> ccSelectView;
const std::vector<float>& getLastAction() const { return action; }
protected:
// Helper methods for trigger actions
void _perform_like_action();
void _perform_dislike_action();
void _perform_randomiseRL_action();
bool _save_RL_to_SD(String id);
bool _load_RL_from_SD(String id);
void _forget_replay_mem_interf();
void _saveSlotNames();
void _loadSlotNames();
static constexpr int kNumSlots = 12;
String slotNames[kNumSlots];
int pendingSaveSlot = -1;
private:
OnMIDICtrlCallback midi5cb = nullptr;
OnMIDICtrlCallback midi6cb = nullptr;
RVCallback rvX1Override;
RVCallback rvY1Override;
RVCallback rvZ1Override;
static constexpr size_t bias=1;
size_t optimiseDivisor = 1;
size_t optimiseCounter = 0;
bool newInput=false;
bool actionBeingDragged=false;
std::vector<size_t> itemsToRemove;
float raw_joystick_[4] = {};
float raw_ml_[6] = {};
float raw_midi_[8] = {};
// Constant used to pad the unused NN input dims; recomputed only on input-mode change.
float unusedInputDefault_ = 0.5f;
static constexpr const char* kInputSourceFile = "/input_source.bin";
void assembleInputs();
void copyAndZero(const float* src, size_t n);
void saveInputSource();
void loadInputSource();
void saveCCNumbers();
void loadCCNumbers();
size_t analysisParamsOffset = 0;
MEMORY_STORE_MODES memoryStoreMode = MEMORY_STORE_MODES::REPLACE_10_PERCENT;
std::array<String, 6> memOptions = {"Add", "Replace 5%", "Replace 10%", "Replace 15%", "Reward Decay 10%", "Reward Decay 20%"};
// Dislike repulsion: how far a 'no' moves the disliked action's training target away
// from the liked region (untapered), and the negative-batch LR base. Bigger = a 'no'
// slides the sound clearly further away; >1 tends to push params to the [0,1] rails.
static constexpr float kGeometricPushScale = 1.0f;
static constexpr float kNegLRBase = 1.5f; // negLRRatio = kNegLRBase - 0.4*negFraction
// A 'no' pushes at full strength for this long (wall-clock), then expires — no decay.
// The number of optimise cycles within the window depends on the mode's NN update rate.
static constexpr uint32_t kDislikeLifetimeMs = 2500;
static constexpr size_t kCentroidK = 4;
std::vector<bool> activeDims_;
bool removeItemsAtDistance(std::vector<float> &experienceState, const float distThreshold, const float reward);
void decayItemsAtDistance(std::vector<float> &experienceState, const float distThreshold);
std::vector<size_t> layers_nodes;
const bool use_constant_weight_init = false;
const float constant_weight_init = 0;
SynthMLP synthMapping; // value member -> lives in the (static) mode object: zero heap
float learningRate = 1e-3;
float learningRateScaled = learningRate;
std::vector<float> action;
ReplayMemory<trainStatelessRLItem> replayMem;
static constexpr size_t memoryLimit = 64;
static constexpr size_t batchSize = 8;
std::vector<float> mappingOutput;
std::vector<float> controlInput;
std::vector<float> savedAction;
float rewardScale = 1.0f;
// OrnsteinUhlenbeckNoise ou_noise;
std::vector<std::unique_ptr<OrnsteinUhlenbeckNoise>> ou_noises;
// Exploration-noise travel speed.
static constexpr float kNoiseDt = 0.004f; // normal OU travel speed (set in setup)
// Jolt = permanent weight modulation while B2 is held (see startJolt/stepJolt).
static constexpr size_t kJoltNumWeights = 40; // random weights perturbed per press
static constexpr float kJoltMorphRate = 0.017f; // EMA per tick (~1s to target @200Hz)
static constexpr float kJoltWeightMin = -1.2f; // target range == weight-init range
static constexpr float kJoltWeightMax = 0.9f;
static constexpr float kJoltTargetEpsilon = 0.05f; // re-roll target once within this
static constexpr float kJoltLRRampStep = 0.001f; // LR recovery rate: 1/(5s * 200Hz)
std::vector<size_t> joltWeightLoc_; // global flat weight indices (StaticMLP::WeightPtrAt)
std::vector<float> joltTarget_; // per-selected-weight target value
bool joltActive_ = false;
// After a jolt releases, learning resumes gently: effective LR *= joltLRRamp_, which
// climbs 0 -> 1 over ~5s so fresh training doesn't immediately drag the net off the
// jolted sound. 1.0 = normal (full LR).
float joltLRRamp_ = 1.0f;
bool resetMinMaxFlag = false;
// Deferred actions: set from ISR, consumed in main-loop loopCallback before optimise()
volatile bool pendingLike_{false};
volatile bool pendingDislike_{false};
volatile bool pendingDragStore_{false}; // drag-release: store savedAction
volatile bool pendingInputSourceChange_{false}; // input-source change: deferred from rotary ISR
INPUT_SOURCE pendingInputSource_{INPUT_SOURCE::JOYSTICK_3D};
// Debounced flash persistence: a save is scheduled this many ms after the last change,
// so scrolling through sources doesn't trigger a flash write (XIP stall) per detent.
static constexpr uint32_t kInputSourceSaveDelayMs = 600;
uint32_t inputSourceSaveDueMs_ = 0; // 0 = no save pending
spin_lock_t *mlpActive;
String _modeRoot{"mlp_rl"};
String _modeTag{"Unknown"};
ExtraSaveDataFn _extraSaveFn;
ExtraLoadDataFn _extraLoadFn;
};
// Template method definitions (header-only so the per-mode instantiation is
// available wherever InterfaceRL<N> is used).
#include "InterfaceRL.tpp"
#endif // INTERFACERL_HPP

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,19 @@
#ifndef INTERFACE_RL_FILE_FORMAT_HPP
#define INTERFACE_RL_FILE_FORMAT_HPP
#include <stdint.h>
static constexpr uint16_t MEML_FILE_FORMAT_VERSION = 1;
// Binary file layout:
// MEMLFileHeader (26 bytes)
// extra_size bytes of mode-specific data (may be 0)
// MLP binary (existing format)
struct MEMLFileHeader {
char magic[4]; // always "MEML"
uint16_t format_version; // MEML_FILE_FORMAT_VERSION
char mode_tag[16]; // null-padded mode identifier, e.g. "VerbFX"
uint16_t extra_size; // bytes of mode-specific data immediately following
};
#endif // INTERFACE_RL_FILE_FORMAT_HPP

View file

@ -0,0 +1,33 @@
# `reference/` — upstream source of truth, never compiled
These four files are `examples/InterfaceRL.{hpp,cpp,tpp}` +
`examples/InterfaceRLFileFormat.hpp`, copied verbatim from
[`MusicallyEmbodiedML/memllib`](https://github.com/MusicallyEmbodiedML/memllib) at
commit `e291192d8e4f2fca7b79670c4df9c2ec8bdf03cd` — the same commit the rest of this
vendored tree pins (see `../VENDORED.md`).
## Why they are here
`InterfaceRL` is the reference implementation of the whole NISPS feedback subsystem.
`nisps/ml/geo_push.hpp`, `nisps/ml/replay.hpp`, `nisps/ml/feedback.hpp`,
`nisps/ml/jolt.hpp` and `nisps/ml/ou_noise.hpp` are all ports of it, and several of them
still carry `// upstream InterfaceRL.hpp:NNN` line references.
The Phase-4 vendoring dropped `examples/` because nothing compiled it. That was correct
for the build and wrong for the codebase: with the source of truth out of tree, upstream
redesigned the geometric dislike (deleted the `/(1+len)` taper, doubled
`kGeometricPushScale`, tripled the negative-LR base, moved to batch training over all
negatives every tick) and we did not notice for months. Keeping these files in-tree turns
the next upstream drift into a `diff` instead of an archaeology session.
## Rules
- **Never compiled.** This directory sits OUTSIDE `../src/`, and PlatformIO's Library
Dependency Finder only recursively compiles an Arduino-format library's `src/` folder
(`../VENDORED.md` explains that mechanism at length). Do not move these under `src/`
and do not add them to any build.
- **Never edited.** They are upstream's bytes. Our behaviour lives in `nisps/ml/`. A
divergence from upstream is a decision recorded in `ALIGNMENT.md` or a task, not an
edit here.
- **Re-sync with the rest of the tree**, in the same step and to the same commit —
`../VENDORED.md` § "Re-syncing with upstream".