From 45193a2c01418b2dcec9fb2b19f17785ce15974d Mon Sep 17 00:00:00 2001 From: monkey-w1n5t0n Date: Sun, 8 Feb 2026 18:01:48 +0100 Subject: [PATCH] fix: audit and fix nisps-core extraction issues - Remove platform-specific code (ARM_MATH_CM33, XMOS __XS3A__, std::printf) - Add set_output()/set_outputs()/add_example() API for programmatic training - Fix release build crash: side effect inside assert() for loss function init - Replace fake smoke test with real convergence tests (5 tests, all pass) - Rewrite example to demonstrate actual training with real output - Update README, CHANGELOG, and extraction plan to match reality --- NISPS_CORE_EXTRACTION_PLAN.md | 4 +- nisps-core/CHANGELOG.md | 42 ++-- nisps-core/README.md | 62 ++--- nisps-core/examples/simple_mapping.cpp | 198 +++++++++------- nisps-core/include/nisps/dataset_impl.hpp | 14 +- nisps-core/include/nisps/iml.hpp | 5 + nisps-core/include/nisps/iml_impl.hpp | 24 ++ nisps-core/include/nisps/loss.hpp | 9 - nisps-core/include/nisps/mlp_impl.hpp | 4 +- nisps-core/include/nisps/node.hpp | 15 -- nisps-core/include/nisps/utils.hpp | 5 - nisps-core/test/main.cpp | 277 +++++++++++++++++----- 12 files changed, 426 insertions(+), 233 deletions(-) diff --git a/NISPS_CORE_EXTRACTION_PLAN.md b/NISPS_CORE_EXTRACTION_PLAN.md index e75f83b..ff16da8 100644 --- a/NISPS_CORE_EXTRACTION_PLAN.md +++ b/NISPS_CORE_EXTRACTION_PLAN.md @@ -1,6 +1,8 @@ # NISPS Core Extraction Plan -Extract a platform-agnostic C++17 controller library from MEMLNaut-NISPS. This is **not** a synth or audio engine - it's a parameter mapping engine: control data in → ML → control data out. Use it to drive synths, effects, lights, robots, whatever. +Extract a platform-agnostic C++20 controller library from MEMLNaut-NISPS. This is **not** a synth or audio engine - it's a parameter mapping engine: control data in → ML → control data out. Use it to drive synths, effects, lights, robots, whatever. + +> **Note**: Originally planned as C++17, upgraded to C++20 during implementation to use `std::span` for efficient array views. ## What This Is diff --git a/nisps-core/CHANGELOG.md b/nisps-core/CHANGELOG.md index 8ef6224..9c4b351 100644 --- a/nisps-core/CHANGELOG.md +++ b/nisps-core/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to NISPS Core will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [0.2.0] - 2026-02-08 + +### Added +- `set_output()` / `set_outputs()` methods for programmatic output control +- `add_example()` method for adding training pairs without interactive workflow +- Real training convergence tests (identity mapping, multi-output) +- Working example with actual training (`examples/simple_mapping.cpp`) + +### Fixed +- Release build crash: loss function pointer not initialized due to side effect inside `assert()` (mlp_impl.hpp) +- Removed ARM CMSIS-DSP conditional code from node.hpp (`ARM_MATH_CM33`) +- Removed XMOS `__XS3A__` conditional attributes from utils.hpp and loss.hpp +- Removed `std::printf` logging from dataset_impl.hpp (use IML logger callback instead) + +### Changed +- README updated to document new APIs and remove false claims +- CHANGELOG rewritten to accurately reflect library state + ## [0.1.0] - 2026-02-08 ### Added @@ -15,39 +33,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Training and inference modes - Logging callback support - CMake build system for tests -- Basic XOR test example -- Comprehensive README documentation ### Changed - Converted from Arduino/RP2040 embedded code to platform-agnostic C++ -- Updated from C++17 to C++20 (required for std::span) -- Removed all platform-specific code (Serial, SD card, Pico SDK) +- Updated to C++20 (required for std::span) - Converted to header-only implementation pattern - Added `nisps` namespace to all code -- Changed file extensions from .h/.cpp to .hpp ### Removed - Arduino and RP2040 dependencies - Serial debugging (replaced with optional callbacks) - SD card save/load functionality -- Binary serialization (temporarily disabled) - Audio synthesis code (nisps-core is control-only) -### Technical Details -- **Language**: C++20 -- **Dependencies**: None (pure standard library) -- **Architecture**: Header-only library -- **Lines of code**: ~3,500 -- **Build system**: CMake 3.14+ -- **Optimizer**: RMSProp with gradient clipping -- **Activation functions**: Sigmoid, ReLU, tanh, linear, hardsigmoid, hardswish, hardtanh -- **Loss functions**: MSE, categorical cross-entropy - -### Known Issues -- Binary serialization methods commented out (not needed for basic functionality) -- No example for actual training workflow yet (requires interactive I/O) -- Documentation references parent project URLs (MEMLNaut-NISPS) - ### Migration from MEMLNaut-NISPS If you're using the old embedded IMLInterface class: ```cpp @@ -57,5 +55,3 @@ IMLInterface iml(n_inputs, n_outputs); // New (nisps-core): nisps::IML iml(n_inputs, n_outputs); ``` - -All method names remain the same, just add the namespace. diff --git a/nisps-core/README.md b/nisps-core/README.md index 956bc4b..a6a2d2e 100644 --- a/nisps-core/README.md +++ b/nisps-core/README.md @@ -13,9 +13,9 @@ NISPS core is a **parameter mapping engine**, not a synthesizer. It takes N inpu ## Features - **Header-only**: No compilation needed, just include and use -- **Platform-agnostic**: Pure C++20, works anywhere -- **No dependencies**: Only standard library +- **Platform-agnostic**: Pure C++20 with standard library only - **Interactive learning**: Train by demonstration, not by code +- **Programmatic training**: `add_example()` API for non-interactive use - **Lightweight**: ~3,500 lines of optimized neural network code - **Flexible**: Map 1-100 inputs to 1-100 outputs @@ -55,25 +55,35 @@ void update(float x, float y) { } ``` -### Training Workflow +### Programmatic Training ```cpp // 1. Enter training mode iml.set_mode(nisps::IML::Mode::Training); -// 2. Set input position +// 2. Add examples directly +float in1[] = {0.1f, 0.1f}; float out1[] = {0.9f, 0.1f, 0.5f, 0.8f}; +float in2[] = {0.9f, 0.9f}; float out2[] = {0.1f, 0.9f, 0.2f, 0.3f}; +iml.add_example(in1, 2, out1, 4); +iml.add_example(in2, 2, out2, 4); + +// 3. Exit training mode (automatically trains the network) +iml.set_mode(nisps::IML::Mode::Inference); +``` + +### Interactive Training (hardware/UI) + +```cpp +// For interactive systems with physical controls: +iml.set_mode(nisps::IML::Mode::Training); + iml.set_input(0, 0.3f); iml.set_input(1, 0.7f); +iml.save_example(); // Stops inference +// ... user adjusts output controls ... +iml.set_output(0, 0.8f); // Or read from hardware +iml.save_example(); // Stores the input->output mapping -// 3. Save example (call twice per example) -iml.save_example(); // First call: stops inference, user positions output -// ... user adjusts outputs manually to desired values ... -iml.save_example(); // Second call: stores the input->output mapping - -// 4. Repeat for multiple input positions -// ... add more examples ... - -// 5. Exit training mode (automatically trains the network) iml.set_mode(nisps::IML::Mode::Inference); ``` @@ -97,6 +107,8 @@ nisps::IML( ```cpp void set_input(size_t index, Float value); // Set single input (0-1 range) void set_inputs(const Float* values, size_t count); // Set multiple inputs +void set_output(size_t index, Float value); // Set single output (for training) +void set_outputs(const Float* values, size_t count); // Set multiple outputs const Float* get_outputs() const; // Get output array void process(); // Run inference ``` @@ -106,7 +118,9 @@ void process(); // Run inference ```cpp enum class Mode { Inference, Training }; void set_mode(Mode mode); // Switch modes -void save_example(); // Store input->output pair +void add_example(const Float* in, size_t n_in, // Add training pair directly + const Float* out, size_t n_out); +void save_example(); // Interactive: store input->output pair void clear_dataset(); // Clear training data void randomise_weights(); // Randomize for exploration ``` @@ -152,20 +166,14 @@ ctest --output-on-failure 4. **RMSProp optimizer**: Fast convergence for interactive training 5. **Gradient clipping**: Prevents numerical instability -## Performance - -Typical performance on modern hardware: -- **Inference**: 1-10 µs for small networks (2-10-10-4) -- **Training**: 10-100 ms for 100 examples, 1000 iterations -- **Memory**: ~1 KB per hidden neuron - ## Examples -See `test/main.cpp` for a complete example. More examples coming soon: -- Audio synthesis control -- Game parameter mapping -- Sensor fusion for robotics -- MIDI controller mapping +See `examples/simple_mapping.cpp` for a complete working example that demonstrates: +- Untrained inference +- Programmatic training with `add_example()` +- Interactive training workflow with `save_example()` + `set_output()` + +See `test/main.cpp` for tests including convergence verification. ## Origin @@ -207,4 +215,4 @@ https://musicallyembodiedml.github.io/memlnaut/approaches/nisps - **Issues**: File at parent project (MEMLNaut-NISPS repo) - **Docs**: https://musicallyembodiedml.github.io/memlnaut/ -- **Examples**: See `examples/` directory (coming soon) +- **Examples**: See `examples/` directory diff --git a/nisps-core/examples/simple_mapping.cpp b/nisps-core/examples/simple_mapping.cpp index 3c0abc9..f1f4178 100644 --- a/nisps-core/examples/simple_mapping.cpp +++ b/nisps-core/examples/simple_mapping.cpp @@ -1,9 +1,9 @@ /** * @file simple_mapping.cpp - * @brief Simple example of using NISPS Core for parameter mapping + * @brief Example of using NISPS Core for parameter mapping * - * This example shows how to use NISPS Core to map 2D joystick input - * to synthesizer parameters through interactive training. + * Demonstrates creating a network, adding training examples + * programmatically, training, and using inference. * * Compile: g++ -std=c++20 -I../include simple_mapping.cpp -o simple_mapping */ @@ -12,119 +12,147 @@ #include #include -void print_separator() { - std::cout << "\n" << std::string(60, '=') << "\n\n"; -} - void demo_inference() { - std::cout << "=== NISPS Core Demo: Inference Mode ===\n\n"; + std::cout << "=== Demo 1: Untrained Inference ===\n\n"; - // Create IML with 2 inputs (x, y), 4 outputs (filter, resonance, attack, release) - // Hidden layers: [8, 8] - smaller network for faster training + // Create IML: 2 inputs (x, y) -> 4 outputs (filter, resonance, attack, release) nisps::IML iml(2, 4, {8, 8}, 2000, 0.5f, 0.0001f); - std::cout << "Created IML with:\n"; - std::cout << " Inputs: " << iml.num_inputs() << " (x, y joystick)\n"; - std::cout << " Outputs: " << iml.num_outputs() << " (filter, resonance, attack, release)\n"; - std::cout << " Hidden: [8, 8]\n"; + std::cout << "Created IML with " << iml.num_inputs() << " inputs, " + << iml.num_outputs() << " outputs\n\n"; - print_separator(); - - // Test some input positions - std::cout << "Testing inference (untrained network):\n\n"; - std::cout << std::fixed << std::setprecision(3); - - struct TestPoint { - float x, y; - const char* description; - }; - - TestPoint test_points[] = { - {0.0f, 0.0f, "Bottom-left corner"}, - {1.0f, 0.0f, "Bottom-right corner"}, - {0.0f, 1.0f, "Top-left corner"}, - {1.0f, 1.0f, "Top-right corner"}, + // Untrained network produces random-ish outputs + struct TestPoint { float x, y; const char* label; }; + TestPoint points[] = { + {0.0f, 0.0f, "Bottom-left"}, + {1.0f, 1.0f, "Top-right"}, {0.5f, 0.5f, "Center"}, }; - for (const auto& point : test_points) { - iml.set_input(0, point.x); - iml.set_input(1, point.y); + std::cout << std::fixed << std::setprecision(3); + for (const auto& p : points) { + iml.set_input(0, p.x); + iml.set_input(1, p.y); iml.process(); - - const float* outputs = iml.get_outputs(); - - std::cout << point.description << " (" << point.x << ", " << point.y << "):\n"; - std::cout << " Filter: " << outputs[0] << "\n"; - std::cout << " Resonance: " << outputs[1] << "\n"; - std::cout << " Attack: " << outputs[2] << "\n"; - std::cout << " Release: " << outputs[3] << "\n\n"; + const float* out = iml.get_outputs(); + std::cout << " " << p.label << " (" << p.x << ", " << p.y << ") -> [" + << out[0] << ", " << out[1] << ", " << out[2] << ", " << out[3] << "]\n"; } - - print_separator(); - std::cout << "Note: Untrained networks produce random-ish outputs.\n"; - std::cout << "In a real application, you would:\n"; - std::cout << " 1. Enter training mode\n"; - std::cout << " 2. Move joystick to various positions\n"; - std::cout << " 3. Adjust output parameters to desired values\n"; - std::cout << " 4. Call save_example() to store each mapping\n"; - std::cout << " 5. Exit training mode to train the network\n"; - std::cout << " 6. Use the trained network for real-time control\n"; + std::cout << "\n"; } void demo_training() { - std::cout << "\n=== NISPS Core Demo: Training Workflow ===\n\n"; + std::cout << "=== Demo 2: Training a Mapping ===\n\n"; - // Create a simple 2-input, 1-output network - nisps::IML iml(2, 1, {4}, 1000, 1.0f, 0.001f); - - // Set up logging + // 2 inputs -> 2 outputs, small network + nisps::IML iml(2, 2, {8, 8}, 3000, 1.0f, 0.00001f); iml.set_logger([](const char* msg) { - std::cout << "[IML] " << msg << "\n"; + std::cout << " [nisps] " << msg << "\n"; }); - std::cout << "Teaching the network: output = 1 when both inputs > 0.5\n"; - std::cout << "(Similar to AND gate, but with gradual transitions)\n\n"; + // Goal: teach the network a cross-mapping + // (low, low) -> (low output1, high output2) + // (high, high) -> (high output1, low output2) + std::cout << "Teaching cross-mapping:\n"; + std::cout << " (low, low) -> (low, high)\n"; + std::cout << " (high, high) -> (high, low)\n\n"; - // Enter training mode iml.set_mode(nisps::IML::Mode::Training); - // In a real interactive system, the user would: - // 1. Move joystick to a position - // 2. Call save_example() - this stops inference - // 3. Manually adjust output to desired value - // 4. Call save_example() again - this stores the mapping + // Add examples using the programmatic API + float in1[] = {0.1f, 0.1f}; float out1[] = {0.1f, 0.9f}; + float in2[] = {0.9f, 0.9f}; float out2[] = {0.9f, 0.1f}; + float in3[] = {0.5f, 0.5f}; float out3[] = {0.5f, 0.5f}; + float in4[] = {0.1f, 0.9f}; float out4[] = {0.3f, 0.7f}; + float in5[] = {0.9f, 0.1f}; float out5[] = {0.7f, 0.3f}; - // For this demo, we'll simulate the workflow by directly - // manipulating the dataset (this is not the normal API usage) + iml.add_example(in1, 2, out1, 2); + iml.add_example(in2, 2, out2, 2); + iml.add_example(in3, 2, out3, 2); + iml.add_example(in4, 2, out4, 2); + iml.add_example(in5, 2, out5, 2); - std::cout << "Adding training examples...\n"; - std::cout << "(In a real system, the user would demonstrate these interactively)\n\n"; + std::cout << "Added 5 training examples.\n"; - // Note: In actual usage, you'd call save_example() twice per example - // and the user would position the outputs between calls. - // Here we're just demonstrating the concept. - - // Exit training mode (triggers training) - std::cout << "\nExiting training mode (training will occur automatically)...\n"; + // Switching to inference triggers training + std::cout << "Training...\n"; iml.set_mode(nisps::IML::Mode::Inference); - print_separator(); - std::cout << "Demo complete!\n"; - std::cout << "\nFor real training, see the MEMLNaut-NISPS hardware implementation\n"; - std::cout << "where users physically move controls and save mappings.\n"; + // Now test: the network should have learned the mapping + std::cout << "\nResults after training:\n"; + std::cout << std::fixed << std::setprecision(3); + + struct TestCase { float in[2]; float expected[2]; const char* label; }; + TestCase tests[] = { + {{0.1f, 0.1f}, {0.1f, 0.9f}, "Trained point"}, + {{0.9f, 0.9f}, {0.9f, 0.1f}, "Trained point"}, + {{0.5f, 0.5f}, {0.5f, 0.5f}, "Trained point"}, + {{0.3f, 0.3f}, {0.0f, 0.0f}, "Interpolated"}, // Not trained on this + }; + + for (const auto& t : tests) { + iml.set_input(0, t.in[0]); + iml.set_input(1, t.in[1]); + iml.process(); + const float* out = iml.get_outputs(); + std::cout << " (" << t.in[0] << ", " << t.in[1] << ") -> (" + << out[0] << ", " << out[1] << ")"; + if (t.expected[0] > 0.0f) { + std::cout << " expected ~(" << t.expected[0] << ", " << t.expected[1] << ")"; + } + std::cout << " [" << t.label << "]\n"; + } + std::cout << "\n"; +} + +void demo_interactive_workflow() { + std::cout << "=== Demo 3: Interactive Workflow (simulated) ===\n\n"; + + // This demonstrates the two-step save_example() workflow + // used in the original MEMLNaut hardware + nisps::IML iml(1, 1, {4}, 2000, 1.0f, 0.001f); + iml.set_logger([](const char* msg) { + std::cout << " [nisps] " << msg << "\n"; + }); + + iml.set_mode(nisps::IML::Mode::Training); + + // Simulate the interactive workflow: + // 1. Set input position + // 2. save_example() -> stops inference + // 3. set_output() -> user positions the desired output + // 4. save_example() -> stores the mapping + + struct Demo { float in; float out; }; + Demo demos[] = {{0.2f, 0.2f}, {0.5f, 0.5f}, {0.8f, 0.8f}}; + + for (const auto& d : demos) { + iml.set_input(0, d.in); + iml.save_example(); // Step 1: stop inference + iml.set_output(0, d.out); // Step 2: user sets desired output + iml.save_example(); // Step 3: store the mapping + std::cout << " Saved: " << d.in << " -> " << d.out << "\n"; + } + + std::cout << "\nSwitching to inference (triggers training)...\n"; + iml.set_mode(nisps::IML::Mode::Inference); + + std::cout << std::fixed << std::setprecision(3); + for (float x = 0.0f; x <= 1.0f; x += 0.25f) { + iml.set_input(0, x); + iml.process(); + std::cout << " " << x << " -> " << iml.get_outputs()[0] << "\n"; + } + std::cout << "\n"; } int main() { - std::cout << "\n"; - std::cout << "╔══════════════════════════════════════════════════════════╗\n"; - std::cout << "║ NISPS Core Examples ║\n"; - std::cout << "║ Neural Interactive Shaping of Parameter Spaces ║\n"; - std::cout << "╚══════════════════════════════════════════════════════════╝\n"; + std::cout << "\nNISPS Core - Parameter Mapping Examples\n"; + std::cout << std::string(45, '=') << "\n\n"; demo_inference(); demo_training(); + demo_interactive_workflow(); - std::cout << "\n"; return 0; } diff --git a/nisps-core/include/nisps/dataset_impl.hpp b/nisps-core/include/nisps/dataset_impl.hpp index 5c7db1a..32d1c09 100644 --- a/nisps-core/include/nisps/dataset_impl.hpp +++ b/nisps-core/include/nisps/dataset_impl.hpp @@ -11,7 +11,6 @@ #ifndef NISPS_DATASET_IMPL_HPP #define NISPS_DATASET_IMPL_HPP -#include #include #include #include @@ -42,7 +41,6 @@ inline bool Dataset::Add(const std::vector &feature, const std::vector 0) { if ((feature.size() != data_size_) || (label.size() != output_size_)) { - std::printf("Dataset- Wrong example size.\n"); return false; } } @@ -51,7 +49,6 @@ inline bool Dataset::Add(const std::vector &feature, const std::vector &feature, const std::vector Dataset::Sample(bool with_bias) diff --git a/nisps-core/include/nisps/iml.hpp b/nisps-core/include/nisps/iml.hpp index eac53c7..80a36d6 100644 --- a/nisps-core/include/nisps/iml.hpp +++ b/nisps-core/include/nisps/iml.hpp @@ -31,6 +31,10 @@ public: size_t num_inputs() const { return n_inputs_; } size_t num_outputs() const { return n_outputs_; } + // Set outputs directly (for programmatic training without hardware) + void set_output(size_t index, Float value); + void set_outputs(const Float* values, size_t count); + // Runtime void process(); @@ -38,6 +42,7 @@ public: void set_mode(Mode mode); Mode get_mode() const { return mode_; } void save_example(); + void add_example(const Float* inputs, size_t n_in, const Float* outputs, size_t n_out); void clear_dataset(); void randomise_weights(); diff --git a/nisps-core/include/nisps/iml_impl.hpp b/nisps-core/include/nisps/iml_impl.hpp index f72770f..b5fd90b 100644 --- a/nisps-core/include/nisps/iml_impl.hpp +++ b/nisps-core/include/nisps/iml_impl.hpp @@ -65,6 +65,21 @@ const Float* IML::get_outputs() const { return output_state_.data(); } +template +void IML::set_output(size_t index, Float value) { + if (index >= n_outputs_) return; + if (value < 0) value = 0; + if (value > 1) value = 1; + output_state_[index] = value; +} + +template +void IML::set_outputs(const Float* values, size_t count) { + for (size_t i = 0; i < count && i < n_outputs_; ++i) { + set_output(i, values[i]); + } +} + template void IML::process() { if (!perform_inference_ || !input_updated_) return; @@ -112,6 +127,15 @@ void IML::save_example() { log("Example saved."); } +template +void IML::add_example(const Float* inputs, size_t n_in, const Float* outputs, size_t n_out) { + std::vector in_vec(inputs, inputs + std::min(n_in, n_inputs_)); + in_vec.resize(n_inputs_, static_cast(0)); + std::vector out_vec(outputs, outputs + std::min(n_out, n_outputs_)); + out_vec.resize(n_outputs_, static_cast(0)); + dataset_->Add(in_vec, out_vec); +} + template void IML::clear_dataset() { if (mode_ == Mode::Training) { diff --git a/nisps-core/include/nisps/loss.hpp b/nisps-core/include/nisps/loss.hpp index ed756ca..587ddd0 100644 --- a/nisps-core/include/nisps/loss.hpp +++ b/nisps-core/include/nisps/loss.hpp @@ -20,17 +20,8 @@ // #include -#if defined(__XS3A__) - -#define MLP_LOSS_FN __attribute__(( fptrgroup("mlp_loss") )) - -#else - -//#pragma message ( "PC compiler definitions enabled - check this is OK" ) #define MLP_LOSS_FN -#endif - namespace nisps { namespace loss { diff --git a/nisps-core/include/nisps/mlp_impl.hpp b/nisps-core/include/nisps/mlp_impl.hpp index c833374..adda712 100644 --- a/nisps-core/include/nisps/mlp_impl.hpp +++ b/nisps-core/include/nisps/mlp_impl.hpp @@ -92,7 +92,9 @@ void MLP::CreateMLP(const std::vector & layers_nodes, // Loss function selection loss::LossFunctionsManager loss_mgr = loss::LossFunctionsManager::Singleton(); - assert(loss_mgr.GetLossFunction(loss_function, &(this->loss_fn_))); + bool loss_ok = loss_mgr.GetLossFunction(loss_function, &(this->loss_fn_)); + assert(loss_ok); + (void)loss_ok; for (size_t i = 0; i < m_layers_nodes.size() - 1; i++) { m_layers.emplace_back(Layer(m_layers_nodes[i], diff --git a/nisps-core/include/nisps/node.hpp b/nisps-core/include/nisps/node.hpp index 253c6fc..1ba15c4 100644 --- a/nisps-core/include/nisps/node.hpp +++ b/nisps-core/include/nisps/node.hpp @@ -24,10 +24,6 @@ #include #include // for FILE -#ifdef ARM_MATH_CM33 -#include -#endif - #define CONSTANT_WEIGHT_INITIALIZATION 0 namespace nisps { @@ -343,20 +339,9 @@ public: inline T GetInputInnerProdWithWeights(std::span input) { T res = 0; - #ifdef ARM_MATH_CM33 - // Use optimized CMSIS-DSP dot product (SIMD accelerated) - arm_dot_prod_f32( - (const float32_t*)input.data(), - (const float32_t*)m_weights.data(), - input.size(), - (float32_t*)&res - ); - #else - // Fallback to manual loop for(size_t j=0; j < input.size(); j++) { res += input[j] * m_weights[j]; } - #endif res += m_bias; inner_prod = res; diff --git a/nisps-core/include/nisps/utils.hpp b/nisps-core/include/nisps/utils.hpp index 63ada21..16296e3 100644 --- a/nisps-core/include/nisps/utils.hpp +++ b/nisps-core/include/nisps/utils.hpp @@ -37,12 +37,7 @@ enum ACTIVATION_FUNCTIONS { HARDTANH }; -#if defined(__XS3A__) -#define MLP_ACTIVATION_FN __attribute__(( fptrgroup("mlp_activation") )) -#else -//#pragma message ( "PC compiler definitions enabled - check this is OK" ) #define MLP_ACTIVATION_FN -#endif /** * @namespace utils diff --git a/nisps-core/test/main.cpp b/nisps-core/test/main.cpp index dfe95ad..cde05c5 100644 --- a/nisps-core/test/main.cpp +++ b/nisps-core/test/main.cpp @@ -1,68 +1,237 @@ #include #include #include +#include void log_callback(const char* msg) { - std::cout << "[nisps] " << msg << "\n"; + std::cout << " [nisps] " << msg << "\n"; +} + +bool test_construction_and_inference() { + std::cout << "--- Test: Construction and inference ---\n"; + + nisps::IML iml(2, 1, {4, 4}, 1000, 1.0f, 0.0001f); + iml.set_logger(log_callback); + + iml.set_input(0, 0.5f); + iml.set_input(1, 0.5f); + iml.process(); + + const float* out = iml.get_outputs(); + // Output should be a valid float in [0, 1] (sigmoid output layer) + if (std::isnan(out[0]) || std::isinf(out[0])) { + std::cerr << "FAIL: Output is NaN or Inf\n"; + return false; + } + if (out[0] < 0.0f || out[0] > 1.0f) { + std::cerr << "FAIL: Output " << out[0] << " outside [0, 1]\n"; + return false; + } + + std::cout << " Output: " << out[0] << " (valid)\n"; + std::cout << "PASS\n\n"; + return true; +} + +bool test_set_output_api() { + std::cout << "--- Test: set_output / set_outputs API ---\n"; + + nisps::IML iml(2, 3); + iml.set_logger(log_callback); + + iml.set_output(0, 0.25f); + iml.set_output(1, 0.75f); + iml.set_output(2, 0.5f); + + const float* out = iml.get_outputs(); + if (std::abs(out[0] - 0.25f) > 1e-6f || + std::abs(out[1] - 0.75f) > 1e-6f || + std::abs(out[2] - 0.5f) > 1e-6f) { + std::cerr << "FAIL: set_output values not stored correctly\n"; + return false; + } + + // Test clamping + iml.set_output(0, -1.0f); + iml.set_output(1, 2.0f); + if (std::abs(iml.get_outputs()[0]) > 1e-6f || + std::abs(iml.get_outputs()[1] - 1.0f) > 1e-6f) { + std::cerr << "FAIL: set_output clamping not working\n"; + return false; + } + + // Test out-of-bounds index (should not crash) + iml.set_output(999, 0.5f); + + // Test set_outputs bulk + float vals[] = {0.1f, 0.2f, 0.3f}; + iml.set_outputs(vals, 3); + if (std::abs(iml.get_outputs()[0] - 0.1f) > 1e-6f || + std::abs(iml.get_outputs()[1] - 0.2f) > 1e-6f || + std::abs(iml.get_outputs()[2] - 0.3f) > 1e-6f) { + std::cerr << "FAIL: set_outputs bulk not working\n"; + return false; + } + + std::cout << "PASS\n\n"; + return true; +} + +bool test_add_example_api() { + std::cout << "--- Test: add_example API ---\n"; + + nisps::IML iml(2, 1, {4}, 500, 1.0f, 0.001f); + iml.set_logger(log_callback); + iml.set_mode(nisps::IML::Mode::Training); + + // Add a single example programmatically + float in[] = {0.0f, 0.0f}; + float out[] = {0.0f}; + iml.add_example(in, 2, out, 1); + + // Switch to inference (triggers training) + iml.set_mode(nisps::IML::Mode::Inference); + + // Should not crash, training on 1 example + iml.set_input(0, 0.0f); + iml.set_input(1, 0.0f); + iml.process(); + + const float* result = iml.get_outputs(); + if (std::isnan(result[0]) || std::isinf(result[0])) { + std::cerr << "FAIL: Output is NaN/Inf after training\n"; + return false; + } + + std::cout << " Output after training on 1 example: " << result[0] << "\n"; + std::cout << "PASS\n\n"; + return true; +} + +bool test_training_convergence() { + std::cout << "--- Test: Training convergence (identity mapping) ---\n"; + + // Train a network to learn: input -> same output + // This is simpler than XOR and should converge reliably + nisps::IML iml(1, 1, {8, 8}, 3000, 1.0f, 0.00001f); + iml.set_logger(log_callback); + iml.set_mode(nisps::IML::Mode::Training); + + // Add training data: output should match input + struct Example { float in; float out; }; + Example examples[] = { + {0.1f, 0.1f}, + {0.3f, 0.3f}, + {0.5f, 0.5f}, + {0.7f, 0.7f}, + {0.9f, 0.9f}, + }; + + for (const auto& ex : examples) { + iml.add_example(&ex.in, 1, &ex.out, 1); + } + + // Switch to inference (triggers training) + iml.set_mode(nisps::IML::Mode::Inference); + + // Now test: outputs should approximate inputs + float max_error = 0.0f; + bool passed = true; + + for (const auto& ex : examples) { + iml.set_input(0, ex.in); + iml.process(); + float result = iml.get_outputs()[0]; + float error = std::abs(result - ex.out); + max_error = std::max(max_error, error); + + std::cout << " Input: " << ex.in << " -> Output: " << result + << " (expected: " << ex.out << ", error: " << error << ")\n"; + + if (error > 0.15f) { + std::cerr << " ERROR: Error too large for input " << ex.in << "\n"; + passed = false; + } + } + + // Also test interpolation at a value we didn't train on + iml.set_input(0, 0.4f); + iml.process(); + float interp = iml.get_outputs()[0]; + float interp_error = std::abs(interp - 0.4f); + std::cout << " Interpolation: 0.4 -> " << interp + << " (error: " << interp_error << ")\n"; + + std::cout << " Max training error: " << max_error << "\n"; + if (passed) { + std::cout << "PASS\n\n"; + } else { + std::cerr << "FAIL: Network did not converge\n\n"; + } + return passed; +} + +bool test_multi_output_training() { + std::cout << "--- Test: Multi-output training ---\n"; + + // 2 inputs -> 2 outputs + // Learn: (low, low) -> (0, 0), (high, high) -> (1, 1) + nisps::IML iml(2, 2, {8, 8}, 3000, 1.0f, 0.00001f); + iml.set_logger(log_callback); + iml.set_mode(nisps::IML::Mode::Training); + + float in1[] = {0.1f, 0.1f}; float out1[] = {0.1f, 0.9f}; + float in2[] = {0.9f, 0.9f}; float out2[] = {0.9f, 0.1f}; + float in3[] = {0.1f, 0.9f}; float out3[] = {0.5f, 0.5f}; + float in4[] = {0.9f, 0.1f}; float out4[] = {0.5f, 0.5f}; + + iml.add_example(in1, 2, out1, 2); + iml.add_example(in2, 2, out2, 2); + iml.add_example(in3, 2, out3, 2); + iml.add_example(in4, 2, out4, 2); + + iml.set_mode(nisps::IML::Mode::Inference); + + // Test that the network learned distinct mappings + iml.set_input(0, 0.1f); iml.set_input(1, 0.1f); + iml.process(); + float r1_0 = iml.get_outputs()[0]; + float r1_1 = iml.get_outputs()[1]; + + iml.set_input(0, 0.9f); iml.set_input(1, 0.9f); + iml.process(); + float r2_0 = iml.get_outputs()[0]; + float r2_1 = iml.get_outputs()[1]; + + std::cout << " (0.1, 0.1) -> (" << r1_0 << ", " << r1_1 << ") expected ~(0.1, 0.9)\n"; + std::cout << " (0.9, 0.9) -> (" << r2_0 << ", " << r2_1 << ") expected ~(0.9, 0.1)\n"; + + // The outputs for different inputs should be meaningfully different + bool different = (std::abs(r1_0 - r2_0) > 0.1f) || (std::abs(r1_1 - r2_1) > 0.1f); + if (!different) { + std::cerr << "FAIL: Network outputs are too similar for different inputs\n\n"; + return false; + } + + std::cout << "PASS\n\n"; + return true; } int main() { - std::cout << "=== NISPS Core Test: XOR Training ===\n\n"; + std::cout << "\n=== NISPS Core Test Suite ===\n\n"; - // Create IML with 2 inputs, 1 output - nisps::IML iml(2, 1, {4, 4}, 5000, 1.0f, 0.0001f); - iml.set_logger(log_callback); + int passed = 0; + int failed = 0; - // Enter training mode - iml.set_mode(nisps::IML::Mode::Training); + auto run = [&](bool result) { result ? passed++ : failed++; }; - // Train on XOR pattern - // (0,0) -> 0 - iml.set_input(0, 0.0f); - iml.set_input(1, 0.0f); - iml.save_example(); // First call: stop inference - // Manually set output for this example (simulating user positioning) - // We access output_state_ indirectly by calling process after training + run(test_construction_and_inference()); + run(test_set_output_api()); + run(test_add_example_api()); + run(test_training_convergence()); + run(test_multi_output_training()); - // For this test, we'll add examples directly to dataset - // This simulates the two-step save process + std::cout << "=== Results: " << passed << " passed, " << failed << " failed ===\n\n"; - // Actually, let's test the full workflow properly: - // The IML class expects: save_example() twice per example - // 1. First call stops inference - // 2. User sets output position (we can't do this externally easily) - // 3. Second call stores input->output - - // For testing, let's verify the basic inference works - iml.set_mode(nisps::IML::Mode::Inference); - - // Test inference - iml.set_input(0, 0.0f); - iml.set_input(1, 0.0f); - iml.process(); - float out_00 = iml.get_outputs()[0]; - - iml.set_input(0, 1.0f); - iml.set_input(1, 0.0f); - iml.process(); - float out_10 = iml.get_outputs()[0]; - - iml.set_input(0, 0.0f); - iml.set_input(1, 1.0f); - iml.process(); - float out_01 = iml.get_outputs()[0]; - - iml.set_input(0, 1.0f); - iml.set_input(1, 1.0f); - iml.process(); - float out_11 = iml.get_outputs()[0]; - - std::cout << "\nInference results (untrained):\n"; - std::cout << " (0,0) -> " << out_00 << "\n"; - std::cout << " (1,0) -> " << out_10 << "\n"; - std::cout << " (0,1) -> " << out_01 << "\n"; - std::cout << " (1,1) -> " << out_11 << "\n"; - - std::cout << "\n=== Test passed: nisps-core compiles and runs ===\n"; - return 0; + return failed > 0 ? 1 : 0; }