From 22efb1c4112b7116fb8e49108d5106d1862f5ec1 Mon Sep 17 00:00:00 2001 From: monkey-w1n5t0n Date: Sun, 28 Jun 2026 04:14:12 +0200 Subject: [PATCH] feat(nisps/ml): crystallise Explore-and-place into shared FeedbackController Add FeedbackMode::ExploreAndPlace + Idle/Exploring/Placing state machine (no-heap, deterministic nisps::Rng): enter/exit_explore, reroll, nudge, undo, begin_place, commit_place, cancel_place + on_down/on_up browser policy. Wire the nisps_ml_feedback_* C API + EXPORTED_FUNCTIONS, extend parity Stage 5d. Fix set_mode(3) falling back to Avoid. Native ctest 4/4; parity native==WASM within 1e-5 (max delta 2.4e-7). Rebuilt nisps.{js,wasm}. --- nisps/CMakeLists.txt | 1 + nisps/ml/feedback.hpp | 461 +++++++++++++++++++++ nisps/wasm/bindings.cpp | 225 ++++++++++- playground/public/nisps.js | 21 +- playground/public/nisps.wasm | Bin 95864 -> 105285 bytes scripts/build-wasm.sh | 11 +- tests/cpp/parity_check.cpp | 81 +++- tests/cpp/parity_diff.mjs | 15 +- tests/cpp/parity_wasm.mjs | 82 +++- tests/cpp/test_mlp_feedback.cpp | 685 ++++++++++++++++++++++++++++++++ 10 files changed, 1573 insertions(+), 9 deletions(-) create mode 100644 nisps/ml/feedback.hpp create mode 100644 tests/cpp/test_mlp_feedback.cpp diff --git a/nisps/CMakeLists.txt b/nisps/CMakeLists.txt index 4bb4e14..8ff80d2 100644 --- a/nisps/CMakeLists.txt +++ b/nisps/CMakeLists.txt @@ -56,6 +56,7 @@ if(NOT EMSCRIPTEN) ${NISPS_TEST_DIR}/test_mlp_training.cpp ${NISPS_TEST_DIR}/test_mlp_loss.cpp ${NISPS_TEST_DIR}/test_mlp_rl.cpp + ${NISPS_TEST_DIR}/test_mlp_feedback.cpp ${NISPS_TEST_DIR}/test_mlp_serialize.cpp ) target_link_libraries(nisps_core_tests PRIVATE nisps_core) diff --git a/nisps/ml/feedback.hpp b/nisps/ml/feedback.hpp new file mode 100644 index 0000000..259d792 --- /dev/null +++ b/nisps/ml/feedback.hpp @@ -0,0 +1,461 @@ +// nisps/ml/feedback.hpp — the "Down Action" negative-feedback controller. +// +// Ported from the firmware InterfaceRL FEEDBACK_MODE state machine (upstream +// 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). +// +// Design: header-only class template over the concrete MLP type. The controller +// does NOT own the MLP — every mutating method takes `MLP_T&`. It owns only the +// exploration state, all fixed-size (no heap), with its OWN per-instance Rng so +// re-rolling outputs is deterministic and never perturbs the MLP's RNG stream. +// Honours the RP2350 perf contract: no heap, no virtual dispatch, deterministic +// per-instance RNG. +// +// The C++/JS boundary: the controller decides *what transition happened* +// (returns a FeedbackAction); the caller decides *what to persist* (add example, +// grow noise, train). All inherently-UI state (pins, pipeline outputs, display) +// stays in the caller. + +#pragma once + +#include +#include +#include +#include + +#include "../core/rng.hpp" + +namespace nisps::ml { + +enum class FeedbackMode : std::uint8_t { + Avoid = 0, // down → move_weights (Gaussian perturb). No internal state. + 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). +}; + +// The explicit lifecycle state for FeedbackMode::ExploreAndPlace. The whole +// mode is a three-state machine; granular methods drive the transitions +// (firmware maps buttons to them directly), while on_down/on_up implement the +// SOFTWARE default policy (browser) on top of the same machine. +// Idle — the real (trained) net is live; no scratchpad. +// Exploring — real net snapshotted aside; a random SCRATCHPAD net is live and +// the user auditions it (reroll / nudge / undo). NEVER trained. +// Placing — the user liked the current scratchpad sound; its output vector +// is FROZEN in placed_out_ and held while they choose WHERE to +// place it. The caller drives inference at the chosen input but +// the audition stays the frozen vector. +enum class ExploreState : std::uint8_t { + Idle = 0, + Exploring = 1, + Placing = 2, +}; + +// What a press resolved to. The caller (JS runtime / firmware glue) performs the +// replay-memory / training side effect; the controller owns the in-core state +// machine and the weight snapshot/restore. +enum class FeedbackAction : std::uint8_t { + None = 0, + AvoidPerturb = 1, // move_weights already applied; caller grows exploration noise. + LikeStore = 2, // caller: add +1 example at (input, output) + train. + EnterExplore = 3, // entered a RANDOMISE_* exploration (UI: show "exploring"). + Reroll = 4, // re-rolled within a RandomiseOutputs exploration. + CommitStore = 5, // caller: add +1 example at (input, captured output); explore ended. + Cancel = 6, // exploration discarded; net restored (RandomiseMlp). + Restore = 7, // exploration kept via drag; net restored (caller already stored). + // ---- ExploreAndPlace (append-only; never renumber the TS↔C++ contract) ---- + ScratchReroll = 8, // scratchpad re-randomised (Exploring); pure audition, no store. + ScratchNudge = 9, // scratchpad nudged (bounded perturb, Exploring); undoable. + ScratchUndo = 10, // last reroll/nudge undone (Exploring). + BeginPlace = 11, // Exploring→Placing; placed_out_ captured + frozen (no store yet). + CommitPlace = 12, // Placing→Idle; real net restored. CALLER adds +1 (input→placed_output) + trains. + CancelPlace = 13, // Placing→Exploring; backed out of placing (no store). +}; + +// UndoDepth = number of scratchpad ops (reroll/nudge) that can be undone in +// ExploreAndPlace. The undo ring is a fixed std::array of weight snapshots +// (no heap); each slot is kWeights floats. WASM uses depth 4, firmware 2 (per +// rl-feedback-design §2.2 — SRAM budget). Default 4 (the WASM depth). +template +class FeedbackController { + public: + static constexpr std::size_t kNOut = MLP_T::kOutput; + static constexpr std::size_t kWeights = MLP_T::weight_count(); + static constexpr std::size_t kUndoDepth = UndoDepth; + + explicit FeedbackController(std::uint64_t seed) noexcept : rng_(seed) {} + + // ---- mode --------------------------------------------------------------- + // Switching mode mid-exploration cleanly tears down: restores the net (in + // RandomiseMlp) and resumes learning, so we never strand a randomised net. + void set_mode(FeedbackMode m, MLP_T& mlp) noexcept { + if (explore_active_) abort_explore(mlp); + if (ep_state_ != ExploreState::Idle) abort_explore_place(mlp); + mode_ = m; + } + FeedbackMode mode() const noexcept { return mode_; } + + // `exploring()` is true whenever a scratchpad net is live and learning is + // paused — for the legacy RANDOMISE_* modes, AND for ExploreAndPlace in + // either Exploring or Placing (the real net stays snapshotted aside the + // whole time). The TS/firmware UI uses it to show the "exploring" state. + bool exploring() const noexcept { + return explore_active_ || ep_state_ != ExploreState::Idle; + } + bool learning_paused() const noexcept { return learning_paused_; } + + // ---- ExploreAndPlace state introspection -------------------------------- + ExploreState explore_state() const noexcept { return ep_state_; } + bool placing() const noexcept { return ep_state_ == ExploreState::Placing; } + // Depth of the scratchpad undo ring currently available to pop (0..UndoDepth). + std::size_t undo_depth() const noexcept { return undo_count_; } + // The output vector frozen at like()/begin-place time. Valid only while + // placing(); empty span otherwise. The caller adds this as the +1 example + // label at commit (input → placed_output). + std::span placed_output() const noexcept { + if (ep_state_ != ExploreState::Placing) return {}; + return std::span(placed_out_.data(), kNOut); + } + + // ---- focus mask: 1 byte per output; 0 == frozen (unfocused). Copied into a + // fixed buffer (no heap, no dangling span). Empty ⇒ all outputs active. + void set_focus_mask(std::span mask) noexcept { + focus_count_ = (mask.size() < kNOut) ? mask.size() : kNOut; + for (std::size_t i = 0; i < focus_count_; ++i) focus_[i] = mask[i]; + } + void clear_focus_mask() noexcept { focus_count_ = 0; } + + // ---- press handlers ----------------------------------------------------- + // `current_out` is the live (post-pipeline) output the user is hearing + // (kNOut floats). `pin_mask` may be empty. Returns the FeedbackAction the + // caller must act on. + FeedbackAction on_down(MLP_T& mlp, std::span current_out, + float speed, float spread, + std::span pin_mask) noexcept { + switch (mode_) { + case FeedbackMode::Avoid: + mlp.move_weights(speed, spread, pin_mask); + return FeedbackAction::AvoidPerturb; + case FeedbackMode::RandomiseOutputs: + if (!explore_active_) { + enter_randomise_outputs(current_out); + return FeedbackAction::EnterExplore; + } + roll_static_outputs(); + return FeedbackAction::Reroll; + case FeedbackMode::RandomiseMlp: + if (!explore_active_) { + enter_randomise_mlp(mlp, spread); + return FeedbackAction::EnterExplore; + } + cancel_explore(mlp); + return FeedbackAction::Cancel; + case FeedbackMode::ExploreAndPlace: + // SOFTWARE DEFAULT POLICY (browser): down enters explore from + // Idle, else re-rolls the scratchpad. (Firmware maps its own + // buttons to the granular methods instead.) + switch (ep_state_) { + case ExploreState::Idle: + enter_explore(mlp, spread); + return FeedbackAction::EnterExplore; + case ExploreState::Exploring: + reroll(mlp, spread); + return FeedbackAction::ScratchReroll; + case ExploreState::Placing: + // Down while placing backs out to Exploring. + cancel_place(); + return FeedbackAction::CancelPlace; + } + return FeedbackAction::None; + } + return FeedbackAction::None; + } + + // Up = thumbs-up / "keep". While exploring it commits: the CALLER must have + // captured the heard output BEFORE calling this (on_up restores the original + // net in RandomiseMlp), then stores it as a +1 example at the current input. + FeedbackAction on_up(MLP_T& mlp) noexcept { + if (mode_ == FeedbackMode::ExploreAndPlace) { + // SOFTWARE DEFAULT POLICY (browser): up begins place from + // Exploring (freeze the heard output), then commits from Placing + // (restore the real net; caller stores +1 (input→placed_output)). + switch (ep_state_) { + case ExploreState::Idle: + return FeedbackAction::LikeStore; // not exploring → plain like + case ExploreState::Exploring: + begin_place(mlp); + return FeedbackAction::BeginPlace; + case ExploreState::Placing: + commit_place(mlp); + return FeedbackAction::CommitPlace; + } + return FeedbackAction::None; + } + if (explore_active_ && (mode_ == FeedbackMode::RandomiseOutputs || + mode_ == FeedbackMode::RandomiseMlp)) { + restore_after_explore(mlp); + return FeedbackAction::CommitStore; + } + return FeedbackAction::LikeStore; + } + + // 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. + FeedbackAction on_drag(MLP_T& mlp) noexcept { + if (explore_active_ && mode_ == FeedbackMode::RandomiseMlp) { + restore_after_explore(mlp); + return FeedbackAction::Restore; + } + return FeedbackAction::LikeStore; + } + + // Inference hook: fills `out` with the held static vector and returns true + // when RandomiseOutputs is bypassing the MLP; else returns false (the caller + // should run mlp.process() normally). `out` should hold at least kNOut. + bool static_output(std::span out) const noexcept { + // ExploreAndPlace: while PLACING, the audition is the frozen vector the + // user liked, held steady as they aim at a location. + if (mode_ == FeedbackMode::ExploreAndPlace && ep_state_ == ExploreState::Placing) { + const std::size_t n = (out.size() < kNOut) ? out.size() : kNOut; + for (std::size_t i = 0; i < n; ++i) out[i] = placed_out_[i]; + return true; + } + if (!(mode_ == FeedbackMode::RandomiseOutputs && explore_active_)) return false; + const std::size_t n = (out.size() < kNOut) ? out.size() : kNOut; + for (std::size_t i = 0; i < n; ++i) out[i] = static_out_[i]; + return true; + } + + void seed(std::uint64_t s) noexcept { rng_.seed(s); } + + // ========================================================================= + // ExploreAndPlace — granular lifecycle methods (firmware maps buttons to + // these directly; on_down/on_up call them for the browser default policy). + // + // CONTRACT: the controller owns the WEIGHT snapshot/restore and all scratch + // state; the CALLER owns example-storage + training. On commit_place the + // controller restores the real net and the caller does add_example(current + // input → placed_output()) + train (warm-start to interpolate all anchors). + // ========================================================================= + + // Idle→Exploring. Snapshot the real (trained) net aside, randomise a + // scratchpad net the user auditions. No-op if not Idle. + void enter_explore(MLP_T& mlp, float spread) noexcept { + if (mode_ != FeedbackMode::ExploreAndPlace) return; + if (ep_state_ != ExploreState::Idle) return; + auto w = mlp.get_weights(); // flat snapshot (size == kWeights) + for (std::size_t i = 0; i < kWeights; ++i) snapshot_[i] = w[i]; + learning_paused_ = true; + ep_state_ = ExploreState::Exploring; + undo_count_ = 0u; + undo_head_ = 0u; + mlp.draw_weights(spread); // first scratchpad candidate + } + + // Exploring→Idle. Restore the real net, discard the scratchpad. No example + // stored. (The hardware "enter/exit explore toggle" off-path.) + void exit_explore(MLP_T& mlp) noexcept { + if (mode_ != FeedbackMode::ExploreAndPlace) return; + if (ep_state_ == ExploreState::Idle) return; + restore_real_net(mlp); + } + + // Exploring scratchpad op: re-randomise the scratchpad. Undoable. + void reroll(MLP_T& mlp, float spread) noexcept { + if (!can_scratch_op()) return; + push_undo(mlp); + mlp.draw_weights(spread); + } + + // Exploring scratchpad op: small bounded perturbation of the scratchpad via + // move_weights on the controller's OWN Rng-free path — move_weights uses the + // MLP's Rng, so to keep the controller's Rng stream out of the MLP stream we + // draw the perturbation here and apply it. Undoable. `amount` is the noise + // stddev (small, e.g. 0.05). + void nudge(MLP_T& mlp, float amount) noexcept { + if (!can_scratch_op()) return; + push_undo(mlp); + auto w = mlp.get_weights(); + for (std::size_t i = 0; i < kWeights; ++i) { + scratch_buf_[i] = w[i] + rng_.next_float_gaussian(amount); + } + mlp.set_weights(std::span(scratch_buf_.data(), kWeights)); + } + + // Exploring scratchpad op: undo the last reroll/nudge (bounded ring). + void undo(MLP_T& mlp) noexcept { + if (!can_scratch_op()) return; + if (undo_count_ == 0u) return; + undo_head_ = (undo_head_ + kUndoDepth - 1u) % kUndoDepth; + --undo_count_; + mlp.set_weights(std::span(undo_ring_[undo_head_].data(), kWeights)); + } + + // Exploring→Placing. Capture + FREEZE the current scratchpad output the user + // is auditioning. The caller MUST have run mlp.process() at the audition + // input first; pass that output here. While placing, static_output() holds + // this vector and the caller chooses WHERE to place it. + void begin_place(MLP_T& mlp, std::span current_out) noexcept { + if (mode_ != FeedbackMode::ExploreAndPlace) return; + if (ep_state_ != ExploreState::Exploring) return; + const std::size_t n = (current_out.size() < kNOut) ? current_out.size() : kNOut; + for (std::size_t i = 0; i < n; ++i) placed_out_[i] = current_out[i]; + ep_state_ = ExploreState::Placing; + } + + // Convenience: freeze the scratchpad's output at its CURRENT input (runs the + // forward pass on the live scratchpad net). Equivalent to process()+capture. + void begin_place(MLP_T& mlp) noexcept { + if (mode_ != FeedbackMode::ExploreAndPlace) return; + if (ep_state_ != ExploreState::Exploring) return; + mlp.process(); + const auto outs = mlp.outputs(); + const std::size_t n = (outs.size() < kNOut) ? outs.size() : kNOut; + for (std::size_t i = 0; i < n; ++i) placed_out_[i] = outs[i]; + ep_state_ = ExploreState::Placing; + } + + // Placing→Idle. Restore the real net. The CALLER then adds a +1 example at + // (chosen input → placed_output()) and trains. Returns the placed output so + // the caller can read it after the restore (it survives the restore). + void commit_place(MLP_T& mlp) noexcept { + if (mode_ != FeedbackMode::ExploreAndPlace) return; + if (ep_state_ != ExploreState::Placing) return; + // Restore the real net but KEEP placed_out_ valid for the caller until + // it transitions to Idle; expose via a separate accessor that does not + // gate on Placing. + mlp.set_weights(std::span(snapshot_.data(), kWeights)); + last_placed_valid_ = true; // placed_out_ holds the just-committed vector + learning_paused_ = false; + ep_state_ = ExploreState::Idle; + undo_count_ = 0u; + } + + // The output vector committed by the most recent commit_place, valid until + // the next enter_explore/begin_place. Lets the caller add the +1 example + // AFTER commit_place has restored the real net. + std::span committed_output() const noexcept { + if (!last_placed_valid_) return {}; + return std::span(placed_out_.data(), kNOut); + } + + // Placing→Exploring. Back out of placing without storing; resume auditioning + // the scratchpad (which is still live — begin_place did not touch weights). + void cancel_place() noexcept { + if (mode_ != FeedbackMode::ExploreAndPlace) return; + if (ep_state_ != ExploreState::Placing) return; + ep_state_ = ExploreState::Exploring; + } + + private: + void enter_randomise_outputs(std::span seed_out) noexcept { + explore_active_ = true; + learning_paused_ = true; + // Seed every dim with the live output the user is hearing, so unfocused + // (frozen) dims hold that value through the exploration — matching the + // firmware `staticRandomOut_ = action; _roll_static_outputs();`. The + // CALLER CONTRACT is to pass the full kNOut live output. Any dims beyond + // a short seed keep their previous static value (we have no live value + // to freeze them to); they are only observable if a focus mask freezes + // a dim the short seed did not cover — an out-of-contract corner. + const std::size_t n = (seed_out.size() < kNOut) ? seed_out.size() : kNOut; + for (std::size_t i = 0; i < n; ++i) static_out_[i] = seed_out[i]; + roll_static_outputs(); + } + + void roll_static_outputs() noexcept { + for (std::size_t i = 0; i < kNOut; ++i) { + const bool active = (focus_count_ == 0u) || (i < focus_count_ && focus_[i] != 0u); + if (active) static_out_[i] = rng_.next_float_uniform(); // [0, 1) + // inactive dims keep their seeded entry value + } + } + + void enter_randomise_mlp(MLP_T& mlp, float spread) noexcept { + explore_active_ = true; + learning_paused_ = true; + auto w = mlp.get_weights(); // flat snapshot (size == kWeights) + for (std::size_t i = 0; i < kWeights; ++i) snapshot_[i] = w[i]; + mlp.draw_weights(spread); // randomise the live net + } + + void restore_after_explore(MLP_T& mlp) noexcept { + if (mode_ == FeedbackMode::RandomiseMlp) { + mlp.set_weights(std::span(snapshot_.data(), kWeights)); + } + learning_paused_ = false; + explore_active_ = false; + } + + // Cancel and abort share restore semantics; the caller stores nothing. + void cancel_explore(MLP_T& mlp) noexcept { restore_after_explore(mlp); } + void abort_explore(MLP_T& mlp) noexcept { restore_after_explore(mlp); } + + // ---- ExploreAndPlace helpers -------------------------------------------- + bool can_scratch_op() const noexcept { + return mode_ == FeedbackMode::ExploreAndPlace && + ep_state_ == ExploreState::Exploring; + } + + // Push the CURRENT scratchpad weights onto the bounded undo ring before a + // mutating op, so undo() restores the pre-op candidate. + void push_undo(MLP_T& mlp) noexcept { + auto w = mlp.get_weights(); + for (std::size_t i = 0; i < kWeights; ++i) undo_ring_[undo_head_][i] = w[i]; + undo_head_ = (undo_head_ + 1u) % kUndoDepth; + if (undo_count_ < kUndoDepth) ++undo_count_; + } + + // Restore the set-aside real net and return to Idle. Shared by exit_explore + // and abort_explore_place. No example stored. + void restore_real_net(MLP_T& mlp) noexcept { + mlp.set_weights(std::span(snapshot_.data(), kWeights)); + learning_paused_ = false; + ep_state_ = ExploreState::Idle; + undo_count_ = 0u; + last_placed_valid_ = false; + } + + void abort_explore_place(MLP_T& mlp) noexcept { + if (ep_state_ != ExploreState::Idle) restore_real_net(mlp); + } + + FeedbackMode mode_ = FeedbackMode::Avoid; + bool explore_active_ = false; + bool learning_paused_ = false; + std::array static_out_{}; + std::array snapshot_{}; + std::array focus_{}; + std::size_t focus_count_ = 0; // 0 ⇒ all active + + // ---- ExploreAndPlace state (all fixed-size, no heap) -------------------- + ExploreState ep_state_ = ExploreState::Idle; + std::array placed_out_{}; // frozen audition vector + bool last_placed_valid_ = false; + std::array, kUndoDepth> undo_ring_{}; // bounded undo + std::size_t undo_head_ = 0u; // next write slot + std::size_t undo_count_ = 0u; // valid entries (0..kUndoDepth) + std::array scratch_buf_{}; // nudge scratch (no heap) + + Rng rng_; +}; + +} // namespace nisps::ml diff --git a/nisps/wasm/bindings.cpp b/nisps/wasm/bindings.cpp index 85897ea..0404e68 100644 --- a/nisps/wasm/bindings.cpp +++ b/nisps/wasm/bindings.cpp @@ -65,6 +65,7 @@ // ML. #include "../core/types.hpp" +#include "../ml/feedback.hpp" #include "../ml/mlp.hpp" #include "../ml/stats.hpp" @@ -93,11 +94,17 @@ constexpr std::size_t kDefaultOutputs = DefaultMLP::kOutput; // the opaque pointer to JS. struct MLHandle { DefaultMLP mlp; + // "Down Action" negative-feedback controller (Avoid/RandomiseOutputs/ + // RandomiseMlp). Seeded off the MLP seed XOR a salt so its static-output + // RNG stream is independent of the MLP's inference/move RNG. + nisps::ml::FeedbackController feedback; // Buffers used to bridge JS → C++: std::array input_scratch{}; std::array output_scratch{}; // Stats buffer fed back to JS via get_layer_stats. std::array stats_scratch{}; + // Static-output buffer for the RandomiseOutputs bypass path. + std::array feedback_static_scratch{}; // Used by infer_batch with arbitrary N — must exceed any reasonable // request from the heatmap. 256x256 = 65536 max points → too many in // practice. We cap batch size at 4096 here; callers must split larger @@ -105,7 +112,8 @@ struct MLHandle { static constexpr std::size_t kMaxBatch = 4096u; std::array batch_out_scratch{}; - explicit MLHandle(std::uint64_t seed) noexcept : mlp(seed) {} + explicit MLHandle(std::uint64_t seed) noexcept + : mlp(seed), feedback(seed ^ 0xFEEDBACC0DEull) {} }; // --------------------------------------------------------------------------- @@ -427,6 +435,221 @@ void nisps_ml_move_weights(void* ml, float speed, float spread, h->mlp.move_weights(speed, spread, mask); } +// --------------------------------------------------------------------------- +// ML feedback — the "Down Action" state machine (Avoid / RandomiseOutputs / +// RandomiseMlp). The controller decides WHAT transition happened (returns a +// FeedbackAction int); JS performs the side effect (store example, grow noise, +// train). See nisps/ml/feedback.hpp. Mode ints: 0=Avoid 1=RandOut 2=RandMlp. +// Action ints mirror nisps::ml::FeedbackAction. +// +// CALLER CONTRACT (commit ordering — important): +// On a "keep" (up) or drag-commit while exploring RandomiseMlp, the controller +// RESTORES the original net before returning. The output the user is hearing +// comes from the *temporary* (randomised) net, so you MUST capture the current +// output (nisps_ml_outputs / nisps_ml_feedback_static_output) BEFORE calling +// nisps_ml_feedback_up / _drag, then store THAT captured vector as the +1 +// example. Reading the output AFTER the call yields the restored (wrong) net. +// nisps_ml_feedback_down with current_out should pass the full kDefaultOutputs +// live vector (RandomiseOutputs freezes unfocused dims at those values). +// --------------------------------------------------------------------------- + +EMSCRIPTEN_KEEPALIVE +void nisps_ml_feedback_set_mode(void* ml, int mode) { + if (!ml) return; + auto* h = static_cast(ml); + nisps::ml::FeedbackMode m = nisps::ml::FeedbackMode::Avoid; + if (mode == 1) m = nisps::ml::FeedbackMode::RandomiseOutputs; + else if (mode == 2) m = nisps::ml::FeedbackMode::RandomiseMlp; + else if (mode == 3) m = nisps::ml::FeedbackMode::ExploreAndPlace; + h->feedback.set_mode(m, h->mlp); +} + +EMSCRIPTEN_KEEPALIVE +int nisps_ml_feedback_get_mode(void* ml) { + if (!ml) return 0; + return static_cast(static_cast(ml)->feedback.mode()); +} + +EMSCRIPTEN_KEEPALIVE +int nisps_ml_feedback_exploring(void* ml) { + if (!ml) return 0; + return static_cast(ml)->feedback.exploring() ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE +int nisps_ml_feedback_learning_paused(void* ml) { + if (!ml) return 0; + return static_cast(ml)->feedback.learning_paused() ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE +void nisps_ml_feedback_set_focus(void* ml, const uint8_t* mask, int n) { + if (!ml) return; + auto* h = static_cast(ml); + if (!mask || n <= 0) { + h->feedback.clear_focus_mask(); + return; + } + h->feedback.set_focus_mask( + std::span(mask, static_cast(n))); +} + +// current_out = kDefaultOutputs floats the user is hearing (may be null). +// pin_mask may be null. Returns the FeedbackAction int. +EMSCRIPTEN_KEEPALIVE +int nisps_ml_feedback_down(void* ml, const float* current_out, + float speed, float spread, const uint8_t* pin_mask) { + if (!ml) return 0; + auto* h = static_cast(ml); + std::span out; + if (current_out) out = std::span(current_out, kDefaultOutputs); + std::span mask; + if (pin_mask) mask = std::span(pin_mask, kDefaultOutputs); + return static_cast(h->feedback.on_down(h->mlp, out, speed, spread, mask)); +} + +EMSCRIPTEN_KEEPALIVE +int nisps_ml_feedback_up(void* ml) { + if (!ml) return 0; + auto* h = static_cast(ml); + return static_cast(h->feedback.on_up(h->mlp)); +} + +EMSCRIPTEN_KEEPALIVE +int nisps_ml_feedback_drag(void* ml) { + if (!ml) return 0; + auto* h = static_cast(ml); + return static_cast(h->feedback.on_drag(h->mlp)); +} + +// If returns 1, `out` (kDefaultOutputs floats) holds the static bypass vector +// and the caller should NOT call nisps_ml_process(); if 0, run process(). +EMSCRIPTEN_KEEPALIVE +int nisps_ml_feedback_static_output(void* ml, float* out) { + if (!ml || !out) return 0; + auto* h = static_cast(ml); + const bool bypass = + h->feedback.static_output(std::span(h->feedback_static_scratch)); + if (bypass) { + std::memcpy(out, h->feedback_static_scratch.data(), + kDefaultOutputs * sizeof(float)); + } + return bypass ? 1 : 0; +} + +// --------------------------------------------------------------------------- +// ML feedback — ExploreAndPlace lifecycle (Idle → Exploring → Placing → Idle). +// Granular transitions so the SAME shared core drives both the browser (which +// also uses on_down/on_up via _down/_up) and firmware (which maps buttons to +// these directly). Set mode 3 (ExploreAndPlace) via nisps_ml_feedback_set_mode. +// +// CALLER CONTRACT (commit ordering): on _commit_place the controller restores +// the REAL net; the caller then reads nisps_ml_feedback_committed_output and +// adds it as the +1 example label at the chosen input, then trains. +// --------------------------------------------------------------------------- + +// Idle→Exploring: snapshot the real net, randomise a scratchpad. +EMSCRIPTEN_KEEPALIVE +void nisps_ml_feedback_enter_explore(void* ml, float spread) { + if (!ml) return; + auto* h = static_cast(ml); + h->feedback.enter_explore(h->mlp, spread); +} + +// Exploring→Idle: restore the real net, discard scratchpad. +EMSCRIPTEN_KEEPALIVE +void nisps_ml_feedback_exit_explore(void* ml) { + if (!ml) return; + auto* h = static_cast(ml); + h->feedback.exit_explore(h->mlp); +} + +// Exploring scratchpad op: re-randomise. +EMSCRIPTEN_KEEPALIVE +void nisps_ml_feedback_reroll(void* ml, float spread) { + if (!ml) return; + auto* h = static_cast(ml); + h->feedback.reroll(h->mlp, spread); +} + +// Exploring scratchpad op: bounded nudge (amount = noise stddev, e.g. 0.05). +EMSCRIPTEN_KEEPALIVE +void nisps_ml_feedback_nudge(void* ml, float amount) { + if (!ml) return; + auto* h = static_cast(ml); + h->feedback.nudge(h->mlp, amount); +} + +// Exploring scratchpad op: undo last reroll/nudge. +EMSCRIPTEN_KEEPALIVE +void nisps_ml_feedback_undo(void* ml) { + if (!ml) return; + auto* h = static_cast(ml); + h->feedback.undo(h->mlp); +} + +// Exploring→Placing: freeze the scratchpad output at its CURRENT input. +EMSCRIPTEN_KEEPALIVE +void nisps_ml_feedback_like(void* ml) { + if (!ml) return; + auto* h = static_cast(ml); + h->feedback.begin_place(h->mlp); +} + +// Placing→Idle: restore the real net. Caller then reads committed_output and +// stores the +1 example at the chosen input. +EMSCRIPTEN_KEEPALIVE +void nisps_ml_feedback_commit_place(void* ml) { + if (!ml) return; + auto* h = static_cast(ml); + h->feedback.commit_place(h->mlp); +} + +// Placing→Exploring: back out of placing (no store). +EMSCRIPTEN_KEEPALIVE +void nisps_ml_feedback_cancel_place(void* ml) { + if (!ml) return; + auto* h = static_cast(ml); + h->feedback.cancel_place(); +} + +// 1 if currently Placing (audition is the frozen vector), else 0. +EMSCRIPTEN_KEEPALIVE +int nisps_ml_feedback_placing(void* ml) { + if (!ml) return 0; + return static_cast(ml)->feedback.placing() ? 1 : 0; +} + +// ExploreState int: 0=Idle 1=Exploring 2=Placing. +EMSCRIPTEN_KEEPALIVE +int nisps_ml_feedback_state(void* ml) { + if (!ml) return 0; + return static_cast(static_cast(ml)->feedback.explore_state()); +} + +// Scratchpad undo-ring depth currently available to pop. +EMSCRIPTEN_KEEPALIVE +int nisps_ml_feedback_undo_depth(void* ml) { + if (!ml) return 0; + return static_cast(static_cast(ml)->feedback.undo_depth()); +} + +// Writes the committed/placed output vector (kDefaultOutputs floats) into `out`. +// Returns 1 if a vector was written (placing OR a fresh commit), else 0. Reads +// committed_output() (valid post-commit) falling back to placed_output() (while +// placing) so the caller can grab the label either before or after commit. +EMSCRIPTEN_KEEPALIVE +int nisps_ml_feedback_placed_output(void* ml, float* out) { + if (!ml || !out) return 0; + auto* h = static_cast(ml); + std::span v = h->feedback.committed_output(); + if (v.empty()) v = h->feedback.placed_output(); + if (v.empty()) return 0; + const std::size_t n = (v.size() < kDefaultOutputs) ? v.size() : kDefaultOutputs; + std::memcpy(out, v.data(), n * sizeof(float)); + return 1; +} + EMSCRIPTEN_KEEPALIVE void nisps_ml_get_layer_stats(void* ml, float* out_stats) { if (!ml || !out_stats) return; diff --git a/playground/public/nisps.js b/playground/public/nisps.js index 3efe02b..9c76fe9 100644 --- a/playground/public/nisps.js +++ b/playground/public/nisps.js @@ -1,2 +1,19 @@ -var createNispsModule=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";if(ENVIRONMENT_IS_WORKER){_scriptName=self.location.href}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;class EmscriptenEH{}class EmscriptenSjLj extends EmscriptenEH{}var readyPromiseResolve,readyPromiseReject;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["d"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what=`Aborted(${what})`;err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("nisps.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP16;var HEAP32;var HEAP64;var HEAP8;var HEAPF32;var HEAPF64;var HEAPU16;var HEAPU32;var HEAPU64;var HEAPU8;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var __abort_js=()=>abort("");var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["ccall"]=ccall;Module["cwrap"]=cwrap;var _nisps_ml_create,_nisps_ml_destroy,_nisps_ml_set_input,_nisps_ml_process,_nisps_ml_outputs,_nisps_ml_infer_batch,_nisps_ml_add_example,_nisps_ml_train,_nisps_ml_eval_loss,_nisps_ml_weight_count,_nisps_ml_get_weights,_nisps_ml_set_weights,_nisps_ml_draw_weights,_nisps_ml_move_weights,_nisps_ml_get_layer_stats,_nisps_ml_example_count,_nisps_ml_clear_examples,_nisps_ml_reset,_nisps_ml_describe,_nisps_engine_create,_nisps_engine_destroy,_nisps_engine_set_params,_nisps_engine_process_block,_malloc,_free,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_nisps_ml_create=Module["_nisps_ml_create"]=wasmExports["e"];_nisps_ml_destroy=Module["_nisps_ml_destroy"]=wasmExports["f"];_nisps_ml_set_input=Module["_nisps_ml_set_input"]=wasmExports["g"];_nisps_ml_process=Module["_nisps_ml_process"]=wasmExports["h"];_nisps_ml_outputs=Module["_nisps_ml_outputs"]=wasmExports["i"];_nisps_ml_infer_batch=Module["_nisps_ml_infer_batch"]=wasmExports["j"];_nisps_ml_add_example=Module["_nisps_ml_add_example"]=wasmExports["k"];_nisps_ml_train=Module["_nisps_ml_train"]=wasmExports["l"];_nisps_ml_eval_loss=Module["_nisps_ml_eval_loss"]=wasmExports["m"];_nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=wasmExports["n"];_nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=wasmExports["o"];_nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=wasmExports["p"];_nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=wasmExports["q"];_nisps_ml_move_weights=Module["_nisps_ml_move_weights"]=wasmExports["r"];_nisps_ml_get_layer_stats=Module["_nisps_ml_get_layer_stats"]=wasmExports["s"];_nisps_ml_example_count=Module["_nisps_ml_example_count"]=wasmExports["t"];_nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=wasmExports["u"];_nisps_ml_reset=Module["_nisps_ml_reset"]=wasmExports["v"];_nisps_ml_describe=Module["_nisps_ml_describe"]=wasmExports["w"];_nisps_engine_create=Module["_nisps_engine_create"]=wasmExports["x"];_nisps_engine_destroy=Module["_nisps_engine_destroy"]=wasmExports["y"];_nisps_engine_set_params=Module["_nisps_engine_set_params"]=wasmExports["z"];_nisps_engine_process_block=Module["_nisps_engine_process_block"]=wasmExports["A"];_malloc=Module["_malloc"]=wasmExports["B"];_free=Module["_free"]=wasmExports["C"];__emscripten_stack_restore=wasmExports["D"];__emscripten_stack_alloc=wasmExports["E"];_emscripten_stack_get_current=wasmExports["F"];memory=wasmMemory=wasmExports["c"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={a:__abort_js,b:_emscripten_resize_heap};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} -;return moduleRtn}})();if(typeof exports==="object"&&typeof module==="object"){module.exports=createNispsModule;module.exports.default=createNispsModule}else if(typeof define==="function"&&define["amd"])define([],()=>createNispsModule); + +var createNispsModule = (() => { + var _scriptName = typeof document != 'undefined' ? document.currentScript?.src : undefined; + + return ( +function(moduleArg = {}) { + var moduleRtn; + +var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&process.type!="renderer";var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=url=>fetch(url,{credentials:"same-origin"}).then(response=>{if(response.ok){return response.arrayBuffer()}return Promise.reject(new Error(response.status+" : "+response.url))})}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var wasmBinary=Module["wasmBinary"];var wasmMemory;var ABORT=false;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b)}var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;function preRun(){var preRuns=Module["preRun"];if(preRuns){if(typeof preRuns=="function")preRuns=[preRuns];preRuns.forEach(addOnPreRun)}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;callRuntimeCallbacks(__ATINIT__)}function postRun(){var postRuns=Module["postRun"];if(postRuns){if(typeof postRuns=="function")postRuns=[postRuns];postRuns.forEach(addOnPostRun)}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var dataURIPrefix="data:application/octet-stream;base64,";var isDataURI=filename=>filename.startsWith(dataURIPrefix);function findWasmBinary(){var f="nisps.wasm";if(!isDataURI(f)){return locateFile(f)}return f}var wasmBinaryFile;function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}function getBinaryPromise(binaryFile){if(!wasmBinary){return readAsync(binaryFile).then(response=>new Uint8Array(response),()=>getBinarySync(binaryFile))}return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}return instantiateArrayBuffer(binaryFile,imports,callback)}function getWasmImports(){return{a:wasmImports}}function createWasm(){var info=getWasmImports();function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["d"];updateMemoryViews();addOnInit(wasmExports["e"]);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);readyPromiseReject(e)}}wasmBinaryFile??=findWasmBinary();instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult).catch(readyPromiseReject);return{}}var callRuntimeCallbacks=callbacks=>{callbacks.forEach(f=>f(Module))};var noExitRuntime=Module["noExitRuntime"]||true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var __abort_js=()=>{abort("")};var __emscripten_memcpy_js=(dest,src,num)=>HEAPU8.copyWithin(dest,src,src+num);var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder:undefined;var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead=NaN)=>{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};var wasmImports={a:__abort_js,c:__emscripten_memcpy_js,b:_emscripten_resize_heap};var wasmExports=createWasm();var ___wasm_call_ctors=()=>(___wasm_call_ctors=wasmExports["e"])();var _nisps_ml_create=Module["_nisps_ml_create"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_create=Module["_nisps_ml_create"]=wasmExports["f"])(a0,a1,a2,a3,a4);var _nisps_ml_destroy=Module["_nisps_ml_destroy"]=a0=>(_nisps_ml_destroy=Module["_nisps_ml_destroy"]=wasmExports["g"])(a0);var _nisps_ml_set_input=Module["_nisps_ml_set_input"]=(a0,a1,a2)=>(_nisps_ml_set_input=Module["_nisps_ml_set_input"]=wasmExports["h"])(a0,a1,a2);var _nisps_ml_process=Module["_nisps_ml_process"]=a0=>(_nisps_ml_process=Module["_nisps_ml_process"]=wasmExports["i"])(a0);var _nisps_ml_outputs=Module["_nisps_ml_outputs"]=a0=>(_nisps_ml_outputs=Module["_nisps_ml_outputs"]=wasmExports["j"])(a0);var _nisps_ml_infer_batch=Module["_nisps_ml_infer_batch"]=(a0,a1,a2,a3)=>(_nisps_ml_infer_batch=Module["_nisps_ml_infer_batch"]=wasmExports["k"])(a0,a1,a2,a3);var _nisps_ml_add_example=Module["_nisps_ml_add_example"]=(a0,a1,a2)=>(_nisps_ml_add_example=Module["_nisps_ml_add_example"]=wasmExports["l"])(a0,a1,a2);var _nisps_ml_train=Module["_nisps_ml_train"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_train=Module["_nisps_ml_train"]=wasmExports["m"])(a0,a1,a2,a3,a4);var _nisps_ml_eval_loss=Module["_nisps_ml_eval_loss"]=a0=>(_nisps_ml_eval_loss=Module["_nisps_ml_eval_loss"]=wasmExports["n"])(a0);var _nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=a0=>(_nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=wasmExports["o"])(a0);var _nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=(a0,a1)=>(_nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=wasmExports["p"])(a0,a1);var _nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=(a0,a1)=>(_nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=wasmExports["q"])(a0,a1);var _nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=(a0,a1)=>(_nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=wasmExports["r"])(a0,a1);var _nisps_ml_move_weights=Module["_nisps_ml_move_weights"]=(a0,a1,a2,a3)=>(_nisps_ml_move_weights=Module["_nisps_ml_move_weights"]=wasmExports["s"])(a0,a1,a2,a3);var _nisps_ml_feedback_set_mode=Module["_nisps_ml_feedback_set_mode"]=(a0,a1)=>(_nisps_ml_feedback_set_mode=Module["_nisps_ml_feedback_set_mode"]=wasmExports["t"])(a0,a1);var _nisps_ml_feedback_get_mode=Module["_nisps_ml_feedback_get_mode"]=a0=>(_nisps_ml_feedback_get_mode=Module["_nisps_ml_feedback_get_mode"]=wasmExports["u"])(a0);var _nisps_ml_feedback_exploring=Module["_nisps_ml_feedback_exploring"]=a0=>(_nisps_ml_feedback_exploring=Module["_nisps_ml_feedback_exploring"]=wasmExports["v"])(a0);var _nisps_ml_feedback_learning_paused=Module["_nisps_ml_feedback_learning_paused"]=a0=>(_nisps_ml_feedback_learning_paused=Module["_nisps_ml_feedback_learning_paused"]=wasmExports["w"])(a0);var _nisps_ml_feedback_set_focus=Module["_nisps_ml_feedback_set_focus"]=(a0,a1,a2)=>(_nisps_ml_feedback_set_focus=Module["_nisps_ml_feedback_set_focus"]=wasmExports["x"])(a0,a1,a2);var _nisps_ml_feedback_down=Module["_nisps_ml_feedback_down"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_feedback_down=Module["_nisps_ml_feedback_down"]=wasmExports["y"])(a0,a1,a2,a3,a4);var _nisps_ml_feedback_up=Module["_nisps_ml_feedback_up"]=a0=>(_nisps_ml_feedback_up=Module["_nisps_ml_feedback_up"]=wasmExports["z"])(a0);var _nisps_ml_feedback_drag=Module["_nisps_ml_feedback_drag"]=a0=>(_nisps_ml_feedback_drag=Module["_nisps_ml_feedback_drag"]=wasmExports["A"])(a0);var _nisps_ml_feedback_static_output=Module["_nisps_ml_feedback_static_output"]=(a0,a1)=>(_nisps_ml_feedback_static_output=Module["_nisps_ml_feedback_static_output"]=wasmExports["B"])(a0,a1);var _nisps_ml_feedback_enter_explore=Module["_nisps_ml_feedback_enter_explore"]=(a0,a1)=>(_nisps_ml_feedback_enter_explore=Module["_nisps_ml_feedback_enter_explore"]=wasmExports["C"])(a0,a1);var _nisps_ml_feedback_exit_explore=Module["_nisps_ml_feedback_exit_explore"]=a0=>(_nisps_ml_feedback_exit_explore=Module["_nisps_ml_feedback_exit_explore"]=wasmExports["D"])(a0);var _nisps_ml_feedback_reroll=Module["_nisps_ml_feedback_reroll"]=(a0,a1)=>(_nisps_ml_feedback_reroll=Module["_nisps_ml_feedback_reroll"]=wasmExports["E"])(a0,a1);var _nisps_ml_feedback_nudge=Module["_nisps_ml_feedback_nudge"]=(a0,a1)=>(_nisps_ml_feedback_nudge=Module["_nisps_ml_feedback_nudge"]=wasmExports["F"])(a0,a1);var _nisps_ml_feedback_undo=Module["_nisps_ml_feedback_undo"]=a0=>(_nisps_ml_feedback_undo=Module["_nisps_ml_feedback_undo"]=wasmExports["G"])(a0);var _nisps_ml_feedback_like=Module["_nisps_ml_feedback_like"]=a0=>(_nisps_ml_feedback_like=Module["_nisps_ml_feedback_like"]=wasmExports["H"])(a0);var _nisps_ml_feedback_commit_place=Module["_nisps_ml_feedback_commit_place"]=a0=>(_nisps_ml_feedback_commit_place=Module["_nisps_ml_feedback_commit_place"]=wasmExports["I"])(a0);var _nisps_ml_feedback_cancel_place=Module["_nisps_ml_feedback_cancel_place"]=a0=>(_nisps_ml_feedback_cancel_place=Module["_nisps_ml_feedback_cancel_place"]=wasmExports["J"])(a0);var _nisps_ml_feedback_placing=Module["_nisps_ml_feedback_placing"]=a0=>(_nisps_ml_feedback_placing=Module["_nisps_ml_feedback_placing"]=wasmExports["K"])(a0);var _nisps_ml_feedback_state=Module["_nisps_ml_feedback_state"]=a0=>(_nisps_ml_feedback_state=Module["_nisps_ml_feedback_state"]=wasmExports["L"])(a0);var _nisps_ml_feedback_undo_depth=Module["_nisps_ml_feedback_undo_depth"]=a0=>(_nisps_ml_feedback_undo_depth=Module["_nisps_ml_feedback_undo_depth"]=wasmExports["M"])(a0);var _nisps_ml_feedback_placed_output=Module["_nisps_ml_feedback_placed_output"]=(a0,a1)=>(_nisps_ml_feedback_placed_output=Module["_nisps_ml_feedback_placed_output"]=wasmExports["N"])(a0,a1);var _nisps_ml_get_layer_stats=Module["_nisps_ml_get_layer_stats"]=(a0,a1)=>(_nisps_ml_get_layer_stats=Module["_nisps_ml_get_layer_stats"]=wasmExports["O"])(a0,a1);var _nisps_ml_example_count=Module["_nisps_ml_example_count"]=a0=>(_nisps_ml_example_count=Module["_nisps_ml_example_count"]=wasmExports["P"])(a0);var _nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=a0=>(_nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=wasmExports["Q"])(a0);var _nisps_ml_reset=Module["_nisps_ml_reset"]=a0=>(_nisps_ml_reset=Module["_nisps_ml_reset"]=wasmExports["R"])(a0);var _nisps_ml_describe=Module["_nisps_ml_describe"]=a0=>(_nisps_ml_describe=Module["_nisps_ml_describe"]=wasmExports["S"])(a0);var _nisps_engine_create=Module["_nisps_engine_create"]=(a0,a1)=>(_nisps_engine_create=Module["_nisps_engine_create"]=wasmExports["T"])(a0,a1);var _nisps_engine_destroy=Module["_nisps_engine_destroy"]=a0=>(_nisps_engine_destroy=Module["_nisps_engine_destroy"]=wasmExports["U"])(a0);var _nisps_engine_set_params=Module["_nisps_engine_set_params"]=(a0,a1,a2)=>(_nisps_engine_set_params=Module["_nisps_engine_set_params"]=wasmExports["V"])(a0,a1,a2);var _nisps_engine_process_block=Module["_nisps_engine_process_block"]=(a0,a1,a2,a3,a4,a5)=>(_nisps_engine_process_block=Module["_nisps_engine_process_block"]=wasmExports["W"])(a0,a1,a2,a3,a4,a5);var _malloc=Module["_malloc"]=a0=>(_malloc=Module["_malloc"]=wasmExports["Y"])(a0);var _free=Module["_free"]=a0=>(_free=Module["_free"]=wasmExports["Z"])(a0);var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["_"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["$"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["aa"])();Module["ccall"]=ccall;Module["cwrap"]=cwrap;var calledRun;var calledPrerun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(){if(runDependencies>0){return}if(!calledPrerun){calledPrerun=1;preRun();if(runDependencies>0){return}}function doRun(){if(calledRun)return;calledRun=1;Module["calledRun"]=1;if(ABORT)return;initRuntime();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();moduleRtn=readyPromise; + + + return moduleRtn; +} +); +})(); +if (typeof exports === 'object' && typeof module === 'object') + module.exports = createNispsModule; +else if (typeof define === 'function' && define['amd']) + define([], () => createNispsModule); diff --git a/playground/public/nisps.wasm b/playground/public/nisps.wasm index 1eecd696149741a8513c5e28ccc39c78688be0a9..d2226c2075364b0a39d6b372d04540c1a9eec6fb 100755 GIT binary patch literal 105285 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 MBhBp^dadXG0L?6Q`Tzg` literal 95864 zcmeFa51dukb>Dmb+&lNq+!^MA1~d{#I`;@!BZO>ZVQK7OF$b`O66EN8p4_L;i>#1D zb?KnALMAfp2h3PT2`M}W4MU3j_`FOgui<0kh8NSAK4WW~kUZRnQ_@FT?C@!6OZ(`1 zYCiN6`hpW;z3*@BbIv{Ya%X&{sA=L>d^CICeb!og?Y-Atd+mQ~dq45XpY}b^^AGvM z8~vUB&W*v&ogOi7W4O~|JNz9RJyNz420M3n8-pE6s_xuj|Ls)r_U!NWjpd#C;PG7% zBN6Nn-SW;I`~$i~BIG^N$O6zFB`68|0ldMe}B^R!qtzK zs)5&ADtUc9q3`=)82UXWzt$fPc%@3&^XGXbe=w}i_q-rja7!2r1wr5i{=$mydy8%@ zm&*Q&pq2SK;ZIBiJ-uX?>^t@iuU7e6cltjSc>eR=YX7f#kNThR9`^s5_kh32TkijL zZ<#;t-Rp1m-s8XE-Q)kX_hJ8Uc&q%s>HU!ZqW2+xi}ykQC2!RKr1yxw)%$?|Gu}%7 zW$z*XQ{MaiZQg_a-||-Yf7^SXpLp-}*LaKlwQ#x4yWjtacc1^5H`4F-_I>Ujmmi%7 z1AlwC^KLJxZ;ne#y-TFU{_d~*htHPu-TP|8=l8$)yT3nSzr(~^PqM#m_>Bv{r`&(7h)$!yr z{C%QP&U2w*tPyThHVb0+HnCS8@KvSdj|}*=wVpSzx>0pabt8OiAfROa*0}T$VkObr z_5>eEF*@6x-0~4Cpg%dNriAA83P_OgyvUFIDXW<5%M+hmY+$YYxf+_XT&=;Yd!uR; z*5kx~Y;mJn6N!%w1Wv$2dSZ1{Jun*dN5Qh7-v}BS7;)g(@%G15%z?og9F-DJ97C#v zV5VZ(2*fTD8ve)tkmC18rm~MJ#zk*4Fu_R353f_HJ@4up({az?>4Vc|7L!5#(Nr`d)96Fz2Bt(KR)(+X{=c{ z-c^<5#i+D6t04P5MK$#}{$_r1=C6Kaz$>8a6G*)cr4C9m)33r)gyf`RanCeG>G|bf zR{72-#g*@Ha?SzfRj`^ORK6DXMx`%pU-Q17je372xob-jZoMx&*|+_T!@sU(t=3-Y z_p3XlFW@KglIrF-Bq(hj2|TKK+m_fHm+(?8{uUsDB-nzGMqv~Rq6BzyFR7zAD zZ+K4BlcSa#g^wd#0QWb1r1vdfS_b@)pc)5{4+P27QD6T+yfBY*?2N6)M^O5s>kYV<@w?DHMz zBOl-?h|hQT8-xM6a!=wZU-iO~phVeG2I1oaVRGoGAL&z6ru3hNxe}%K68|N|%JxYe zpoO!bmUs;`YlT{+@?dL8TuK7E>_~9>3zX+c3t9az!~zM#Spua@E>Y2oY$*s6Ia`OVBbR1wnH{;RLO0 zf(DQYS`d}rp`fAt%G7bYK;`8`5}gcHoeYsW?>qU}xI%=^H)};mccN&t#6q zix{ZsVyGN5QD|9=3827-KMq1;FG#KbHu22H%2g40N zsfs}_xeY4*%bOYM{N%337jK1^60+=ckUG8-TaD*_a>vV3KJWvSM)lH`@wi7aRWe;9 z;&l;^f(GV_N*=~}MZ}glW3N>PV3ZO*yc(L0zBC?{RBvhJSflia*Aw(F{+E97H^*MG zGx8RbUaPJ0DX=P>4^J*`_~95z$FDDrJ=$>BF=4ot59XI;Fj86+L;X&)rdRv7CBL*o zxXXa*i^@}qEt!nVQTcQG*B-jy^HN35Ut~5qCwmK^Ff!M@1rhr#u^(15!c{Kl) zWZ&MHtkF4XuLBK3T1^>CQ8mSzio*jmnm8ukp|}ba&-*cynl}hVPmPF^OOm zrs@QJQ8*R*qiAu-RcR{jz2W^@TpKm{>Wymq8NMmuGL%Cv{GR`1wpve-|kGNFq`6-{irF9-<;z3vK_N2PU6-3oZauYOmfuuvPn)px5*-JM1f5u zO4EsN==pS`H=Azs&NN{PS>$sY=26Nbo8YX=rZickfZmoxaYmD~QOJVA-8`iU-IRtl zWJ)8wvPFEO<+Az7-}eLNCT8gEiJ71T$?tvr*RF?}djy(N{CtV1nilPKfRG+ZrwQC- zMCm95+M&M0;*$sVyDk+Z-~Bs#t{EBkSsf$=$sl03^n^?grO!;&m&E?w0uaUvKQ%32 z_E!5?z`Xj?!B}$jUTGcwOOf}bHI?mauA|iN`t0^NSVQEk!Dqzxuf*PJX1rUU)MU1f z1yxJ-q2g9Z*X?99xO%U%+x*%#KiuvtZ)OJcH2xiqgTp4i1f|0$lwjSr7XRHh{?Si< zcJ<+PAHDYGjW2zC)9UHNB>&Dsd-wg}9j}x8z5nrN@A&4^U&OFJj6Mq1z4S}3kN)X` zZ}8=pzB#<|$&dY>5WMN>*!z2A;V=I6+K>AFc9&b4sSm)qzy8!8Tw1gLyN@1Tcki8p zSAK5hue<@Czi!Qk%Kz%W|JeV!S|Fc!uK!Qpdh1`laX4d}Os9Xn_Al=VOFNhD2-FWI zP~s&fpUl9##=OX17A$S_QIkZXxH1|nb(QQz^JFZd#ZVi3M>$XJXI9X=XYGzV_Pyuh zOj07H4mD^A=`+l0>9c{Tl0wK9kmoKe zXb5&j0}?Oq$D@jc&)KAyk?Uy4M4#{&bLesDWZlJarxn3)TOR?BwXQQBaeh5E*3o!MlLq| zNI2|b^+&>bGA|iO`jcAHm-Hspq>}U`<%D@pfN!#F-cYw3HX@gsN6I{-2RQ@{S zOQ)ZeV2p|%S8i`~aMRE90Cw9AX; zRC82@ssdBn)rd5eb+O^Ls`!heS~?WF)JX>Fa}`^q9d?BtYPSE9wnG1x(VZO%Xlohm z?K(7j0ad&uu6V=E;ze!6`_DfNbSO$sr?z6XtFwrzAdxG`l4e1cwiV>v9lJU(UFpO$ z3l5HHnNTuLVjH8a?ni^8nhwcMVWBo=W#cIE+QXPGYa0Xq^psYplA{5(OB67H*J3V1 zeq5HdFdw65Li1}GQ(iP*Cj9g_Cf9zZ@92kE)n}uEj02JFNylLgrY_O4%Tt+E8fYLU z^3-aY8n&eNr>UiuR93N#Lb)Y%C{68YNgYm8D=n!@(o}X&3Oqzf)Bv z7t+)vEvYZ2sdu!bZcS6~Y)Rcls-$@|*dCQ6)l3)6n4i?oQrv4{HLh9MANN~Wj|VIq ziU%zmj_VdKiEptmiia#*8ZWePBwl3U^7vK@SH!~>u8bF0I2zw(;i~v{3&-Nc7Osnz zShzmE!@>>mofbY7-(}&`ab)2$al^uA#|$R5VL}cMVL)hv@oIt7Is~|+XL)q`^8Qz ziw-}x&26Bw92~Ov=T@Qu2u-|E+~@NSXb&kxd`w$qqG4OM-bZa#N`^KSHb9eb?Rx^` zHawGwLGAc4bdxsMcIU)EzB<5m=l#+BhvVgklih}W|KVhxBJ2T@b}&uT^3d)92)ixL zUvE^9r<(TImIWum#=^7)Ri@R{%6C3_S2DTwcP73i8MMC07=wIR5+vCF5?09h97CKlJDdb8E`0=z7k z@EZ#hZf~&OzAV_*Ky2Gq4EAZ3Pl5c#EiSX;e%uGEd1ecu!Kvu>sIIk&+q6h&%OXdW zHyF!;W6GNXH}gJjc^@(Mmj%;)gDrz)!6EU|*OvPU<&HF;^^L8oA@ZirSvmnllxE0oQ=J)^-{ z)<0Z}4MYp~6N{3mcwRKm(wImqLZSb?qV;H?>GFos?~kV^Q!%QQ#rd# z)G=M&tWk^xZ}L5v2*$OJmGqBm1x^e!D&ZK_(UUBB0TWHb+Av6-$I`fdAP@Yl+amif zu&92J)qE04+SKqo*0spQrMtW)(_B^CZi&1es4Cd56W<+l`+r!z;0>^|3DLFjyq3 zrQRsT2=$;Ui+UdLj`t~WFmyPYOq{^Nln@fH~(~q;HPYWi07(U4+)eSn8la$(IIjHRDy-7kY(75y~ zHZZ7`^TDhVa^6-0uM{QZvXc-D?n|kJ@L3XaC6y2WQVF^2B;;Zc59Shb&T>AhoT&%I zFe*q$5B-zx$t5I&l1W19;h5DI8wB;JFBw5Vdd=Fg)JbiN!abdac4aS!UP9M!*BG-!|$HNwEcfRf)sw_-v&78nY(M&UI8J&yK3TT(9Kc zOD#Dpu4xH*gz9Bv?|s0#r!mj=@A_eP9$Ob|kPfH=2O7Rd0@pkOT#8IZ^IxLUs#|Fu zJG&+X9Y`?@>7bi24BXCZcoHtld5-q&jru?2-9skGAgGy44(hj)$zL#;yvPoIDwD4a z1p_-mWO5|95XSR!(KwG6P4+sP}?n*QUTy}3^K)^Tmb4$ z01R?(kvTSUBl^U$;2@iRC5gb=de1A5lE&_|()Ts#8e`e#)*KK3E8>wrbJ}I}Zf#sY z;2jAYgNS||wyEtcw5mu34J*?XpOSDyW7O$!>ual{p8Zp!frc6>_+0?#xYmtZ9!clY z#za&V&|cd<7hM}OG^QZpWUxDM++6aN5liu?9qJqXYSr*uJct51ro7$2M;!!aFiuS! zaQZqLWC%P2gdwX*JkOztqo~Jm5z<&PI9t)KIb-E&;5I_e1R6n>1s4Km^&Su0*3Pv+ z>SGzo#O=>qN~`WrXr*+=-OJcp*HO; zG|LS&i`&~I-5MntXlczifiSeb0-DB-NZJwWxGzPU*MfFB7cw(H7>YDHWD?LXIXofC z2&;%?!g^gmsdUu@B;DdiwCY*r5`Hvax@V9=3?@$ZTnMExvwzY%yMlS;k{@_>a@D$2 zN$2CN3#or72@NtzkSPrvljGov>o?NXH#l{2!x?Rjl4da}p0MBa_h;DpdDS;nud;w- zVoqV99e`=fTaE&~*2J{Bl-dD}4aHpoRKzm%I4Z+wx8^?;>sV{C&=^5pti9yEEI8tZ z{3H3Gn-@!cHY?T-5-oeAN(|70Y2BYGSq( z>sVGS%amg2H!~OOx{6&ey>qd|xe5I|@02(vLZ(pLqxPBoe0{pe0-+tAVLx7G9Z5$6 zL)y#XjSzS-Zt0i!rSET=w!ChCZ`j`n#J}DneUdC6M|b^$$3L^$%r=L3IYqpB6C!q? z4@uxG>tdD;mbgIRKrL$1dD5sZ{vUXQ1Ur`izDIMERkr!vH&DI#=FV3xWZ zXW9_YIz)z9O}7W4d7Nva(_jZ_ltyKPeF*+#?DR#oNxoTRiULhd(v`w&X2lq4Ho(CW zGVcyFfxFLD!BlCoP921kb&i^*XS=i|{lEWY(6e)J2W(fzH6|w$;kZ^-lG7Y@G&85v z=ncoP!L;NbmozQx)jW;KZ&cMfSKP22XL=g@I9 zRi&5-i{jcESKmDIN_Aep$K8igxu;U??W=M4&5zv1`^i@*f#RjLpZLTlRx?6Pd?roN zu;nos{MOkGA7%zVou&6@kh`)JvjLirN3!&x403mtVsqst$>DI=|r$Fub1YuEp)zpWBYWhpCKAx~uKD>Ir0vy{G^`HcNRL`q3H*dbEaCIG3eAL&~(JPb>X=mj0|D*Bbm< z(l2D`8wGFa(@MXXr9UtDbq2qV^h;U#Cc#_!w9+qU=`RTWBL@Ew(ywIcFACn$rfQtZn<9Q1P`%aRTOJN-RZqk=t{eVEQ+4`oS5fSveb*II&| z&ORK?VUJ`<$2d%SH2ZPfUwh=m9TM(iS<(r5rDP$Kt zJ%#L|r>Brz^z;<6i=LiBcG1&Q$S!(%3fVc+1J}X6gqp`mqKrE`%>ucZC?ugz3oe(!?%6us>i1dH&;oiY z16}u8Hf}d{K%Uf80(nwX3*<>ZIBKAEoc_x?;G-C38S!(mC@27+XBqQz=zob9@ZH%c*_`|v^`=T_0pFW}H&*}*yr~vi z!1ra~&1C=sZ>okC@PdKR!h2Iaw15|kgck6oif92Z7z!=mO*PR1-c%JW$xU_9lH61o zEy+JPYQq(47S++=V^e)}_*hUO9ljRSNQbWlRnp;WL7jB?T2Lt+z82I<`gIl~!+p&b zu$uZM#hS&~Xu&EdniOjmqoW0@pmADkj_yp3o=&=E4mGo(Rn26O9W;zVHZ@EJ*+ID& zWK+3hkR9}jK{oYD2H8QS7-UnWWRM-Si9t5CNe0i*`s;U$kL$tt;B_y4DkIcwOs=S?3_xG`=tenRNz| z!4~wvtcFJh`)AVv+DF63&{F=gy>hJdTBb>Co5xxvTdO4Hq`tFQ>7b96@@WT5xJ@=K zU;^_4y;qU$zWg_hlI}kJ7u&Dspg68(Hs-btADddG!^f^QO9$MpwMz%wKf8v>dto;! zCI{|D$K=3&HYH={18&;dxOo$^WyNkfYspTVm@SZVni?&TbDA10kaL zni?&TbDA10kWEt~GfJA4MwZevG_sVYosp$9&5TS$JMLs=A3JVmW*3r*27 zWuH@1v_Q_ODOw=s)D$g{b83nf$T>Ad3*?-dq6KnJP0<4R?rRE3eMc>!4;?jxVRSn2 zU>Kc_JQzl&GY^K*>Cl5=bUO857@dwiWNM=dR$1rws} z%dTumVXxozrC>U=eJLzn{3X={I+Fe04n754X@YmWJdut5e^#3zR|Iyz>0D;H>EM%7 z5G!?%>3o)M2cPUf)ZUv8J~{ZW-+b`N;IAtEQbtDypB()Bn-4x2{4%9q$>4SH$-%Ex z`kPt$R`s5ay}#{4v*)*cDD?WaFKs=(kV{*SFMMh1@r5sKJ-+beFMW?suL5Y5;gxFK zyFcoEjcXS8k>EO&U8Y*3+ZhjdGp`gcB_eCh)1jAsHPpE;PC{}skF8iaz=GbDUk&2{ zJ34#$SGmYAk0Wxw%DGy7LL{5$S8qZ`2XRLQbgroqyC(EOPWKx604G?@PZ!g!sb~?G zN27WB%>|Y_=ZZynfP8hH)cNJ=OOMt~9+=d%UD$$&I@hIoqrv@=PG&kpr0hW4C&Zd= zu5wPcfLI?DL&n!HljyLNdb49lMco^$qsLr*LnFxx&E?pNP=d|YM9f)f= zw{j(B3{8q#stg)%OX0y1^2K7N``R+)q65t7nsJ&b2nWT48Jd`w`{7fC~kM+!y4z&!0VaIEIRHzg*1y_)WRf zeRV&&Q1+8~?kAM{Ei9rHa=#_deZlOx!!cBpJL;@i_L-Yt@-{|Yo7bU195LIeh6tn z{J5136^@hRRe}@c_?VbK8R01c=Muek5BAZiMsMx4Mh!2gXo%CVxU0h>!Nud1hP#qw zUgyOBrQ_}n_wfo{ObHwibv=)h-*)cXAPhGSzZ~`Lk9GZM;xXM&cNdYkWJ{DA%E6cM zrI}nj9*%P@FyMX=E(*NNVN0YW=MOGDR9>_G=$y^q+EhK8dycM4yuvYgh9qcFdFFmb zq#jlYT%mq5FVvN*Hx=s5t5&G1S1V~3<0Pvq6gQbBr6ff2l3cr5 zC<*?=U6T}B2g*@DB|(`tOLBygz=28v1^c4*4Jip(M|L;iC<9gfHSx=uc&y89PJwf_ zHy(%v(DLbUfCqq~*D@yna*`+~vhJhZ*&cXpYVE_{y5)^Y3sxg)~BNm!nx}?Q5{Tb#&b6{PKm?U7^fUwFL>teK#q6mUtaA$4VCWV{w(h&@OnaW z?hAe6aU8`Oe)+>?spP~LNxEBiB)BBS-3#4BSyF(B9L$me%u>Nb1(>A{CMv*;2xe&kX2iiPEx;@n%t!%dxq}%gz^o9=@&e2X z2eZ5Yvr;fC3NR}j%!&fcs9;tWU`8Fx$^y(P!HgDQRyml_0?e3TRuy2z9L%Z$%sRo0 z6=2pmn6U!PdcmwKz^r#L>k2R%1hc*Xv%$fvFTgw{m<Ow1au7 z0P~Dso-V*V<6xdHz&tCMX9_USI+$k)FdGH)YyoDYgLyXcgB<}E^tiTlOykTLZeR5G zKpdV*hF(tI8r%}c$?+;~e63r%%WH4_7!CxuLlsU&adLg|N9~t=#}S!xj}zZAt;*@Ttg`_X~O|Is71vt#hCR}|=E(_e_7vWdqd!l>N_b=|5j^7jA^UJ>+-;=+8 z!N`e%ulL4TZx`8zsF#Ju_*BVsObju0`wGiQjvJkBIglZ;1uCxZBHbJ@XcC7ZIS zU1aSsT10mz8b9RX1W*J5UiN_#YM@ejIxul~feQhqC41GS4!RCThu5BOIPfxScGzxqwt9_=3Nb_hy>{iq6LL?^B8n+B4{+o>J~s4E359PN{n*=yjtLCM+N)b{z%TP zxDH5;+|-LgQs&FcfQ@W&&9e40r4Ov0VgO^%fg}W}%|jD`BL_-jzkMS~-$sI~|EQN* zX6UD}>S?BB^@o|`y|Hfb+ITh@<0^y{aLk&!6Boo_Iua*8*O!wRbjJxr;NWh?YRb$F zNAl#}D+w9?TOI_!a|BL*v&qA}lm|vbrj;<)V3U@FPS2tPeU^4t;8O zv+%tsdauw=Lzme%=!gP8GW1^xKnF{WKXVAr7|F54cvnw6y9l04<1L6bb^-wzFPNPc~M3!bS_}rCUS&X$y{lPq(;%naav~xCv@|3slz|)I9*pO&M0#EmNAMpMM z9>K7SxO8}|OQ`V~-`tAm^y~xPGr8xNem7B#F;bTWTg8f78FHOniQCgT;I>KmQQt<1 zNvx-6rud(4qM@4T;DUI8nX1A~)?+q-W?4-6O<;5-y-`4ia5HAm2l4&Dvd=*>L4xE4 zq>3|m*}kyvM)TELZISCmd0CgEpMI9XaDyh9!TXgj~8=wVjkpH`r{tbUS5;$9;{Ya{1;)=A%C2aoFb4 zZARHLvisSh)+O^^mwGrOyQ)e-ji^4kojytaz+X`AXaOt$L0}cXpP$mHmctHz}A1+H>Cpt~tQ5 zvR_MczuKwnyR|J!{g_`G#f}E@+;^;yJH@u#i)EiCPgnLMH|2gbxT%chGJ^pv^>QEal52#9rPIpHih4$w;i0Zs(ayWWOzJl0RNzc0+~(lxxviPc zxuW8=!8AYPL!d!f4isE=_`98A(NsDg4}vr^A}ucr{uqFgYg zoJ7j8Ex(>F^5f`oa};1WX&(h}vgJk?I*C{NQcqCY5ir82wCBEK(s24axa}~dclJAh zD_5XV9A^>+k(9;-eI97{d9vU&I2Y%wrfDod~Qe-{VxK`DIU_q-oT-vToCA<}Xq!34bNRW+&Fhd*AaTPsE{UhvLRYd$F46cF?l^^l3Kk}Dy zkXDcnALePCUwQ0@_Wsk&egFLKRXDy;u{=42o%S8FEkZbC8&w?g(+*jU(+T-ViagZJ*%m4s@-q&3 zLO`9c?X5qUwdE6+wRsD6*v~rbU9({yOY>gZ%sXn9dZWYMJsUQTeXY)xH?c>W*v~s` z%+Jo8<5JkLS2nR%G_f~1?0vIgpG>h=HL*vV*e^KjsoAixUoG!-P3*BI_KOZ1gTFK9 zGb#3lCieO!_Ev|D72g^AG#`>nm2o!vyyPe$&(|ead&XHn^O70d4lR=Ko%V64NH`-$ zi-f0St`nY=yGOzkJ~jrS+2Dj`gA-zd_bL5|>`}sLztNB5q@id5`X-(~j#K3Y<~9}C zUaF}AVI;eVa7j9!7?#z-ZyeiNXjU7)`>~LfZZ-YSEPvj+GX*p1i`M zrX7}B)U?BhMNQ)@wWw*F$fBmj@YoAo(_+IG9Wr6GsA)#31prgF2p&;Wwp2FJl3-~U zqMoAY!CRt5t&*9EsH?_XqM>o~Y0tW*RC=`LIsDg~Zg;wv8Q$)B=X4%DCC z`lE}J?+pq>Q`h7(+kO~hKBKiUbhJ*sh?T+DZ=ycj^PFjB29R9cWVEhsdUA0xIS2^- zOB__jzqDrM&%SZE_O)KWyo1u&+OJjHRK0@5%gdPZ{udsIWzK8KqETtucPWr$X9Kh0*QK#2I3AELke(KrJ8Z`2{K}>l*~4m zfd)OdNH*+5R*6F*CKFBwt}tRzeo}AZZFVBymiAI45lgex=8h|_qov|@O;Qr40uxYL zC`E~uRpb_e2^sZ{=CVqohEypiFk8g;HkxsjZY}5K5GnHRTqllCt*&Tsh}u>$ja&O7 zbKjD9`_~?jr%(pWnqQC>Qnii{@LM(K8c^$|szWP=%R}v(6L;)rWsiEkxr|J*gDLd5 z7O`GKZx@SKUi*iofB1$LyvD6*?KSQoET36|40?C^T!$ zCO;xts0zG2TfK#8Riq1aP;BOJ)swcA3ApN{y%6?63>4ed62(A!;p{`FYLhg)TlK~3 z48^`@MJ=0%s1l4v|z`xgcCN~V)gnJgK*oyQm5zT*pS*Kw2Eb9~#%J7KX?&^? z(J~r4Serqf#>SqrL1mBavGL267^-`0{U!U^zsFQ&_Os4IEDqIxhxXXp%GuXpUa}~B zU9!iHDrH}zJ$A?^`?++Fos`Lbj_iqb5GDJ$e2*PZ$$qZb6Au=?uG|yrcw+W-bdNoD zmHk|`CtgtaI<_a)_FndN-JW=;@N@m1SVLd-b;F)mCsOqD%Y@U(0oS8KbM5?b`n5bm ze|#JXPa{29Bz-YvoL$89lXD{tuyYJeq?Uc^&msd^WL_2-%p&zHGCzwf$Rf97k)bTI zFpDfoBRTsTu~_6{N0!A&mZ9YWm2*U#zKm*|QsTv+>=YZv+46&ILz5A2Bk?Q}&Bh}} zckR)WMtAPf14no4(Q`+%E{`8A-lM0GZr`Jak#5_gXOa*C9#6V;4^Jskfmc<54D`{h zym)dO53DR^m+B67uMY!B%99(Mp!FLTlnwL$qort#zBn|y)z=D3u}*+ z?G_LeQW4A^&|PX~#-Z4Z1Cwg)(kRl1qXwyM^4n-c0p4ljfVrB@SWpm0DmtyoB#Rk? z!b6%t5FXTwf^bSR48nbyaS-m+41{pEW+a5WG(#brsJM-k8$H@AW5&{>4Ieg`dz^Ru zt38}c^`cm!+P^mKX^s4s6Wj%8D+)!y5nqap1fzryVssC3jLJcj(KJXi3I>5juOQQ? z6T})V!ud>;$0V-?=@GE`s0zvD?aEGaIlJV^X)yn%68GmIX|Ba52LAk7d?;)26ARrr zm*WeWMbSdX7V69{)9!_|5aE%9sYEPHC1PPJ5erj^SeQ!0!c-y_rV_C*m57C@L@Z1t zVknh}p;RJxX<`Tn}< z&s}0n2hO3JQi-~&(J&p-pqr;UL_65uPlw#yL5JLJnx$Qb#L3lv%Snc)Ax+ZI4+&PL zw;If3rMC!6%9D`1wUxvvkyL-R>oGcbrpf}HX$q1n|K5lq9a!l)CCY#>z3?MxH@+LK zF*BfFGC!qJ+%L1~^qdr;W3Nipj`f7KPy0K{JEMTf2TevBGe-!%{F=SoLA11g5>p4t-@w>Xut?BeqbD45 zW?fX)K2{IY^9MNbOf--;dJw%vAB!43)y6UGSL6Pu?^O=vay#PHm~$O#=Kt)guX50v zD8FCjb_d(>YaaOdSL2%foyT8tu0;b5tLhL^OgO~*`K$Cs;j1%hiqEj3hSHS00;YI~ z$uz}rY%m-{@YQ%Q%~@oLD${uWt8rcV&3iRoK-wU@z?hyx7rDAXNVAYf(s^N!p}r;p zagA5c`8hADzxqh%VF?V`z5z=G2nVggRhI2*Mt1*0HO@m*@hzOKSCog82BP_$zo71; z!C5}(EW0Y2#;i8_b=r_$(a;OZ6sx*f`T9%)uxMfY3`fJ_hlQ89SaQv!(z1kTO*ac0 z)R8qO2d1qJ`-Efnc4*mIa2)GkSj8h3wa8ju%?!FVxwW<0soC0nv`gpKwzhWFpEgy$ zYnwh=qDDg?7(>xQ2}Z5HVL=#;nY!bwGMOoCdHyJ9Ip75uF~YMJRE%d8Bf1f9ta+*c%bDs(4!X*yX=|{y2}!4*U+`nAvu#^oSj1(9 zsI5jg%I?|enh_4Hqp{EyR7nZuRWzUc`TvR7UksMD#A1d56dA12J|n>}>V|c(5suA! z_5iZ#)6bd_rmOw99_IT{?3Yl>$%!ri->%8 zzd=A~*=R&UJiHhuJGFwIys=Xc|4Akhv~61&Jz4oHXy4?nMim*bA|uIfy`>FJ+VDLH zRQ2V*7JHjtHc3tVt(%iad9@*Zj)oS>t4)pMH6)!1K)y|{)xEqKy4B>a#}{vHR%KRe zn3g{ZQ`-ID*cfbhR^^`dgRL73nORQ@CwIJ@l(vjpbv(A1kEO-r!CpL6Bb8SO<(C`} zWu&^gl`5jVNOfh4+KX1n+qgmfSQCb0#YRMEo@kJn+R`;+SrMycYL!;;U5(db&$6^J zEKGY9wWq?A)zqxDDUvodvG%4$G)Z+$UACs?XWGTVyGE-6)Eib3_pGKPAi8qL&)x);fL7cRHz5}L{9|Da~w zNvPJr=zq|#4kzc?J;}7|U;#_xic_)bDFQ!s)Yt#glw=AwLM^hCtB;Hv`H8*vp(}%( z$(vskE@ZD+L^5~%i~gFOoKH-y5}BChjKM@nKLO>%Iv%!uaZ7UH3%~b;@+ZZ>#4$>s z$hj}3k%_1lqkXB%$ln|zGpMX52dc?g%ONh4*=ZMLLxP71Bb2C)1NJ^K&`WO+AuJh$ z6d4TCe_0uYeEy4m!=p`UMvoCn4Fom@8A@dQi2+Hx#KZ)PqbfQWy7MulmMuaR$*PLS z&9NW@hL4Ea+)Rz}lsI+1dCNF2L}5t6ahP$e>Pb1Kdr1kGpB#|MSb1VVz+^$4KR)31 z<8F*=ahugqlxm^{RWYV#dIRe&;)W_oR*Xj_@lN~WTr6z>aL~=0#Z0hyOB~dD9XMwj z;LKntASHTy0I5-(^FKx6-4+@FBeUMy2vq6@1cXXIOM@tQQoo>!F6iOV4nJ!uSXww0 z&06w|QH(rE8*yn?PYsjGSy*TTS3+rD${3d^rKPyqSE-JqvZ>02fyL4*q-lt;JvT48 zKn@!G)UKACoIs{`D&-^*C!qRuDHX7gDzj0Zj%VXM10drn4N2q-5l<$Emqao-th$^(*ycYX;y@5jM-GQv=w9EX7$RvSRb101dMq~3apLcJYHq5G4n z9+O0kB0)kC(U7b{%LnlNJrPhYJ{=iUWR z-XC8|s+y%eJWx1`qzWP-B)i7LX{ zA0s0trzFT!3es6mDyU3-Kz-TGe(dBNRUjjuW|JU_&afVxlR}Z|Ffz!dgHoFT#b?qn z-ZA7#W^?gVRz2ey^EJ$CKOBqfC=MM7^O}Y;csiTRZ+@FSt|1yNpej_BC9IpRx#yV>_Gzg16xdy4AK`I?J2vuwoyj+6>xdx%$gIt4{M%x;fO`S=D zFg`kUW&*1Ut)M~Blcqs}bYh~0_FVI8qHFV$iZqBO9%^?t(Ljf277{yK-NYc*AXN7- zJW4^jzywnY6nVDPREdU?9Dx#KRl#b8GjszC8@G#C>2r z+eAn+{Pj2gzAO ziR3Jtb)+&iqeeA3V=$+)Iyp;6D$Rr>XBBmGH=9?vxZ)tSpnjeeQ-xM$5Yt#?g+~Xf zDoRey3?;1*%{rZ&nKtZoat3ZHXEeIxOxhQ~o8_$L3G(+c<8E+0(euRlnka+9u%=yF{9OYr$xcplOO4zq{s>jt&m_7&_S{o2G z$0E<->adMOc5QvdKl#wq+D*HwUrXI#aJP4@$G$A1Tv=DK_Va6I3{-v{n8I}PnR$s4 z3G7Jhx1(C=6&fzQkh7~y3hs~33%$T+pb%rw(bJp!eZ!qu_q>U(e;it>27Q|}kQvpV zD+q_%`M}vEtI=DvD5^#X$G^@5)=xg@R!)pEKq|lBX#wzA8K?CAV~a&W89G!ZZeLd! zl0{FJb{KK4@mY40n$a?qFfJfRkDjtwcI}VKVb7<7ogqGRI55S1YKO;5(S4H*c)c&Y z^m;`$?S0_|B7q`Th#;Q#h3ANb*f3gg3$}95f;>va6?I&&q&o1TFa*8a~|ewpj4Dh+Z{h1=f$Ox*7EHI;S@Os7m9Ov8O?xH}Ce((pPYwfY^Wpf0aja?~_p3uJr*KsFMh#ih318#o>W^)4yl4=~Y7PUVwVmZ=%E{t8 zYUhjdUCl-1MVWGQVqOukLxZtD#?&N>-8mCs}(9$AU2`#B;kZJ?K$ZR+GVTmh)mi!X~nb_n@u1 z?MznC*^Xv@>PrgjJDudIo*W`*WiJFH!;86{kIR|OmuShqL$gCu{cv_D@{aZEBNwBsOuSPvH$7M1X zd92H8?bi)?%zt+s)9l=tlK%X%$_o)Hq<*J5XYEThzoP3otOb;Vv5fNo_G}PiVM2qQ zNKm=ydV!#{AefEg>1t8G6jo+V3a1*ESa8%NvN@5|dWV~GvFr&9^G1k;`GOM$hqKGE zwCwo=VAzIW&ZsRHTc4`OCEHJ+kTpA|5@>1_hnh2jZtyU#Wg+()1ZkplONNzD3@zT< z)@(R@oE2N`N?|riwu5v^rbcsXISe3tVpOu#3e6bI=wlT3v_GHDJmGQ)6qzhAHqwgREqOmPK*(Z=edSqphn57-`uTfAo-!XgZ~Zh1lr z>y+EJBa9|Bn=Mdups9Vxv~pQNK@(_>52Hmv;wNInk{e`a7o^slu=bf}Fqf*{i%uI7j6n=B+h=HAUb-E*+$}D?7x9b@w&8`ombv0+w z;wCnf=1X)Lb$k?y3lI#>%`kHeA(PDqA04Zw#G?&BKw*-%7CC}gmgyh zep=vS5OXU&C|H*M0WPH-CD?k6-2{s%QU=uARrv%a)Ftp(3hzt>j$rTJf}sn~(=G~}c9P)@SvI6adL zvjQw;p{q#(*)~ey7%4$csWB9eaX^RBY3n%GZc#?c6v^A1rW}rjra2&~9N^v=*_v~U zt0Aj$mOc3`#u*T5$@`+-&D^?MphIfxD7@ zO)jyJtl~jGW%I>H58nvB5--etenU*%py$G`t00UralcTkQVW}*2(z+j(#qfhWc^mo z_+%t!Rv$i;l8a^#Kfc@u;{LDva%0F1VQi;RcN^jbvgXvgwc=$a5?&Puqj^6=FmEeH z6fQ!ku&95$p_P~V&qy{Z$XtqrA5(D<0w=_u?tf5wGuMf}{kSmHVbP7vL zU7P0xzK5kjs^8EWY+7n=kEjf6MXRK>s9%lERmX(=WG}76tE;0S>ds1r`IDE9bSj=N zoU%$mJ?|$NT`2_(kFrGAsv8D82}auVvZdAjG8#%RiS?+;)j5iWfB{!A!A`+L_@?tY za0#G>J=g~>kor}>aD-VT+gyMnUu{=gz9#dM7bn;At?0`CE3Ui0%l&1&6~>f^toNa0 zn9d^c%mh671VZPywxZ>AXFVYfzLEhJdIA+8-O<~qNdBZrWgu9UIvVOPwPthCEOeI8 zDIQuv6=79oJv+tB+TkmQQ#G@h>de`E^su^{3KP1so}6$%dui_;pD8FgdJyYbdAm1; z#F{M=a|W>&8EC6msiC;<{_yy{%y#>vkTUfXRUWq<-O%`^a!q69INfMk2xxKhTNMKlS)1k2TnD6hzLSfj@An}&3Rlm19c6D!0w0QT6zD-@}t3;VVvW*0L zk;hc%cG9pAB7e)`TJ1<+&QnlJ>h+AjW$U*a5D{-JBc=CaZprX1Ms_x;KB^fMBb9wR zU}<#dKVf|_NItU@2wUjlB#65h3xHACFm;N(`756mjsNNY`y%tfGF*>qMQ|eeJgX2c z*Dc8({`~pxvQ7qhdC(=NY~^n%4)&i)CN^&QdK|9xHvS86(r;tjw3FpUDW{0IVC^6L z``36!`wL%Kwi-8sq^*6=|FVsQTYl&4qc~zFje(P^Y`D~eK_gdM`}_Ak{Q>(hx%S)t zPw6CJ+-s%Iz1qL1`u+}3bePsXerIteUffd59$A?943>^S6RW0C$vgEN=@Kag(x)$)6EO~uP^64yjLre0REcvOH z|0*4j->) zA1C|{UwH(;q3-JRb>fxu>+VipcV%DkRcC_R!m-}LFpl?i`nos!I)&S=_E&oi?O=+Q z$!msi2!!?rdktZ_72!~Za0G<*w|Wghr>fQ8(j4SY+|rNv?uAYJaol&lEO9TCeLul> z{D(W>ll;K7cTQXce~cVWpg8(0`POmRB3><0NM=L)Q|r7Tb83P;N=q5z0-;NGk89vg{KkVemvGm<8!QYqJ{Ub{ zHvH|3mI=etvwDh?Ku8td*8v(Jjp1r_F*(@T2fT8lFqTGif-Jh9}eTcp4r}!|60Un1=h( zaCaI`kYD2wN|WXKe%hTD_@U_i$%TJPBfXD^`TY0*HT1sd5jpsXSB%HYu)QASCsp<# zRZr##;{%cXv^&m4j~Kcn85KNYlnWj)QipWxU`SD!95O3PKPdf% zQZJ(5c4StRIw*BR87iU-&4Drm%8;Q97g2C|GHWiwpbQ(zk|GKYQ)We30?HCYiHaz= zTbUIl0wpq(rA3sbbD%5*WvQWz6j5;XGHWg)po|#G@*)b3VrE5I4$5*vL2#Q*iJO^O zQC5Jm!cbNgQE*Z-E6PexRvOA^5d{}Fv!aZGGHNKRiYVxdSy5JjvdU1#HW$h=HV4WW zDC)Z_lGB{NZI*xC95{L|YhCoA=!abQ%Gr5D^VUb2YU>eGPeH1tf>ccfshSE>H5H_4 ziua@)JZkFx_`&2POEo_f-JhJ{1ljutPcn>TdS*0LGo^x(L5JX#QeM=eRL_|9er~bb z%9id}*{VD4Gu`pvxTW5Q+bd}Ta&OfhAQiQTkRFJdNGF7JqFsN0RMa2sNGF9vd6iRB zgMd`jAniz}gmlUwHFXF`MIF+PG$SNxSUEMd2uMXO(vEamNT(fAQ;&dD)FbUkXM{wX zDyOC<0ja1-+L6u*>8wL)>JpHOx}+WHoRE+r<!&Q$Q-}ly;;`LZYWAr>0f`si;-jkuD4Aa%uLiXui54 z&pf|zd&xY%u~A^2-|kR|=eIi*;`!|^g?N686yo`nm^(Va)C`9HlfuyB zZsMoMVCXI>%;F9((rjy8_2u~0=$@%)c(O4}oxW!(x^ZuAM2zmUa1#Fo z+&Z3$h9+6R;uU_Sj8Oyb4B9)g-bskL`Gw5v}x@c)Me%=2B1K^otd^qteb z`}D5dF~))jx1BOoBi>p&qHqCDF%|NL#vz46IAd10kk{iBE*cH?DdbhLy$Xl%$xWzj zu?d}QK*jHN7tY-Mv8xi}}<=W^N%DzkBMUXbT=NJf&Ixi};)@ZoKB zv9FDb31+YS*Re7#i1>w^I3vz%Y+MxN#T=4h=4Ljg#m02X##9>{yXIiyl89f*i8CC{ z#>QnqUd|!u5jV4OL~I;M**Mt7#_l=TxFX_La^iIV+1PkfkZ4~$kaZQlda!5w6o7p%fHt-^EY)rSYv2PAGu8a8foH(Q0Y;4>R zSv8Zomf)Z0r^yPXVNCjLpu*UP127A=hZ!mzYW>p1siMegl`fq%Ev5O=qmgdyu zy>@^GG7o6Xes4RO_r5(c>4J$nKL~dpX3>Jh2Zp-$ws5yBdX$BgS-BJ66jZy|EQfiJ zStjm+b~3@+BLgp5yUCYxmPHTRtmeI1$*REa{p16?4{GR@hqtUU-^)B>4tDb~I%W5M z=sI>=$pjJgJA0|y$-oQEk+?&l(ft^U^BcViPtM1u#r+yiPtMox)AJiOg){Rxab)Q; zmVRbFhkEa~6-9oxl!ybODxZYHA|@1&xLTp!d0DX)npb->Xp3af*93j79hzmd4B8?Y z^mRdBZ-;hIT^c9OXQspxPHqVLMmuyzVFr3aD?t-gA?}jrV8XLj8!G)-l^!28g%<6^ zBNTQS1$+)0g>yNDb6r!|Z4}IVF+7~lDV*<`!d|0*A7taADO}DeT<)5}A)_#zQn->+xY9L+X`^r?rSN7>;mxio z95D(PoWhN+DV*S_ zP=gs&a-lrl$kpZYcSl_=e^=FIdg^~U4*B{}mxR0Zq~LO?OI{x1_mo~1B;41By0r9t zmVU604zpYe;~>9V)I~>i35jf_sujAUx&%5~qH2Ziq>98|wl>uY-BDcvoh?eWLU&Y` zKxeB_L{FDgYTO%IZK(8zRQj&eB}-Fao=GW8=M<*9rocKC6wam;j^q@MbWMRpD=3^x zDICoy9POF{D_BrCpHkojNu)ycaI9+zEN4OCLQ3IyPT_dh6jodIQr4FzB+Zj{mySCuv}#*z;^0-z-syrEQfpxu$?*{u-fGX zmK%Nr*iPLJ*nS7gU6KN9r%nefhi_T!rLz%Sv?@@MnW1ZCqKH2WUqeD1cKDZa{7YTq zb9SFKV26J>$7gEZW&XBa89DqbIX<)RF7a(WbE(6BGskC6-X;E0@jv45ujcqnySv06 z5&m+A&%CCX2|ZP0 z{4>$+5`R?qs~rBW9G~fTm-wrMKj!dv=lIO9yTl(8{yK-hH^*n*-6j4y;jef2`*M7y z)?MPS7ybr^&og>y{W0P05`Tm6pK|y-$Cu*UQes#A_bK5&?eGuf__hex75>x0f5zcY z=lHgI*A+e&GkN3M;UCHIk91xBXNAAf;UCTMk9Li}QTWe0d>&0q>(3Uty5j$N;cs&I z$8&sJq3Q~Mlki_~_$P9FTUzQ0{{`W{=%b#VnCV8aE}luCyJsq{Nm_M#B;)4^q}DS zquV|{zy*}XgVA~cu8&r^djjnKla&xFf8eM6M0&OQbQvi<%EZcBTfHT~ML^hdPA<0sQy3>JepbhFWKHqEW|nx09MuCN(|{pYE)3k%e0z8Ed-H;ToQ6 zhs8wrZH44qI!-x{0@?;ZyW7Mdu2Nimr>~S}5iqU>Xal zB-UVcnou9ui7tGo&i0}Ir4FRjIXRkWf{Uh?ii~iY+NZlYYT8+O2CP|6Q`0jLyLHQaxX6}!EPuPLhhobEU2Pk0gcqkg49Pl3t8HnEXeCv#scytGRl6p zvgaqcb3ic5T~0m79raCw3hc3+VG2ZuL+m0YbAQC6s5vdQa9*&iYDF23zNVt6Ig?i! z(u7_WCK)JJ7;oU^+%m{?LtZTh&l>TJXCL`8MXmvXGHvzhg$VelCZ->(aO)V}x+Uczr=P zBB@Ji@3u&e7WfzyA9KqkEX*mFh^ebz!m-Up?rjKX+N9kG=lViMXFM_9wTR+}=pBgY zQ-uuAS%%#TYVSJ{)MpCWU9jxB7FRqRzw6?Vw@#tQ?cS+h)O;A`f%RD%sB|t88CnHrL?WlhU*`yOGih#Z0=EPh6zG z154miF|TeVv*(>i=9OYjUF%Z(>b&bR$p%8!i1WIUNx9B{w`KBrk%zfulI-V{$q6~b zbThGeZ%-!EcI-wbS@dsa(zQ(D%laM2Bn$k_yt?Ta=OXM zKsPc85AV86vap}k#sGpe*OZ>@=afm-=!>M@#wVoqJlBy0Qcx=fzSIia4<1>6KzuS1hZpWz{_Xy=_^&TFmjP<=CynhVMjTuNU*Xo>pd;HYa;U zyxqidu2!E*3G2D?42rz%3)zmU?f)BLY3G6+{-;@b9%DtZx$^AxqTKSc|D`DS(wgXJ zcQtO)zEdgl5u7bY+afe8%EgE7OVMpwd9+|u3)^@QVtGFpvlj!B1{-Y54b1Dl+p6m4 zCo#4?G-)7Kb&sYKV}FgK7E0sZ42LEZS9ZDMb^GXy7_9crMl2+u@T)< zwb*i9KegBj-72-%%F$rV#YS}n)Y4Xs25fOFx3SS+gNv;j4W4qb^`ikB$ojfrG+--O zv8P4@Hgy$ydNkPRV$X~Q&%4;OqXAo{`nqv6c)`V<9}Qk~u}!1FRu_9=G}z{1FOCM= zU2H3hpuUT4W4SVoZs$OL8lAw0!d^dX5*NyI=E-jd#dZnT$TiLX_4=3p{_019)89sL zE#b2h6Eh^7Qi66>pOV)W9L0FtRhx_}T&?+zzVHMom}g#>Rj>Urd=+%7rXNSL*)QIS@&}y_lsBx29jp(wDTRU(V7ar8~zWqH~3G zcze20p`;v;x@K@Hhxw(M63P8fU8= z`ic}iJnBBckn@-l{RRvjBoT(j*rXs>m0pRc!SwT5iPc2A2tY%N^#jyNxpK5C_! zHY$A}{9?0Jjw+EP+-6kvM^)PIkb%Mdx;U%UM@c?mMJIW^t7fixxN>h$IEz-8MH9eQM<$38o3}=!P>n1B&lARo>C0#P@CJ~od81$-^WG6PNIr2PXE_K#R zKup#o59?dg_gU?b{#2gnYN$IQBl(%k0n;%6`Mozi9Jt%(cmuzsGjv>~7iLVz*z;-; zvZk0wXt*wy37LCK9uMQQ2^nVuC7-VQ&LL+akjhyBnle}onP}rLN*CQt=a>jWw}Z6d z?vpNDkh5)BrJPj*N!YB)QILp{3O?btN?t+Wj1p3)XWKeU+XTANNtXsBV zAVWZ|`y`ELn!PO7eUipP@5`l8-l+=Go9n*kMaMa9%5V`?`3K7&Akkpqtl3D$@z$q_cBYqxtCdTOD}V(`;ULK zef)9S=NFaUp_f^DOD`+HxA!tel(zBere4-7UC=&W^=0E#t!2EDeL5M>GE&uM8>u+? zu93ija2=zh(6ptDB(HNMm$$sM!a+kgIK4*syI$5Kh?wRL)5R~ z7y68;D2zFVC5)qQz5eAtVANq4l98l0>&z@|jzFQa-KoCy408nQ8dU{j?&82IXKjwA z)}}KER$y-evK+Tu1S5DWAn3<^ff-|_4P~C0`J#xEXr5NbwZY`t-yi(u*i>CMD#dq& ziswc1@;<7E;rlzo0sX$Fh3-bXN~AZ;dvP_{i82_@8R7IdQVE6CC9Y7zu29Sxbr~Qx zp_xAB){U-Mdd`w|9bGocES7Y2Hqnu&Y)kEjIm3hu9um` z*=k8^9(9Njl_ErDrcm8ZK-!IJAeOcoGT+aH*3wmGLZ8Y;ZKjwrS)56x36gT(ierc@ zeVa^oIT*#{R12%pth-2j%^FJQgEU*~q%hUIrtpRS7j|Z7R>0F@w=f-aa_+{PTwQN> zdZ1trP54=ov^1rhW!h+!MD?L`B@IqV6S00Jyybj!*322%|G7S0LAn#TMbf{yK7BUh z*Q~kRYO(Qd%86ZWy8NI{Ge7Z(Pe=+2)690YxdeD)ej^t98vP9B&E(@oH~tfIZsF7)}=Z(IGYbRmRcLbJ)-0+Rl+l-vX|k43T% zavLa5$&DbES;h`IKdS$KdtV;f9i=d=ydWac*E$N(w`&l}tM>w6+a6e1u z^cZtvxTkOS5RYl>N$UA=He8yazgG!&;|#X7;y?<9Dou(`gUg47VNI57os6tMEoB&gflH?LnuwvoJ$$N%rQ0geP9Vp*;HcT^ix`OT^gdY@Gr;#jpI#HvlzP&7j{$qe^~a@45J=dPh+ho z4Hw4=(b)xWorbbk3F-2K{VEuwK}s7=h2P#w-Pt45RGkx zt$)7BNpu{IU->J3JNQ{!BW>PjfW8;wPW!&|Uw!Yu_ZUd{)E>>H#*O*-nLjYHJl0Cg zBS=cm#E96M8#mZVB8#rCU-Y9$3QAkcI`XfwE(Jd;KBV2()yz& z_Qs7SyhN8}@u^=H8!SU^Dx?zRo;HtbQ0plU40G317*)FJLN8KzRDm%**qu}73Wh29L>!BICDmjD9I)TIV^iC$`M z1PjVL&CC~%!C!=5Rs&J2%+%TJ^BVHUrcwZmB)C_FON`&QfpwK)^4HH3ccKxE#~~=* zcSgtIt5?m9m#f|<#V^h5m1<&V9-@4ga|%jhnGqOynJ&Nc)jnyYCgCl)Todr-1pM%w zFC2(4=CBySF9sFZ^H~i2IcpGeIRKc=5Xq&04(}~OeD{QqM0lMf3jZBRME;ML1TI?@ zk|<)RNurnokOU5CyGWu0A!|8)g^nlZFzWapED+#;4uB4@Ch4bG@suUH9{+<52VWvb zG}Mc7_5Uk+k%Qe$=tX%9HQ5z#0J6j71TL~GM5v)&RQ&Hq;(vZGD&xvR5+w{ZNtAN{ zk|<^998VV!vf^x8!@VfO&~PsjM+H=M06FpwAb`VQ#sciwz(svh(*p2o*#aDQaA6c9 zR&i%3Y=Gr(y=02rd7$N7GdT(~mAw$`p1>&ry)|U8Kjvs&+!J)j2HwO1ZbY}xa7LIuZ_#SL>-4=UthyV;Cz~f!x53B;bY)< z%WmYyC;!=lqo@Ec`Go4*S?^-^4bGp2>J>94Ey%BcH4adSuTY|po={GpLU=+Y0fp>@ z>H-SU355j|l4C8!DL`RER2FqL5UEL?I}4cng}I(S?V94bJ5`>10Ng|>Ivio;DwT&2)sz*Nx%RJf!l!#O#nO-c)8@y0?xypWyY^XRY^Ptc(ug&m8cqt^CMBU zdVYFR?Jc1$ou;!j(%ET3MW=yQz^eJ5>bx)ZT`+z{A99aDKT41>Y$UjR109vfF&OD7 z*x^D;Nj^W-;;JQnZCn`G@CRndNM2CdqEl<>V@7xb!7gKX_34s4e3D5rh7CuT7!)a) zV>n}U3dzLh9bK64d6tY}SG-FMKF^jhe16C!1~tmj5*9VuF8M+msuAl=78pCmZEiJU zb;&Vo)4L>(BIU}M2=^EiDUV~+8aWun=oSqX2~KwP!A;NWF}#2(lJ3jaB*LXKEKW2a zTqeW90gZ6E496n@)k`lNQx_$fOyCs#}I2w}pCxdQE0BdMcRs@zAYmDDjv z!e!IW#>yTd8ShTUE%}1X2eVwbad$s>aTb`bPZH-qXTk8o)x=Ca0p=}%64M?hldtKo2l32^CktJU=S46g6R@x zEr_QY1YO+Su6AOv%(qLrxJ+G0FjG>*z2<^~=O4tkcAf0$co#wtGBrYuB;bWTbu8pc zLN<4cLg7CUojp&|@yedIg(c|QB|Vqvzt!GKrsBgyX$xKXJ(q@!cnwyKsvR3@jw(uEw>^GWQ5*akMy_u{Y0@ zypiUdDCM5nP4i@O-M-n))1=%)yLpzx;bqSA=1G#b>C_n|1eL+li;xn2?GnnDaG}B; z)*^m8+t-)<0zyD|oLcM`vT*#Yp-zjz+i5XCzwxY{f`5tUZuYmLKNwjME^%UBmo74v z1*S*UH4R?B*mo-7j;pf{?5H(XyqwO>wQj9Oh+4a3ATkeC<3z<>;zZ_fE^&fv6o|~F z^GInZbLm22V*tD^mI=H_oJeTB+KJ5L)Ht5}Z>l-g^86i7{~~kgM~Y}DbLmauY2H2a z5o#iR>t!G^AJ1{P6GryU`jwxkej2CcKUodOHkt&tN}@;u{PisjAdB0t zM2t?*`JOVJFUOeu`T8}o+7{w*2LDj(FEpO2G=AP{du3t5%g?D`AlSj{Z{~FPoM;y! z)}>InzEgdz#;J`Pk+;F-ivQ(BmVUdrW*!0k->9hI}tL31MFP#*$(x$8ir7niZd4cyAKu* zSmQ;Af5qp*3MZvVZj1^hs;tx8cnmXCYBEI2>0y)$Xp;~&u+=UqQg*zqBB^{#5LKzz ziU*5vHdW0)9Q)+*XUb@s4bgl!o9ilm>tu5v4?goGDE#viK@rCe#d&&JNj|K|ESIwr zY{`fAB27N7#T5GrjZz{h<*O5HwjF!G!-vHL&s!RWkC-{L;-3W;)F*7$2wXzu$QA^o z#>cLN{P-9nS^M^(V%LH*FCQIcIoMNB(W4^vlkbauU#n9O{*Y$p^Wi?x^ffU0#ZNJ` ze!vGyVH8B_5gm%2Y1KW)!zH;~u=8&X(2+tlZnkf1786@?00yhh2hF$$Bw0@FcaDQRpTVBaG40nc~TsR^qpBR-p~ z_*#=f`OZ-TZYRe`u_oc#DSW9HK_d(l4--70JT(2SHgC30PywqBV}w~_-<+iDZA~o4 z+)Dg~H7QFMTh)GAcWYu6+i6V02;|OEZ%}}8t5Kjyp|kkVaH|oZvElp>r+-uL*Wza? zsyG#0nic+jxU`RQ1E6Wo0bX&$+{kuYA98)#mRA>Zc=l z#GEg_{?_M*N^+~D8uGA~8Uj4bVl0mq`PsNLp;}@nqOV`)Q57e`Jf=k7$6+m(QOIYy zwBU{caI@gm5h%D>KOv!I_&9SnqM^1p%>VJ=;AR9jkHzc!HEP1qJm@(Z^5*7ITgfn4 zgbYwOAUd@Wj0eGl`CY&q2{CJn=5Ywvhq#6iQ<;p$>?qiI3B-96NmwOejf6IC34E6b z$b48;3ka8`fSEErTZXZU6Lg1!SdWSC4@(GNpNNMqP(Unp1jGtT@TJ2(;C$c{6EIOi z8z7ehPAox(?@2&7zXZ&b@kJ8CwWR6{6^wW9$P-m6_dRq$^m6W!HdL8|F!9tbD#NKV zY?oo!SRx+AjNmVn;c^+iD8uk(i1=iNNIyr0vEUWqqcU79!!D|Mth7IL!N`JP*=eS^Vicoe`{s`e}m$<&Zk z4N1_D>P?clj1lY69Bf%Zf!mB~awv3e!No)Q_yJF8ju#lpbNJq4Yl1_qHIMNul|g?I zuZ6*ws!|yOoC(-v0yQ=vhFAFhU2Bf0Iay)5dE0C2gF=G zp%Z2&wwan>4miJh<;LR8m52|B`MF4q^2gS-QR^TqZcHQ$)ZU#WdnE)|i=oY0s9Hj8 zsG}!q-M;r5=lZ6m2{)>f#tyl(*bVE$qB(|DEc*4~H1un!;e}R4r>Mn`5`<<3ZZpDK z1FP1+JRxXWnpKZ0eY*^6)*_!{P}QRrfrMFaiI0$QhK3^_PMCEZtKFn}^aL4pCOlW= zgUaNjxV+QqQiWP|EtNDYMz~Xl$WNtVE9O_^tj()PF`_QT&hFo~LlC?T&Z1)0WK$Wl zsIz-`sold`>TF)te@Mf?+LU3Qgxmu;oFyS^UCt{2pl)fQN4G!~E4E_BuGx)fv^;Z# zs6bJ>N2nU&dboVbbpb0^6d+DzWoaOU+1ZsYK)yT;zD`qQK9&Q4BIadXup%CN- zg5%kS;O{h>LbEY=0@c3DVqW<;-I zPqh49wd8Bi29YZj>=JPr_7u%>!zPm`WSJV_SdLM|Mg}K{xl_a)R^yykR){{v6-QsU zbI%l=O%iz6iB9Jd7M%}a8*=6NbeW(tfn*l~GbGjv+q5u-5!(|iR%~Jjh^-C*c|VHj zHVJF+4MZyzXbz^iRvX(5f*Jc0PMKmigYn`FSxUR)!82Zhm$wQyg91P#Y~vLjp4U?6 zm;bA4IOq|U1+Ormomf1#(is|CxN}kwvoC8Ne03}Jp?w8y%k!sHHaBSyjqPQX4T^?4 zu$%fvCYm;Za?w8E8G{p+YRArF8k`O35$zL){6y0|f2K>=tSFWItb1ZfvvI}J5gD_2 z&CUW|Ao3K-g<SrCrHBZMNVe?lr*7Gm0_X1@qv~AOG%3tN{>aW5m;v> z)C+6VgO-4pK3q${t?ZgWNxd-7NgS^wP~Z|TO28e;HG$lE;p}<|WVpnO1jP7|MdB(X zxiE+-)k}Z}iE9m})rFaTqI+F{laTQn9jgv3<2(dlb#by}5{Cmna?ga%-n39gh-gSd@-Onl&^^ zcbH*^C>?7EYx^jjjp3b9I+!}vc2PQLHy$sXRh=yyEBm)*MDu9UXuJzvXGCLOcXoPD z?(}f53%AdilEvp%RF>pkfuk9&WzEAQd}wSBL3`BMaHVg5u2z>-8OsI$665NzAQ3__ zg=kbKG`qf-8mWS4l{H=sjc{k>W&|rYE&!UGQ=R6{Gee~&Lp02kC?SiA!%)%;WOvj& z;ZALxDcQLDa^kXE!{ZD~L&iB63j3!Qw(ZsQM9XM45op#D;k~X_7nj^B+}|0Zd<;+( z2BUrJrK;IkM`7Uan4fmJeBlS@z}!wwl{ZJ>UwkDdE)+yFj@3{{$nm)>=;)z>2| zI5#fhQ&ON~rC$gTt)YE(>%hC!TT0Dr6KQ@93pQ5MW*>aSNp ziBLP2U9Uo(rMa{5%+8Q!c7`?yYmfkkt0XMf)oBNB)(+TrtG7+1_%#8|w-dTCJOdQ- z2Xrc%=qOjr8^G=QUP4hUWE`6n*beI^dAYmesW_3P0dKU)ei6_HBLOcw@x&@TLB4ne zOQy~XPCVJ>8wdh#USO|}w;Kgb^6-w}869>b@dM|8sb+mU(nq_zr-qtZDWHX-&p z_Wd9ahiSY+M zqB%fuOu>q$Q}8c7vf{lX6pj1hfPp377L=cyaO$J(9O`){XJRvr4P- z1PuyJWSOi&>_v(!(Ps?NDw+-yr!TPIjDI4hSn*($%rG3=MGI*=l48HBvfCj}sFeC#-MqE_2 zHV*#bKJT!L7U#QPd5B@>yN$1Ju{U0PQ@glly)N!)wKwCI_f#Mayokl`b8!byJh3s4 zIQ57A2#`3)B#Z}n&R)z(xA>8N-B0{grOd~R_>c|?B#wx;Rxz9qS5&NB-B0`Fq=$2{ zbwBG@6&}8gsQXEO-4FUz<%n3E3>TFS)5q2Ql)vtW{FqhIr%ZZlkbIR<&u@Hjv-3eM z4>`v1>X$eOkHNs07~=vHW1L^VGx76j0sOdKm8ZJJ=#ND0y1y;eAT`ytNHxRox21ZX zI9XNRr4Kr9GAIMeQw`)-4s^7NUIZ%Up)h?F(D5p@8YoX#;H?2VSw(As!pdT*ZG`i> z-2|XBRca#8D!fR^97({dC7uktM&hZ!YbBlz+y-UO{29O#ByIm&6l* z=LYjQ#G$iCIp4LMz=8mtvi^&Q{eH9Zk17o7&>+y)AiC?Gl!%0SK z`46HC;gpJ}s%PHv(s|xOvyj5O?AZL`$y%X~dRWLs8tw-_yM_nBsi)zs;oa2mcJOLy zcxT)ztl<&8^tCFEU&BG>yVUdz(b%BX@c3SaWX7$?eF~mrbQ$%0JFciTN`>+NH;wwg zBh{Nn{oj$QbJU|b@N1Z?R32k!juALhqTyqD>2o!FA}+eq@M&-;s<^ngtVq%gRhXa- zYk?XBiZw19pUBk$2?&&FY>8gI4CPuN8G$M-kd8o&7O*2=OH<2es0u=xYJnOAlC^*> zxR)Va4d6n{7Dz@QOADkTkgWyM5y;U383^QR0XqVDS|AgF?OGrU0f!dI zMxa0o=OHLiBXOwA{3wR`9B z7E35-RNIJOK9*zC1DmbQeDfOXn=I%tPnf{5g#ViO!V<7~2y0i`!)8ym71Sq-p$UbZ zg7ev6YdeLnH)LCgPoJ}$#HY_O?M*RZk;MiYA6>ILV11YxBY7-?JU>Y@DLnRvm`n`f zO=bo&O?NPeGqqz7ZMu`e3{!gseN7!0M4CbwOfz+45Mc^q5N_(kV6q9zRhR^(E(}6V zT^UR?-Nm4t39F$n-0Rnk!FW@52CYp!7=)N27>qI9%^=9slRX{K~xmafclSHmpLm2hV1#w^_%W(iSxFiQlp z+}$us3#BKs^kSCChFMxFy_uyCvqUw_5}@>DmVV6AA1qCA4-z{qQUST&Lv(>wG7-;z zJaE{CD>P1-g5S-;cQ#EN)NiNUmdZAVq67)pS_u{~L}?~qkkVYh03}2~yhbjdx6)ET zBd)&T?}-Zm7coDBiVHX8e+?8#b;j{Cs@BHB=0gE_$yjSN1v0ELngSSB8`187RYp?~ z!;3~!FvD`AsTspEqp3N=Qlkk+^~13BgO?-#i;bq1438R3SZ;>l*9@jM42z7W+Zh%b zO>JETf{r>x;SnCG3Pdy;Dikg&)>#TDR0^n_6p$hX)LIHCL<%TK3MfDd$WLi41=dCi z>~<-zwk)tl?gC@E{khNrU4>R4h31e#+b)HcCxw51yiYm?^MHOd&0&yZAKLuAK0(vXBMgq_Xza2xq z>s4U|k8z;8S>~$TF=~{U$NZ_K7(QYYDZ9t2Vd(j$Nddu#!7qT)d`)=F5^4=*C0{TG z^Flovtv*w8~2y6;g~FV_t!aDqz0ZBj#x7x*xtPxmHm_bIqPHwvi55QnRLC^U3-o}v}!Nlm+18MveL@aFN`+ggnEUFDfL`TDz0t8+^94%<#CP5!x zKk(qcD$C5p!C&WmTWM*s?Ao-^pH^CiKN@&gx4hCaXZXCndCyl`)^DBMZRvDK5E&q~X|5Bv05ce%o1jtCs`!RZQ1mt_;idmpN>bO`JD z&58U9%fkEL9@Qzg!qRa>{3)*u6_&*#`{c~etgw6$wcx3D>=l-o<1UuJxTeCgRQJ;h zLGM;rZaXw&)w1_1EGO+@G3T=@Ea&%rk<)8Sg(bOa?ZP(;DlB8~?EkUhn+nUuuOC^p zro6&3=&|2_o^_+blJUUeQ>$B4TAog>EKzz@T9#)1UJ^38(lT!KoE|IZR$5LZXTG;7 zv(gehVEUu84p*{_@LzdM>K$+1TQeowa;4z6VTbY$S&lv6cO$0j_cA^*_K$^~o*WcB zC|c#)-e2YGD*0Aec3!FZ$HfW1SXzXycy!+%r}O(IZo22vb5|{ywMV;8_C1-O6Hryv zJFVI>XG_~guUU`hFE-@mr0u_Ane*i{Eq{3X>-_g8Ci=DTzhddO`R6OE+Z@ifzu`N_ z{MgTys@YMk+RWXbZ;5Dm&pn@Awyf#9Y5BQpyYsi5f3<4#9hWU*UVl0N;P|ci>u8nL zl2&CoG;Yx5x1U{~uhy$dmY*nl&|BA&##dPSU2V7KK;$PD`xJfKPckoBw!IlS;=@@5 zmhWq)Jk(S9$#SHu;2z(ELzXV<-+kh%-9K1bKe?}Jfag)mCF{$E@0XNYYSy&sobg7n z6Y|X-nZRJL6eSo$|uazkYSLmjMlElz*Op zi>|##araH9+K$2W)k7WVRK!(!{G@~Wu1dp?C}Qb};eXI)r#H}+345u}^mcS)XbcVZ zEG3^GyHe|vujr>Xe^AGdT2p!db#(Rag%tN$SIYPyfpXi=AnW{bbnMOE^x&f2lu_NA zUK^J{q1Sd$P1`6sePbmxN%x?pcU4lOM_!|6+CD>H&hJS+2RqQf`?t_LPg$tTkK<@t zhZpJmPoGo2IhX0z-d>dQz^nB8)E}wsN8`wQ?CTV67)5hx2U69BsibWGl6qFZMdv zTFH;b(vo=t>D+T&X!X9gsK?_(xku(w?z)xKYvea{y#Ft>HXwwC&Z?z}Z->#0;~VIS zxivI9=OJnye1RN;M^V(VZ)sgZB(=NaART-4C3h))5mFa)M+}sI)#pyLn(Ag zIh`Fnl=dCyPTuF5QR%#c)Z8(QuC!T4A*GK}*V`g!QQ+gWC|*Zb-*}WZ<}9GwfB2c= z?}?>=Nng|YQ^oYJaWOPy!XauDnn_=;$|cVaex@-mEugi#7tw^iiS+6VFVm)^N9ft( zJE+SCPf&IKL3;g_N@~Ar5{*1CfDU}ylR~qO(C!JZQ{euW$k;xJ0tc?9f`<#JaBmz9 z{rF9y=2z%w>_+N1_&i9gyNDgLconv^+>o_juqULV_?)&%F#_VYgUdbd02 z*O|R3+@m%5`|0Rj@5$7;Vl)j}pG-3^=xN8XcJ%AK4`_P)^R%W}CB2dQ0p-5=3w`zA zaQd>*&!nXHpsV-Jr;neQOsD#fqv){)TD)uljeMXZ^_u9QXNm);dwh3#bjXWz-*ZQ4 zz=-MO>$8i(u0BmcPklojyxyj<#$l8jb&`IZwU?G&Yfgiw&Z9ORw$X~GJJPOQRnA}kuh^8z2ucjSHfoy)pnpCpUa}Bb`PeBrTu8-sYSHbvXWv`uap0h zBs}jZ<6F z@ILFQ&D!0x>C(SwO#483X7^C)GCPADqgIl^vWR@DexXj$N%Ty&Rn$Azi~N*eD&F%b zT}>&VeH-tk`wqNM>w0}b-+$PXHl(D|shG*s;)&BVE2)wuzxDvtp6o^cdUpeT7u=12 zkl*hoXyVYP=#s}4>iA?hJv1tx2HogGQ?6~L7k6Hv(B5OI ztPS=1B$FO$^-t>gg%8~q^8?+sGn0DU@hMfUZ%Ow*nMFr-y-&WDCiLjbU(g-bX3?D2 zjI^xz^R(}S2kF`AXQ}chqSg0|rh*e&X#G>$Xx5is(pUO6^wH%ml(cFW?b-MeEr~xt zzmB!jK3g?qSKURYF1|&UG27|z>1QbU^R^Urax<;H6Fc3T-lB~UE~m{88L4BJT)Njb zpH6tKqgm@4)0~IjrDlO)RI;TD{dWF3Ej#%KE!%mW%4SU?&kt*=6e*6eJ>Q@u}^XW3`aN{MMYqLSGccJ^gu#@usD*EKT zuj#I_jVSk83^hG*kml_?Osh}sq7BV=(1G_}A@3<;sO!k))PY9Qf}kood?(Th z>+7_1=V>}|J&dN_xtZoHiy)6Hf6&(acWCmI`{|yrEV{qo5$aO2osK_}PSu~Mk^j)w zN$K+eWj-{Hf-E1AEi#RY6dgTxY6y_xdIU!|c>HYJ_cR7%RprjH$mY52%^y8ZXY zl=l1{TI(4FqyKL5ESW>41B&Rrj=#~d1BFz6#Y%H0o~G3ki|N~)-6(VA9(rl?QF{8^ zUaC5A8*S^;mj;Fmq>(R2&pklT zzIiu!m+qw-$6u%EQ_fP(^?kJ5dl$_eJCmkv{FuI&{x*36zo*RSuSQ3ERJTg=@T zzrBrK+qRVoR@PFttPXT}`W{N%+mnj3Mp16or}Sk}V+vf>g`RyVf;x{{Myn2;qhrce zn)=v!>K~RtO~)0{b>DmFO!e>dM$|KOc1dUY;b;QY=6*<(6<<-;yZ6&wy(dtlDUMow zTS%|Kqx}0lQ|PYN#q?lq0^PSNiAI=fRviD0m`N+R$(B37qIQDTeEs3Je z|45~P2}>z#$Q-O9TT*O(ASJd>rkDFJqpX55I)8Ew74+Fn?W=#JUnijtq~z0+YdcZD zXC;Eqd==dj8Tg)cf_> zv@q{On%aE~4eanJQN$%WLN8Hm^+?h^Jd{eGvyt_a4!-@Oe>YtdmTs50nf+nh+R_Ks t^aUJ0yy)d94lim|)f+HcxpQ_Ths_Jm^#r6gSDwBb8wVYH4FRJ9{vQ_m?y3L) diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh index 0087e91..28db680 100755 --- a/scripts/build-wasm.sh +++ b/scripts/build-wasm.sh @@ -13,7 +13,7 @@ set -euo pipefail -EMCC="${EMCC:-/usr/lib/emscripten/emcc}" +EMCC="${EMCC:-$(command -v emcc || echo /usr/lib/emscripten/emcc)}" ROOT="$(cd "$(dirname "$0")/.." && pwd)" OUT="$ROOT/playground/public" SRC="$ROOT/nisps/wasm/bindings.cpp" @@ -38,6 +38,15 @@ EXPORTED_FUNCS='[ "_nisps_ml_clear_examples","_nisps_ml_example_count", "_nisps_ml_weight_count","_nisps_ml_get_weights","_nisps_ml_set_weights", "_nisps_ml_draw_weights","_nisps_ml_move_weights", + "_nisps_ml_feedback_set_mode","_nisps_ml_feedback_get_mode", + "_nisps_ml_feedback_exploring","_nisps_ml_feedback_learning_paused", + "_nisps_ml_feedback_set_focus","_nisps_ml_feedback_down", + "_nisps_ml_feedback_up","_nisps_ml_feedback_drag","_nisps_ml_feedback_static_output", + "_nisps_ml_feedback_enter_explore","_nisps_ml_feedback_exit_explore", + "_nisps_ml_feedback_reroll","_nisps_ml_feedback_nudge","_nisps_ml_feedback_undo", + "_nisps_ml_feedback_like","_nisps_ml_feedback_commit_place","_nisps_ml_feedback_cancel_place", + "_nisps_ml_feedback_placing","_nisps_ml_feedback_state","_nisps_ml_feedback_undo_depth", + "_nisps_ml_feedback_placed_output", "_nisps_ml_get_layer_stats","_nisps_ml_describe", "_nisps_engine_create","_nisps_engine_destroy", "_nisps_engine_set_params","_nisps_engine_process_block" diff --git a/tests/cpp/parity_check.cpp b/tests/cpp/parity_check.cpp index 4aaaca8..99bf291 100644 --- a/tests/cpp/parity_check.cpp +++ b/tests/cpp/parity_check.cpp @@ -57,6 +57,7 @@ #include "../../nisps/engines/channel_strip.hpp" #include "../../nisps/engines/paf_synth.hpp" +#include "../../nisps/ml/feedback.hpp" #include "../../nisps/ml/mlp.hpp" namespace { @@ -82,7 +83,11 @@ constexpr std::array kProbeIdx = { }; constexpr std::uint32_t kMagic = 0x5450524Eu; // 'NPRT' -constexpr std::uint32_t kVersion = 1u; +constexpr std::uint32_t kVersion = 3u; // v3 adds stage 5d (ExploreAndPlace lifecycle) + +// Must match the salt in nisps/wasm/bindings.cpp MLHandle so the controller's +// static-output RNG stream is identical native ↔ WASM. +constexpr std::uint64_t kFeedbackSalt = 0xFEEDBACC0DEull; void push_floats(std::vector& v, std::span add) { for (float f : add) v.push_back(f); @@ -106,7 +111,7 @@ int main(int argc, char** argv) { const std::string out_path = (argc > 1) ? argv[1] : "parity_native.bin"; std::vector payload; - payload.reserve(126u + 12u + 126u + 1u + 2u + 2u); + payload.reserve(126u + 12u + 126u + 1u + 2u + 2u + 276u); // ---- Stage 1: ML inference at fixed input ---- ParityMLP mlp(kSeed); @@ -190,6 +195,78 @@ int main(int argc, char** argv) { payload.push_back(r_acc / static_cast(kSynthFrames)); } + // ---- Stage 5: feedback ("Down Action": RandomiseOutputs + RandomiseMlp) ---- + // Seeded exactly as the WASM MLHandle (kSeed XOR kFeedbackSalt) so the + // controller's static-output RNG stream is bit-reproducible native ↔ WASM. + // RandomiseOutputs proves the controller's own RNG; RandomiseMlp proves the + // weight snapshot/restore round-trips identically across platforms. `mlp` is + // untouched by stages 3-4, so its RNG state here equals post-stage-2. + { + nisps::ml::FeedbackController fb(kSeed ^ kFeedbackSalt); + std::array sbuf{}; + const std::span no_out{}; + const std::span no_mask{}; + + fb.set_mode(nisps::ml::FeedbackMode::RandomiseOutputs, mlp); + fb.on_down(mlp, no_out, 0.1f, 0.5f, no_mask); // enter + fb.static_output(std::span(sbuf)); + push_floats(payload, std::span(sbuf.data(), 126u)); + fb.on_down(mlp, no_out, 0.1f, 0.5f, no_mask); // re-roll + fb.static_output(std::span(sbuf)); + push_floats(payload, std::span(sbuf.data(), 126u)); + fb.on_up(mlp); // commit (no weight change) + + fb.set_mode(nisps::ml::FeedbackMode::RandomiseMlp, mlp); + fb.on_down(mlp, no_out, 0.1f, 0.5f, no_mask); // enter → randomise temp net + { + const auto w = mlp.get_weights(); + for (std::size_t idx : kProbeIdx) payload.push_back(idx < w.size() ? w[idx] : 0.f); + } + fb.on_up(mlp); // commit → restore original net + { + const auto w = mlp.get_weights(); + for (std::size_t idx : kProbeIdx) payload.push_back(idx < w.size() ? w[idx] : 0.f); + } + + // ---- Stage 5d: ExploreAndPlace lifecycle ---- + // Proves the shared explore→reroll→nudge→undo→place→commit core is bit- + // reproducible native↔WASM: the scratchpad nudge uses the controller's + // own per-instance Rng (no libc rand), and the snapshot/restore round- + // trips identically. We REUSE the same `fb` controller (not a fresh + // one) so its RNG state matches the WASM MLHandle.feedback, which by + // this point has drained identical RandomiseOutputs draws on both + // platforms (enter + reroll = 2*kNOut uniform draws each side). + fb.set_mode(nisps::ml::FeedbackMode::ExploreAndPlace, mlp); + fb.enter_explore(mlp, 0.5f); // snapshot + randomise scratchpad + fb.reroll(mlp, 0.5f); // scratchpad op (undoable) + fb.nudge(mlp, 0.05f); // controller-Rng perturb + // Probe the scratchpad net (12 weights) — exercises the new RNG stream. + { + const auto w = mlp.get_weights(); + for (std::size_t idx : kProbeIdx) payload.push_back(idx < w.size() ? w[idx] : 0.f); + } + fb.undo(mlp); // pop nudge + // Audition + place at a fixed input; freeze the scratchpad output. + mlp.set_input(0u, kInputX); + mlp.set_input(1u, kInputY); + mlp.process(); + fb.begin_place(mlp); + // Push the frozen placed output (126 floats) — must match across plats. + { + const auto v = fb.placed_output(); + for (std::size_t i = 0; i < 126u; ++i) payload.push_back(i < v.size() ? v[i] : 0.f); + } + fb.commit_place(mlp); // restore real net + // After restore, the probed weights must equal the pre-explore real net, + // and the committed output is the +1 label the caller would store. + { + const auto w = mlp.get_weights(); + for (std::size_t idx : kProbeIdx) payload.push_back(idx < w.size() ? w[idx] : 0.f); + const auto v = fb.committed_output(); + for (std::size_t i = 0; i < 126u; ++i) payload.push_back(i < v.size() ? v[i] : 0.f); + } + } + // ---- Sanity: every value finite ---- for (std::size_t i = 0; i < payload.size(); ++i) { if (!std::isfinite(payload[i])) { diff --git a/tests/cpp/parity_diff.mjs b/tests/cpp/parity_diff.mjs index e8bd40f..c2cc6e6 100644 --- a/tests/cpp/parity_diff.mjs +++ b/tests/cpp/parity_diff.mjs @@ -16,7 +16,7 @@ import { readFile } from 'node:fs/promises'; const MAGIC = 0x5450524e; -const VERSION = 1; +const VERSION = 3; const DEFAULT_TOL = 1e-5; // Layout for context in error messages — must match parity_check.cpp / parity_wasm.mjs. @@ -27,6 +27,19 @@ const SECTIONS = [ { name: 'final_loss', count: 1 }, { name: 'paf_synth_means', count: 2 }, { name: 'channel_strip_means', count: 2 }, + // Stage 5 (feedback): 126 enter-static + 126 reroll-static + 12 temp-net + // weight probes + 12 restored-net weight probes. + { name: 'feedback_randout_enter', count: 126 }, + { name: 'feedback_randout_reroll', count: 126 }, + { name: 'feedback_randmlp_temp', count: 12 }, + { name: 'feedback_randmlp_restored', count: 12 }, + // Stage 5d (ExploreAndPlace): 12 scratchpad-net probes (post enter/reroll/ + // nudge) + 126 frozen placed output + 12 restored-net probes + 126 committed + // output. + { name: 'feedback_ep_scratch', count: 12 }, + { name: 'feedback_ep_placed', count: 126 }, + { name: 'feedback_ep_restored', count: 12 }, + { name: 'feedback_ep_committed', count: 126 }, ]; async function readBlob(path) { diff --git a/tests/cpp/parity_wasm.mjs b/tests/cpp/parity_wasm.mjs index b49f6ad..840242b 100644 --- a/tests/cpp/parity_wasm.mjs +++ b/tests/cpp/parity_wasm.mjs @@ -38,7 +38,7 @@ const __dirname = dirname(__filename); const repoRoot = resolve(__dirname, '..', '..'); const MAGIC = 0x5450524e; // 'NPRT' -const VERSION = 1; +const VERSION = 3; // v3 adds stage 5d (ExploreAndPlace lifecycle) const SEED = 42 >>> 0; const INPUT_X = 0.25; @@ -101,6 +101,17 @@ function bind(Module) { getWeights: cwrap('nisps_ml_get_weights', null, ['number','number']), drawWeights: cwrap('nisps_ml_draw_weights', null, ['number','number']), moveWeights: cwrap('nisps_ml_move_weights', null, ['number','number','number','number']), + feedbackSetMode: cwrap('nisps_ml_feedback_set_mode', null, ['number','number']), + feedbackDown: cwrap('nisps_ml_feedback_down', 'number', ['number','number','number','number','number']), + feedbackUp: cwrap('nisps_ml_feedback_up', 'number', ['number']), + feedbackStaticOutput: cwrap('nisps_ml_feedback_static_output', 'number', ['number','number']), + feedbackEnterExplore: cwrap('nisps_ml_feedback_enter_explore', null, ['number','number']), + feedbackReroll: cwrap('nisps_ml_feedback_reroll', null, ['number','number']), + feedbackNudge: cwrap('nisps_ml_feedback_nudge', null, ['number','number']), + feedbackUndo: cwrap('nisps_ml_feedback_undo', null, ['number']), + feedbackLike: cwrap('nisps_ml_feedback_like', null, ['number']), + feedbackCommitPlace: cwrap('nisps_ml_feedback_commit_place', null, ['number']), + feedbackPlacedOutput: cwrap('nisps_ml_feedback_placed_output', 'number', ['number','number']), describe: cwrap('nisps_ml_describe', null, ['number']), engineCreate: cwrap('nisps_engine_create', 'number', ['string','number']), @@ -235,7 +246,7 @@ async function main() { api.process(ml); const outsStage2 = getOutputsCopy(api, ml, N_OUT); - api.destroy(ml); + // (ml stays alive through stage 5 below; destroyed after the feedback stage.) // --- Stage 3: PAFSynth --- // PAFSynth has 33 params per param_count() in nisps/engines/paf_synth.hpp. @@ -244,6 +255,72 @@ async function main() { // --- Stage 4: ChannelStrip (24 params) --- const [csL, csR] = runEngine(api, 'channel_strip', 24, 0.25, SYNTH_FRAMES); + // --- Stage 5: feedback ("Down Action": RandomiseOutputs + RandomiseMlp) --- + // Mirrors parity_check.cpp stage 5. The controller is seeded inside the WASM + // MLHandle as (seed XOR salt), matching the native side. ml is untouched by + // stages 3-4, so its RNG state here equals post-stage-2. + const FB_RANDOUT = 1; + const FB_RANDMLP = 2; + const feedbackFloats = []; + const fbBuf = api.malloc(N_OUT * 4); + api.feedbackSetMode(ml, FB_RANDOUT); + api.feedbackDown(ml, 0, 0.1, 0.5, 0); // enter + api.feedbackStaticOutput(ml, fbBuf); + for (const v of new Float32Array(api.HEAPF32.buffer, fbBuf, N_OUT)) feedbackFloats.push(v); + api.feedbackDown(ml, 0, 0.1, 0.5, 0); // re-roll + api.feedbackStaticOutput(ml, fbBuf); + for (const v of new Float32Array(api.HEAPF32.buffer, fbBuf, N_OUT)) feedbackFloats.push(v); + api.free(fbBuf); + api.feedbackUp(ml); // commit (no weight change) + + api.feedbackSetMode(ml, FB_RANDMLP); + api.feedbackDown(ml, 0, 0.1, 0.5, 0); // enter → randomise temp net + { + const tempW = getWeightsCopy(api, ml); + for (const idx of PROBE_IDX) feedbackFloats.push(idx < tempW.length ? tempW[idx] : 0); + } + api.feedbackUp(ml); // commit → restore original net + { + const restoredW = getWeightsCopy(api, ml); + for (const idx of PROBE_IDX) feedbackFloats.push(idx < restoredW.length ? restoredW[idx] : 0); + } + + // --- Stage 5d: ExploreAndPlace lifecycle --- + // Reuses the single MLHandle.feedback controller (mode → ExploreAndPlace) so + // its RNG state matches native `fb` (both drained identical RandomiseOutputs + // draws). enter → reroll → nudge → undo → place → commit. + const FB_EXPLORE_PLACE = 3; + api.feedbackSetMode(ml, FB_EXPLORE_PLACE); + api.feedbackEnterExplore(ml, 0.5); // snapshot + randomise scratchpad + api.feedbackReroll(ml, 0.5); // scratchpad op + api.feedbackNudge(ml, 0.05); // controller-Rng perturb + { + const scratchW = getWeightsCopy(api, ml); + for (const idx of PROBE_IDX) feedbackFloats.push(idx < scratchW.length ? scratchW[idx] : 0); + } + api.feedbackUndo(ml); // pop nudge + api.setInput(ml, 0, INPUT_X); + api.setInput(ml, 1, INPUT_Y); + api.process(ml); + api.feedbackLike(ml); // begin place: freeze scratchpad output + { + const placedBuf = api.malloc(N_OUT * 4); + api.feedbackPlacedOutput(ml, placedBuf); + for (const v of new Float32Array(api.HEAPF32.buffer, placedBuf, N_OUT)) feedbackFloats.push(v); + api.free(placedBuf); + } + api.feedbackCommitPlace(ml); // restore real net + { + const restoredW = getWeightsCopy(api, ml); + for (const idx of PROBE_IDX) feedbackFloats.push(idx < restoredW.length ? restoredW[idx] : 0); + const committedBuf = api.malloc(N_OUT * 4); + api.feedbackPlacedOutput(ml, committedBuf); + for (const v of new Float32Array(api.HEAPF32.buffer, committedBuf, N_OUT)) feedbackFloats.push(v); + api.free(committedBuf); + } + + api.destroy(ml); + // --- Build payload, write blob --- const payload = []; for (const v of outsStage1) payload.push(v); @@ -252,6 +329,7 @@ async function main() { payload.push(finalLoss); payload.push(pafL, pafR); payload.push(csL, csR); + for (const v of feedbackFloats) payload.push(v); // Sanity: all finite. for (let i = 0; i < payload.length; ++i) { diff --git a/tests/cpp/test_mlp_feedback.cpp b/tests/cpp/test_mlp_feedback.cpp new file mode 100644 index 0000000..2a64195 --- /dev/null +++ b/tests/cpp/test_mlp_feedback.cpp @@ -0,0 +1,685 @@ +// tests/cpp/test_mlp_feedback.cpp — verify the FeedbackController "Down Action" +// state machine: Avoid / RandomiseOutputs / RandomiseMlp. +// +// Mirrors test_mlp_rl.cpp conventions (SmallMLP, snapshot-and-compare). The +// controller owns no MLP; every mutating call passes the MLP by reference. + +#include +#include +#include +#include + +#include "test_helpers.hpp" + +#include "../../nisps/ml/feedback.hpp" +#include "../../nisps/ml/mlp.hpp" + +namespace { + +using SmallMLP = nisps::ml::MLP<2, 4, 4, 4, 6, 8, 32>; +using FB = nisps::ml::FeedbackController; +using nisps::ml::FeedbackAction; +using nisps::ml::FeedbackMode; + +constexpr std::size_t kNOut = SmallMLP::kOutput; // 6 +constexpr std::size_t kW = SmallMLP::weight_count(); + +// Empty spans for the "don't care" args. +const std::span kNoOut{}; +const std::span kNoMask{}; + +std::array snapshot_weights(SmallMLP& m) { + std::array out{}; + auto w = m.get_weights(); + for (std::size_t i = 0; i < w.size(); ++i) out[i] = w[i]; + return out; +} + +int distinct(const std::array& a, const std::array& b) { + int d = 0; + for (std::size_t i = 0; i < kW; ++i) { + if (a[i] != b[i]) ++d; + } + return d; +} + +bool weights_equal(SmallMLP& m, const std::array& ref) { + auto w = m.get_weights(); + for (std::size_t i = 0; i < w.size(); ++i) { + if (w[i] != ref[i]) return false; + } + return true; +} + +// -- Avoid ------------------------------------------------------------------ + +NISPS_TEST(feedback_avoid_routes_to_move_weights) { + SmallMLP m(99ull); + m.draw_weights(0.5f); + FB fb(7ull); // default mode is Avoid + + const auto before = snapshot_weights(m); + const FeedbackAction a = fb.on_down(m, kNoOut, 0.1f, 0.5f, kNoMask); + const auto after = snapshot_weights(m); + + NISPS_EXPECT(a == FeedbackAction::AvoidPerturb); + NISPS_EXPECT(distinct(before, after) > static_cast(kW) / 2); // perturbed + NISPS_EXPECT(!fb.exploring()); + NISPS_EXPECT(!fb.learning_paused()); +} + +// -- RandomiseOutputs ------------------------------------------------------- + +NISPS_TEST(feedback_randout_enter_roll_state) { + SmallMLP m(0ull); + FB fb(123ull); + fb.set_mode(FeedbackMode::RandomiseOutputs, m); + + std::array cur{}; + for (auto& v : cur) v = 0.5f; + + const auto before_w = snapshot_weights(m); + const FeedbackAction a = + fb.on_down(m, std::span(cur), 0.1f, 0.5f, kNoMask); + + NISPS_EXPECT(a == FeedbackAction::EnterExplore); + NISPS_EXPECT(fb.exploring()); + NISPS_EXPECT(fb.learning_paused()); + + std::array buf{}; + const bool bypass = fb.static_output(std::span(buf)); + NISPS_EXPECT(bypass); // MLP bypassed while exploring + + int changed = 0; + for (std::size_t i = 0; i < kNOut; ++i) { + if (buf[i] != 0.5f) ++changed; + } + NISPS_EXPECT(changed == static_cast(kNOut)); // every dim rolled (no mask) + + // RandomiseOutputs must NOT touch the network weights. + NISPS_EXPECT(weights_equal(m, before_w)); +} + +NISPS_TEST(feedback_randout_reroll_changes_focused_only) { + SmallMLP m(0ull); + FB fb(55ull); + fb.set_mode(FeedbackMode::RandomiseOutputs, m); + + // Focus mask: roll dims 0,2,4; freeze dims 1,3,5. + std::array mask{1, 0, 1, 0, 1, 0}; + fb.set_focus_mask(std::span(mask)); + + const std::array seed{0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f}; + fb.on_down(m, std::span(seed), 0.1f, 0.5f, kNoMask); // enter + first roll + + std::array a{}; + fb.static_output(std::span(a)); + + fb.on_down(m, kNoOut, 0.1f, 0.5f, kNoMask); // re-roll + std::array b{}; + fb.static_output(std::span(b)); + + // Unfocused dims frozen at their seed value across both rolls. + for (std::size_t i : {1u, 3u, 5u}) { + NISPS_EXPECT(a[i] == seed[i]); + NISPS_EXPECT(b[i] == seed[i]); + } + // Focused dims re-rolled → at least 2 of 3 differ between the two rolls. + int focused_changed = 0; + for (std::size_t i : {0u, 2u, 4u}) { + if (a[i] != b[i]) ++focused_changed; + } + NISPS_EXPECT(focused_changed == 3); // all focused dims re-rolled +} + +NISPS_TEST(feedback_randout_commit_clears_state) { + SmallMLP m(0ull); + m.draw_weights(0.5f); + FB fb(9ull); + fb.set_mode(FeedbackMode::RandomiseOutputs, m); + + const auto w0 = snapshot_weights(m); + std::array cur{}; + for (auto& v : cur) v = 0.5f; + fb.on_down(m, std::span(cur), 0.1f, 0.5f, kNoMask); // enter + + const FeedbackAction a = fb.on_up(m); // keep + NISPS_EXPECT(a == FeedbackAction::CommitStore); + NISPS_EXPECT(!fb.exploring()); + NISPS_EXPECT(!fb.learning_paused()); + + std::array buf{}; + NISPS_EXPECT(!fb.static_output(std::span(buf))); // no longer bypassing + NISPS_EXPECT(weights_equal(m, w0)); // net untouched +} + +// -- RandomiseMlp ----------------------------------------------------------- + +NISPS_TEST(feedback_randmlp_snapshot_and_cancel_restores) { + SmallMLP m(0ull); + m.draw_weights(0.5f); + FB fb(0ull); + fb.set_mode(FeedbackMode::RandomiseMlp, m); + + const auto w0 = snapshot_weights(m); + const FeedbackAction enter = fb.on_down(m, kNoOut, 0.1f, 0.5f, kNoMask); + NISPS_EXPECT(enter == FeedbackAction::EnterExplore); + NISPS_EXPECT(fb.exploring()); + NISPS_EXPECT(!weights_equal(m, w0)); // live net randomised + + const FeedbackAction cancel = fb.on_down(m, kNoOut, 0.1f, 0.5f, kNoMask); + NISPS_EXPECT(cancel == FeedbackAction::Cancel); + NISPS_EXPECT(!fb.exploring()); + NISPS_EXPECT(weights_equal(m, w0)); // original net byte-restored +} + +NISPS_TEST(feedback_randmlp_commit_restores_then_caller_trains) { + SmallMLP m(0ull); + m.draw_weights(0.5f); + FB fb(0ull); + fb.set_mode(FeedbackMode::RandomiseMlp, m); + + const auto w0 = snapshot_weights(m); + fb.on_down(m, kNoOut, 0.1f, 0.5f, kNoMask); // enter (randomise) + + const FeedbackAction a = fb.on_up(m); // keep + NISPS_EXPECT(a == FeedbackAction::CommitStore); + NISPS_EXPECT(!fb.exploring()); + NISPS_EXPECT(weights_equal(m, w0)); // net restored; kept example trains it later +} + +NISPS_TEST(feedback_randmlp_drag_repositions) { + SmallMLP m(0ull); + m.draw_weights(0.5f); + FB fb(0ull); + fb.set_mode(FeedbackMode::RandomiseMlp, m); + + const auto w0 = snapshot_weights(m); + fb.on_down(m, kNoOut, 0.1f, 0.5f, kNoMask); // enter + + const FeedbackAction a = fb.on_drag(m); + NISPS_EXPECT(a == FeedbackAction::Restore); + NISPS_EXPECT(!fb.exploring()); + NISPS_EXPECT(weights_equal(m, w0)); +} + +NISPS_TEST(feedback_mode_switch_aborts_explore) { + SmallMLP m(0ull); + m.draw_weights(0.5f); + FB fb(0ull); + fb.set_mode(FeedbackMode::RandomiseMlp, m); + + const auto w0 = snapshot_weights(m); + fb.on_down(m, kNoOut, 0.1f, 0.5f, kNoMask); // enter (randomise) + NISPS_EXPECT(!weights_equal(m, w0)); + + fb.set_mode(FeedbackMode::Avoid, m); // switching aborts → restore + resume + NISPS_EXPECT(fb.mode() == FeedbackMode::Avoid); + NISPS_EXPECT(!fb.exploring()); + NISPS_EXPECT(!fb.learning_paused()); + NISPS_EXPECT(weights_equal(m, w0)); +} + +// -- determinism + bypass invariants --------------------------------------- + +NISPS_TEST(feedback_determinism_fixed_seed) { + // Same controller seed + same press sequence → byte-identical static output, + // proving the per-instance Rng (no libc rand()). + SmallMLP m1(0ull), m2(0ull); + FB a(42ull), b(42ull); + a.set_mode(FeedbackMode::RandomiseOutputs, m1); + b.set_mode(FeedbackMode::RandomiseOutputs, m2); + + std::array cur{}; + for (auto& v : cur) v = 0.25f; + + a.on_down(m1, std::span(cur), 0.1f, 0.5f, kNoMask); // enter + b.on_down(m2, std::span(cur), 0.1f, 0.5f, kNoMask); + a.on_down(m1, kNoOut, 0.1f, 0.5f, kNoMask); // reroll + b.on_down(m2, kNoOut, 0.1f, 0.5f, kNoMask); + + std::array ba{}, bb{}; + a.static_output(std::span(ba)); + b.static_output(std::span(bb)); + for (std::size_t i = 0; i < kNOut; ++i) NISPS_EXPECT(ba[i] == bb[i]); +} + +NISPS_TEST(feedback_focus_empty_means_all_active) { + SmallMLP m(0ull); + FB fb(1ull); + fb.set_mode(FeedbackMode::RandomiseOutputs, m); // no focus mask set + + std::array cur{}; + for (auto& v : cur) v = 0.5f; + fb.on_down(m, std::span(cur), 0.1f, 0.5f, kNoMask); + + std::array buf{}; + fb.static_output(std::span(buf)); + int changed = 0; + for (std::size_t i = 0; i < kNOut; ++i) { + if (buf[i] != 0.5f) ++changed; + } + NISPS_EXPECT(changed == static_cast(kNOut)); // all dims rolled +} + +NISPS_TEST(feedback_static_output_bypass_only_in_randout) { + SmallMLP m(0ull); + std::array buf{}; + + FB avoid(0ull); // Avoid, not exploring + NISPS_EXPECT(!avoid.static_output(std::span(buf))); + + FB ro(0ull); + ro.set_mode(FeedbackMode::RandomiseOutputs, m); // mode set but NOT entered + NISPS_EXPECT(!ro.static_output(std::span(buf))); + + std::array cur{}; + ro.on_down(m, std::span(cur), 0.1f, 0.5f, kNoMask); // now exploring + NISPS_EXPECT(ro.static_output(std::span(buf))); +} + +// -- LikeStore / non-exploring fallbacks ------------------------------------ + +NISPS_TEST(feedback_avoid_up_is_like_store) { + SmallMLP m(0ull); + FB fb(0ull); // Avoid + NISPS_EXPECT(fb.on_up(m) == FeedbackAction::LikeStore); + NISPS_EXPECT(!fb.exploring()); +} + +NISPS_TEST(feedback_idle_up_is_like_store) { + SmallMLP m(0ull); + m.draw_weights(0.5f); + FB fb(0ull); + fb.set_mode(FeedbackMode::RandomiseOutputs, m); // mode set, never entered + const auto w0 = snapshot_weights(m); + NISPS_EXPECT(fb.on_up(m) == FeedbackAction::LikeStore); + NISPS_EXPECT(weights_equal(m, w0)); // idle up touches nothing +} + +NISPS_TEST(feedback_drag_non_explore_is_like_store) { + SmallMLP m(0ull); + FB fb(0ull); // Avoid, not exploring + NISPS_EXPECT(fb.on_drag(m) == FeedbackAction::LikeStore); +} + +NISPS_TEST(feedback_commit_without_enter_is_like_store) { + SmallMLP m(0ull); + m.draw_weights(0.5f); + FB fb(0ull); + fb.set_mode(FeedbackMode::RandomiseMlp, m); // never entered + const auto w0 = snapshot_weights(m); + NISPS_EXPECT(fb.on_up(m) == FeedbackAction::LikeStore); + NISPS_EXPECT(fb.on_drag(m) == FeedbackAction::LikeStore); + NISPS_EXPECT(weights_equal(m, w0)); // no enter → nothing restored/mutated +} + +// -- drag must NOT cancel a RandomiseOutputs exploration (firmware semantics) - + +NISPS_TEST(feedback_randout_drag_stays_in_explore) { + SmallMLP m(0ull); + FB fb(0ull); + fb.set_mode(FeedbackMode::RandomiseOutputs, m); + std::array cur{}; + for (auto& v : cur) v = 0.5f; + fb.on_down(m, std::span(cur), 0.1f, 0.5f, kNoMask); // enter + NISPS_EXPECT(fb.on_drag(m) == FeedbackAction::LikeStore); // plain store + NISPS_EXPECT(fb.exploring()); // exploration CONTINUES + NISPS_EXPECT(fb.learning_paused()); + std::array buf{}; + NISPS_EXPECT(fb.static_output(std::span(buf))); // still bypassing +} + +// -- fidelity: unfocused dims freeze at the live (current_out) value --------- + +NISPS_TEST(feedback_randout_unfocused_freezes_at_current_out) { + SmallMLP m(0ull); + FB fb(3ull); + fb.set_mode(FeedbackMode::RandomiseOutputs, m); + std::array mask{1, 0, 1, 0, 1, 0}; // freeze 1,3,5 + fb.set_focus_mask(std::span(mask)); + const std::array cur{0.11f, 0.22f, 0.33f, 0.44f, 0.55f, 0.66f}; + fb.on_down(m, std::span(cur), 0.1f, 0.5f, kNoMask); // enter + std::array buf{}; + fb.static_output(std::span(buf)); + NISPS_EXPECT(buf[1] == cur[1]); // frozen at the live value, not 0 or random + NISPS_EXPECT(buf[3] == cur[3]); + NISPS_EXPECT(buf[5] == cur[5]); + int focused_changed = 0; + for (std::size_t i : {0u, 2u, 4u}) { + if (buf[i] != cur[i]) ++focused_changed; + } + NISPS_EXPECT(focused_changed == 3); +} + +// -- golden RNG stream: catches an RNG/seed-mix regression even if symmetric -- + +NISPS_TEST(feedback_randout_golden_stream) { + SmallMLP m(0ull); + FB fb(42ull); + fb.set_mode(FeedbackMode::RandomiseOutputs, m); + std::array cur{}; // no mask → all dims rolled from the FB RNG + fb.on_down(m, std::span(cur), 0.1f, 0.5f, kNoMask); // enter + std::array buf{}; + fb.static_output(std::span(buf)); + // GOLDEN (seed 42, first roll) — regenerate ONLY on an intentional RNG change. + NISPS_EXPECT_NEAR(buf[0], 0.0857555866f, 1e-6); + NISPS_EXPECT_NEAR(buf[1], 0.310411394f, 1e-6); + NISPS_EXPECT_NEAR(buf[2], 0.0625697374f, 1e-6); + NISPS_EXPECT_NEAR(buf[5], 0.3030653f, 1e-6); +} + +// -- RandomiseMlp: repeated enter/commit re-snapshots each cycle ------------- + +NISPS_TEST(feedback_randmlp_repeated_enter_commit_restores_each_time) { + SmallMLP m(0ull); + m.draw_weights(0.5f); + FB fb(0ull); + fb.set_mode(FeedbackMode::RandomiseMlp, m); + for (int cycle = 0; cycle < 3; ++cycle) { + const auto w0 = snapshot_weights(m); + fb.on_down(m, kNoOut, 0.1f, 0.5f, kNoMask); // enter → randomise temp net + NISPS_EXPECT(!weights_equal(m, w0)); + fb.on_up(m); // commit → restore + NISPS_EXPECT(weights_equal(m, w0)); // a stale snapshot would fail here + m.move_weights(0.05f, 0.5f); // mutate the "original" net for next cycle + } +} + +// -- focus mask edge cases --------------------------------------------------- + +NISPS_TEST(feedback_clear_focus_restores_all_active) { + SmallMLP m(0ull); + FB fb(8ull); + fb.set_mode(FeedbackMode::RandomiseOutputs, m); + std::array mask{1, 0, 0, 0, 0, 0}; + fb.set_focus_mask(std::span(mask)); + fb.clear_focus_mask(); // back to all-active + std::array cur{}; + for (auto& v : cur) v = 0.5f; + fb.on_down(m, std::span(cur), 0.1f, 0.5f, kNoMask); + std::array buf{}; + fb.static_output(std::span(buf)); + int changed = 0; + for (std::size_t i = 0; i < kNOut; ++i) { + if (buf[i] != cur[i]) ++changed; + } + NISPS_EXPECT(changed == static_cast(kNOut)); // all dims roll again +} + +NISPS_TEST(feedback_focus_mask_truncates_when_oversized) { + SmallMLP m(0ull); + FB fb(0ull); + fb.set_mode(FeedbackMode::RandomiseOutputs, m); + // Oversized mask (kNOut+4): set_focus_mask must clamp to kNOut, no overflow. + std::array mask{}; + for (auto& v : mask) v = 1; + fb.set_focus_mask(std::span(mask)); + std::array cur{}; + for (auto& v : cur) v = 0.5f; + fb.on_down(m, std::span(cur), 0.1f, 0.5f, kNoMask); // must not overflow + std::array buf{}; + NISPS_EXPECT(fb.static_output(std::span(buf))); +} + +NISPS_TEST(feedback_short_static_buffer_clamps) { + SmallMLP m(0ull); + FB fb(0ull); + fb.set_mode(FeedbackMode::RandomiseOutputs, m); + std::array cur{}; + fb.on_down(m, std::span(cur), 0.1f, 0.5f, kNoMask); // enter + // Caller passes a buffer SHORTER than kNOut — must fill only that many. + std::array small{-1.f, -1.f, -1.f}; + NISPS_EXPECT(fb.static_output(std::span(small))); + for (float v : small) NISPS_EXPECT(v != -1.f); // all 3 written, no overflow +} + +// =========================================================================== +// ExploreAndPlace — the Idle → Exploring → Placing → Idle lifecycle. +// =========================================================================== + +using nisps::ml::ExploreState; + +NISPS_TEST(ep_enter_explore_snapshots_and_randomises) { + SmallMLP m(0ull); + m.draw_weights(0.5f); + FB fb(0ull); + fb.set_mode(FeedbackMode::ExploreAndPlace, m); + + const auto real = snapshot_weights(m); + NISPS_EXPECT(fb.explore_state() == ExploreState::Idle); + NISPS_EXPECT(!fb.exploring()); + + fb.enter_explore(m, 0.5f); + NISPS_EXPECT(fb.explore_state() == ExploreState::Exploring); + NISPS_EXPECT(fb.exploring()); + NISPS_EXPECT(fb.learning_paused()); + NISPS_EXPECT(!weights_equal(m, real)); // scratchpad net is live + + fb.exit_explore(m); // back out → real net restored + NISPS_EXPECT(fb.explore_state() == ExploreState::Idle); + NISPS_EXPECT(!fb.learning_paused()); + NISPS_EXPECT(weights_equal(m, real)); +} + +NISPS_TEST(ep_reroll_changes_scratchpad_undo_restores) { + SmallMLP m(0ull); + m.draw_weights(0.5f); + FB fb(7ull); + fb.set_mode(FeedbackMode::ExploreAndPlace, m); + fb.enter_explore(m, 0.5f); + + const auto cand0 = snapshot_weights(m); + fb.reroll(m, 0.5f); + const auto cand1 = snapshot_weights(m); + NISPS_EXPECT(distinct(cand0, cand1) > 0); // re-rolled + NISPS_EXPECT(fb.undo_depth() == 1u); + + fb.undo(m); // back to cand0 + NISPS_EXPECT(weights_equal(m, cand0)); + NISPS_EXPECT(fb.undo_depth() == 0u); +} + +NISPS_TEST(ep_nudge_is_bounded_and_undoable) { + SmallMLP m(0ull); + m.draw_weights(0.5f); + FB fb(3ull); + fb.set_mode(FeedbackMode::ExploreAndPlace, m); + fb.enter_explore(m, 0.5f); + + const auto before = snapshot_weights(m); + fb.nudge(m, 0.05f); + const auto after = snapshot_weights(m); + NISPS_EXPECT(distinct(before, after) > 0); // perturbed + // Bounded: small stddev → deltas stay modest. + float max_delta = 0.f; + for (std::size_t i = 0; i < kW; ++i) { + const float d = after[i] - before[i]; + const float ad = d < 0.f ? -d : d; + if (ad > max_delta) max_delta = ad; + } + NISPS_EXPECT(max_delta < 1.0f); // nudge, not a re-roll + + fb.undo(m); + NISPS_EXPECT(weights_equal(m, before)); +} + +NISPS_TEST(ep_undo_ring_bounded_to_depth) { + // Default UndoDepth = 4. After 6 rerolls, only 4 undos are available. + SmallMLP m(0ull); + FB fb(11ull); + fb.set_mode(FeedbackMode::ExploreAndPlace, m); + fb.enter_explore(m, 0.5f); + for (int i = 0; i < 6; ++i) fb.reroll(m, 0.5f); + NISPS_EXPECT(fb.undo_depth() == FB::kUndoDepth); // saturated at depth + for (std::size_t i = 0; i < FB::kUndoDepth; ++i) fb.undo(m); + NISPS_EXPECT(fb.undo_depth() == 0u); + fb.undo(m); // extra undo is a no-op + NISPS_EXPECT(fb.undo_depth() == 0u); +} + +NISPS_TEST(ep_place_freezes_output_and_holds_via_static) { + SmallMLP m(0ull); + m.draw_weights(0.5f); + FB fb(0ull); + fb.set_mode(FeedbackMode::ExploreAndPlace, m); + fb.enter_explore(m, 0.5f); + + // Audition at a fixed input, then begin_place freezes that output. + m.set_input(0u, 0.3f); + m.set_input(1u, 0.7f); + m.process(); + std::array auditioned{}; + { + auto o = m.outputs(); + for (std::size_t i = 0; i < kNOut; ++i) auditioned[i] = o[i]; + } + + fb.begin_place(m); // convenience overload: process + capture + NISPS_EXPECT(fb.placing()); + NISPS_EXPECT(fb.explore_state() == ExploreState::Placing); + + auto placed = fb.placed_output(); + NISPS_EXPECT(placed.size() == kNOut); + for (std::size_t i = 0; i < kNOut; ++i) NISPS_EXPECT(placed[i] == auditioned[i]); + + // While placing, static_output holds the frozen vector regardless of input. + std::array buf{}; + NISPS_EXPECT(fb.static_output(std::span(buf))); + for (std::size_t i = 0; i < kNOut; ++i) NISPS_EXPECT(buf[i] == auditioned[i]); +} + +NISPS_TEST(ep_commit_place_restores_real_net_exposes_committed) { + SmallMLP m(0ull); + m.draw_weights(0.5f); + FB fb(0ull); + fb.set_mode(FeedbackMode::ExploreAndPlace, m); + + const auto real = snapshot_weights(m); + fb.enter_explore(m, 0.5f); + fb.begin_place(m); + std::array frozen{}; + { + auto p = fb.placed_output(); + for (std::size_t i = 0; i < kNOut; ++i) frozen[i] = p[i]; + } + + fb.commit_place(m); + NISPS_EXPECT(fb.explore_state() == ExploreState::Idle); + NISPS_EXPECT(!fb.learning_paused()); + NISPS_EXPECT(weights_equal(m, real)); // real net restored + + // The caller reads the committed vector AFTER restore to add the +1 example. + auto committed = fb.committed_output(); + NISPS_EXPECT(committed.size() == kNOut); + for (std::size_t i = 0; i < kNOut; ++i) NISPS_EXPECT(committed[i] == frozen[i]); +} + +NISPS_TEST(ep_cancel_place_returns_to_exploring) { + SmallMLP m(0ull); + m.draw_weights(0.5f); + FB fb(0ull); + fb.set_mode(FeedbackMode::ExploreAndPlace, m); + fb.enter_explore(m, 0.5f); + const auto scratch = snapshot_weights(m); + fb.begin_place(m); + NISPS_EXPECT(fb.placing()); + + fb.cancel_place(); + NISPS_EXPECT(fb.explore_state() == ExploreState::Exploring); + NISPS_EXPECT(fb.exploring()); + NISPS_EXPECT(weights_equal(m, scratch)); // scratchpad untouched +} + +NISPS_TEST(ep_software_policy_down_up_drives_machine) { + // on_down / on_up are the BROWSER default policy over the same machine. + SmallMLP m(0ull); + m.draw_weights(0.5f); + FB fb(0ull); + fb.set_mode(FeedbackMode::ExploreAndPlace, m); + const auto real = snapshot_weights(m); + + NISPS_EXPECT(fb.on_down(m, kNoOut, 0.1f, 0.5f, kNoMask) == FeedbackAction::EnterExplore); + NISPS_EXPECT(fb.explore_state() == ExploreState::Exploring); + + NISPS_EXPECT(fb.on_down(m, kNoOut, 0.1f, 0.5f, kNoMask) == FeedbackAction::ScratchReroll); + NISPS_EXPECT(fb.explore_state() == ExploreState::Exploring); + + NISPS_EXPECT(fb.on_up(m) == FeedbackAction::BeginPlace); + NISPS_EXPECT(fb.placing()); + + NISPS_EXPECT(fb.on_up(m) == FeedbackAction::CommitPlace); + NISPS_EXPECT(fb.explore_state() == ExploreState::Idle); + NISPS_EXPECT(weights_equal(m, real)); + + // Down while placing backs out to exploring. + fb.on_down(m, kNoOut, 0.1f, 0.5f, kNoMask); // enter + fb.on_up(m); // begin place + NISPS_EXPECT(fb.placing()); + NISPS_EXPECT(fb.on_down(m, kNoOut, 0.1f, 0.5f, kNoMask) == FeedbackAction::CancelPlace); + NISPS_EXPECT(fb.explore_state() == ExploreState::Exploring); +} + +NISPS_TEST(ep_mode_switch_aborts_session_restores) { + SmallMLP m(0ull); + m.draw_weights(0.5f); + FB fb(0ull); + fb.set_mode(FeedbackMode::ExploreAndPlace, m); + const auto real = snapshot_weights(m); + fb.enter_explore(m, 0.5f); + fb.begin_place(m); + NISPS_EXPECT(fb.placing()); + + fb.set_mode(FeedbackMode::Avoid, m); // switching aborts + NISPS_EXPECT(fb.mode() == FeedbackMode::Avoid); + NISPS_EXPECT(fb.explore_state() == ExploreState::Idle); + NISPS_EXPECT(!fb.learning_paused()); + NISPS_EXPECT(weights_equal(m, real)); +} + +NISPS_TEST(ep_determinism_fixed_seed) { + // Same seed + same op sequence → byte-identical scratchpad nudges, proving + // the per-instance Rng (the nudge draws from the controller's own stream). + SmallMLP m1(0ull), m2(0ull); + m1.draw_weights(0.5f); m2.draw_weights(0.5f); + FB a(42ull), b(42ull); + a.set_mode(FeedbackMode::ExploreAndPlace, m1); + b.set_mode(FeedbackMode::ExploreAndPlace, m2); + a.enter_explore(m1, 0.5f); + b.enter_explore(m2, 0.5f); + a.nudge(m1, 0.05f); + b.nudge(m2, 0.05f); + auto wa = snapshot_weights(m1); + auto wb = snapshot_weights(m2); + for (std::size_t i = 0; i < kW; ++i) NISPS_EXPECT(wa[i] == wb[i]); +} + +NISPS_TEST(ep_full_flow_two_anchors_caller_trains) { + // End-to-end: explore→place→commit twice, with the CALLER doing the + // add_example + train (the contract). After warm-start the net should bend + // toward both placed anchors. + SmallMLP m(0ull); + m.draw_weights(0.5f); + FB fb(5ull); + fb.set_mode(FeedbackMode::ExploreAndPlace, m); + + const std::array, 2> inputs{{{{0.1f, 0.1f}}, {{0.9f, 0.9f}}}}; + for (int anchor = 0; anchor < 2; ++anchor) { + fb.enter_explore(m, 0.5f); + // audition at the chosen input + m.set_input(0u, inputs[anchor][0]); + m.set_input(1u, inputs[anchor][1]); + m.process(); + fb.begin_place(m); + fb.commit_place(m); // restores real net + // CALLER stores the +1 example (input → committed output) and trains. + auto out = fb.committed_output(); + NISPS_EXPECT(out.size() == kNOut); + m.add_example(std::span(inputs[anchor].data(), 2u), out); + } + const float loss = m.train(0.3f, 200u, 0.0f); + NISPS_EXPECT(loss >= 0.f); // trained, finite + NISPS_EXPECT(m.example_count() == 2u); +} + +} // namespace