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
This commit is contained in:
monkey-w1n5t0n 2026-02-08 18:01:48 +01:00
parent be85a5cd71
commit 45193a2c01
12 changed files with 426 additions and 233 deletions

View file

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

View file

@ -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<float> iml(n_inputs, n_outputs);
```
All method names remain the same, just add the namespace.

View file

@ -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<float>::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<float>::Mode::Inference);
```
### Interactive Training (hardware/UI)
```cpp
// For interactive systems with physical controls:
iml.set_mode(nisps::IML<float>::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<float>::Mode::Inference);
```
@ -97,6 +107,8 @@ nisps::IML<Float>(
```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

View file

@ -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 <iostream>
#include <iomanip>
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<float> 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<float> iml(2, 1, {4}, 1000, 1.0f, 0.001f);
// Set up logging
// 2 inputs -> 2 outputs, small network
nisps::IML<float> 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<float>::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<float>::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<float> 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<float>::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<float>::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;
}

View file

@ -11,7 +11,6 @@
#ifndef NISPS_DATASET_IMPL_HPP
#define NISPS_DATASET_IMPL_HPP
#include <cstdio>
#include <cassert>
#include <random>
#include <algorithm>
@ -42,7 +41,6 @@ inline bool Dataset::Add(const std::vector<float> &feature, const std::vector<fl
if (data_size_ > 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<float> &feature, const std::vector<fl
if (replay_memory_enabled_) {
RemoveOneExcessExample();
} else {
std::printf("Dataset- Max dataset size of %zu exceeded.\n", max_examples_);
return false;
}
}
@ -61,8 +58,6 @@ inline bool Dataset::Add(const std::vector<float> &feature, const std::vector<fl
timestamps_.push_back(current_timestamp_);
current_timestamp_++;
std::printf("Dataset- Added example.\n");
std::printf("Dataset- Feature size %zu, label size %zu.\n", features_.size(), labels_.size());
_AdjustSizes();
return true;
}
@ -115,7 +110,6 @@ inline void Dataset::RemoveOneExcessExample() {
features_.erase(features_.begin() + index_to_remove);
labels_.erase(labels_.begin() + index_to_remove);
timestamps_.erase(timestamps_.begin() + index_to_remove);
std::printf("Dataset- Memory full, removing example at index %zu (mode %d).\n", index_to_remove, forget_mode_);
}
inline void Dataset::Clear()
@ -178,17 +172,12 @@ inline void Dataset::_AdjustSizes()
inline void Dataset::ReplayMemory(bool enabled)
{
replay_memory_enabled_ = enabled;
if (replay_memory_enabled_) {
std::printf("Replay memory functionality enabled.\n");
} else {
std::printf("Replay memory functionality disabled.\n");
}
(void)replay_memory_enabled_;
}
inline void Dataset::SetForgetMode(ForgetMode mode)
{
forget_mode_ = mode;
std::printf("Forget mode set to %d.\n", mode);
}
inline void Dataset::SetMaxExamples(size_t max)
@ -206,7 +195,6 @@ inline void Dataset::SetMaxExamples(size_t max)
break;
}
}
std::printf("Max examples set to %zu.\n", max_examples_);
}
inline std::pair<Dataset::DatasetVector, Dataset::DatasetVector> Dataset::Sample(bool with_bias)

View file

@ -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();

View file

@ -65,6 +65,21 @@ const Float* IML<Float>::get_outputs() const {
return output_state_.data();
}
template<typename Float>
void IML<Float>::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<typename Float>
void IML<Float>::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<typename Float>
void IML<Float>::process() {
if (!perform_inference_ || !input_updated_) return;
@ -112,6 +127,15 @@ void IML<Float>::save_example() {
log("Example saved.");
}
template<typename Float>
void IML<Float>::add_example(const Float* inputs, size_t n_in, const Float* outputs, size_t n_out) {
std::vector<Float> in_vec(inputs, inputs + std::min(n_in, n_inputs_));
in_vec.resize(n_inputs_, static_cast<Float>(0));
std::vector<Float> out_vec(outputs, outputs + std::min(n_out, n_outputs_));
out_vec.resize(n_outputs_, static_cast<Float>(0));
dataset_->Add(in_vec, out_vec);
}
template<typename Float>
void IML<Float>::clear_dataset() {
if (mode_ == Mode::Training) {

View file

@ -20,17 +20,8 @@
// #include <string>
#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 {

View file

@ -92,7 +92,9 @@ void MLP<T>::CreateMLP(const std::vector<size_t> & layers_nodes,
// Loss function selection
loss::LossFunctionsManager<T> loss_mgr =
loss::LossFunctionsManager<T>::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<T>(m_layers_nodes[i],

View file

@ -24,10 +24,6 @@
#include <span>
#include <cstdio> // for FILE
#ifdef ARM_MATH_CM33
#include <arm_math.h>
#endif
#define CONSTANT_WEIGHT_INITIALIZATION 0
namespace nisps {
@ -343,20 +339,9 @@ public:
inline T GetInputInnerProdWithWeights(std::span<const T> 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;

View file

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

View file

@ -1,68 +1,237 @@
#include <nisps/nisps.hpp>
#include <iostream>
#include <cmath>
#include <cassert>
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<float> 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<float> 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<float> iml(2, 1, {4}, 500, 1.0f, 0.001f);
iml.set_logger(log_callback);
iml.set_mode(nisps::IML<float>::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<float>::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<float> iml(1, 1, {8, 8}, 3000, 1.0f, 0.00001f);
iml.set_logger(log_callback);
iml.set_mode(nisps::IML<float>::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<float>::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<float> iml(2, 2, {8, 8}, 3000, 1.0f, 0.00001f);
iml.set_logger(log_callback);
iml.set_mode(nisps::IML<float>::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<float>::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<float> 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<float>::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<float>::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;
}