feat(nisps-core,vcv): complete Phases 6 + 7 — persistence, derived outputs
Phase 6 — State persistence: - Full state serialization: version, weights (3D), examples (features+labels), mlpConfig, noiseLevel, slewMs, output/input ranges - Validation on load: version check, graceful missing field handling - .nisps preset save/load via right-click menu (osdialog file dialogs) - Param values included in preset files Phase 7 — Derived outputs: - Mean, STD, delta computed on audio thread (trivial cost) - Novelty/confidence: nearest_example_distance() computed on background thread after each training/perturbation job, cached for audio thread - Defaults with 0 examples: novelty=10V, confidence=0V nisps-core IML additions: - get_weights() / set_weights() for MLP weight serialization - get_example_features/labels() / load_examples() for dataset serialization - nearest_example_distance() for novelty/confidence metric - get_example_count() / get_max_examples() for UI display
This commit is contained in:
parent
6a76f15736
commit
74c52fadc7
3 changed files with 285 additions and 3 deletions
|
|
@ -54,6 +54,26 @@ public:
|
|||
// speed: noise magnitude, spread: 0 = flat noise, 1 = Xavier-scaled + weight decay
|
||||
void move_weights(Float speed, Float spread);
|
||||
|
||||
// ── Serialization accessors ───────────────────────────────────────
|
||||
|
||||
// Weight access (delegates to MLP)
|
||||
typename MLP<Float>::mlp_weights get_weights() const;
|
||||
void set_weights(typename MLP<Float>::mlp_weights& weights);
|
||||
|
||||
// Dataset access
|
||||
size_t get_example_count() const;
|
||||
size_t get_max_examples() const;
|
||||
// Returns copies of the dataset vectors
|
||||
std::vector<std::vector<Float>> get_example_features() const;
|
||||
std::vector<std::vector<Float>> get_example_labels() const;
|
||||
// Bulk-load examples (clears existing, adds all)
|
||||
void load_examples(const std::vector<std::vector<Float>>& features,
|
||||
const std::vector<std::vector<Float>>& labels);
|
||||
|
||||
// Nearest-neighbor distance for novelty/confidence computation
|
||||
// Returns the minimum Euclidean distance from `input` to any training example
|
||||
Float nearest_example_distance(const Float* input, size_t n_in) const;
|
||||
|
||||
// Optional logging
|
||||
void set_logger(LogFn fn) { log_fn_ = fn; }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
#ifndef NISPS_IML_IMPL_HPP
|
||||
#define NISPS_IML_IMPL_HPP
|
||||
|
||||
#include <limits>
|
||||
#include <cmath>
|
||||
|
||||
namespace nisps {
|
||||
|
||||
template<typename Float>
|
||||
|
|
@ -226,6 +229,84 @@ void IML<Float>::train() {
|
|||
log("Training complete.");
|
||||
}
|
||||
|
||||
// ── Serialization accessors ───────────────────────────────────────
|
||||
|
||||
template<typename Float>
|
||||
typename MLP<Float>::mlp_weights IML<Float>::get_weights() const {
|
||||
return mlp_->GetWeights();
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
void IML<Float>::set_weights(typename MLP<Float>::mlp_weights& weights) {
|
||||
mlp_->SetWeights(weights);
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
size_t IML<Float>::get_example_count() const {
|
||||
Dataset::DatasetVector* feats;
|
||||
Dataset::DatasetVector* labels;
|
||||
const_cast<Dataset*>(dataset_.get())->Fetch(feats, labels);
|
||||
return feats ? feats->size() : 0;
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
size_t IML<Float>::get_max_examples() const {
|
||||
return Dataset::kMax_examples;
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
std::vector<std::vector<Float>> IML<Float>::get_example_features() const {
|
||||
auto feats = const_cast<Dataset*>(dataset_.get())->GetFeatures(false);
|
||||
std::vector<std::vector<Float>> result;
|
||||
result.reserve(feats.size());
|
||||
for (auto& f : feats) {
|
||||
result.emplace_back(f.begin(), f.end());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
std::vector<std::vector<Float>> IML<Float>::get_example_labels() const {
|
||||
auto& labels = const_cast<Dataset*>(dataset_.get())->GetLabels();
|
||||
std::vector<std::vector<Float>> result;
|
||||
result.reserve(labels.size());
|
||||
for (auto& l : labels) {
|
||||
result.emplace_back(l.begin(), l.end());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
void IML<Float>::load_examples(const std::vector<std::vector<Float>>& features,
|
||||
const std::vector<std::vector<Float>>& labels) {
|
||||
dataset_->Clear();
|
||||
size_t count = std::min(features.size(), labels.size());
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
std::vector<float> feat(features[i].begin(), features[i].end());
|
||||
std::vector<float> label(labels[i].begin(), labels[i].end());
|
||||
dataset_->Add(feat, label);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename Float>
|
||||
Float IML<Float>::nearest_example_distance(const Float* input, size_t n_in) const {
|
||||
auto feats = const_cast<Dataset*>(dataset_.get())->GetFeatures(false);
|
||||
if (feats.empty()) return static_cast<Float>(-1);
|
||||
|
||||
Float minDist = std::numeric_limits<Float>::max();
|
||||
size_t dims = std::min(n_in, n_inputs_);
|
||||
for (auto& f : feats) {
|
||||
Float dist = 0;
|
||||
for (size_t d = 0; d < dims && d < f.size(); d++) {
|
||||
Float diff = static_cast<Float>(f[d]) - input[d];
|
||||
dist += diff * diff;
|
||||
}
|
||||
dist = std::sqrt(dist);
|
||||
if (dist < minDist) minDist = dist;
|
||||
}
|
||||
return minDist;
|
||||
}
|
||||
|
||||
} // namespace nisps
|
||||
|
||||
#endif // NISPS_IML_IMPL_HPP
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
#include "plugin.hpp"
|
||||
#include <nisps/nisps.hpp>
|
||||
#include <osdialog.h>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <fstream>
|
||||
|
||||
static constexpr int NUM_ML_INPUTS = 2;
|
||||
static constexpr int NUM_ML_OUTPUTS = 12;
|
||||
|
|
@ -75,6 +77,9 @@ struct MEMLNaut : Module {
|
|||
bool outputRangeUnipolar[NUM_ML_OUTPUTS] = {}; // true = 0-10V, false = ±5V
|
||||
bool inputRangeUnipolar[MAX_ML_INPUTS] = {}; // true = 0-10V, false = ±5V
|
||||
float clearHoldTime = 0.f;
|
||||
float cachedNovelty = 10.f; // default: everything novel (10V)
|
||||
float cachedConfidence = 0.f; // default: no confidence (0V)
|
||||
float lastInputs[MAX_ML_INPUTS] = {};
|
||||
|
||||
// ── Triggers ──────────────────────────────────────────────────────
|
||||
dsp::BooleanTrigger randTrigger;
|
||||
|
|
@ -186,6 +191,18 @@ struct MEMLNaut : Module {
|
|||
iml.move_weights(job.noiseLevel, job.spread);
|
||||
}
|
||||
|
||||
// Update novelty/confidence for current input position
|
||||
if (iml.get_example_count() > 0) {
|
||||
float dist = iml.nearest_example_distance(lastInputs, NUM_ML_INPUTS);
|
||||
// Novelty: scale distance to 0-10V (1.0 distance = 10V, saturates)
|
||||
cachedNovelty = std::min(dist * 10.f, 10.f);
|
||||
// Confidence: inverse of distance (close = high confidence)
|
||||
cachedConfidence = std::max(0.f, 10.f - dist * 10.f);
|
||||
} else {
|
||||
cachedNovelty = 10.f;
|
||||
cachedConfidence = 0.f;
|
||||
}
|
||||
|
||||
swapReady.store(true);
|
||||
isTraining.store(false);
|
||||
|
||||
|
|
@ -340,6 +357,8 @@ struct MEMLNaut : Module {
|
|||
// Read and normalize inputs
|
||||
float x = normalizeInput(INPUT_X, 0);
|
||||
float y = normalizeInput(INPUT_Y, 1);
|
||||
lastInputs[0] = x;
|
||||
lastInputs[1] = y;
|
||||
|
||||
iml.set_input(0, x);
|
||||
iml.set_input(1, y);
|
||||
|
|
@ -410,14 +429,15 @@ struct MEMLNaut : Module {
|
|||
}
|
||||
outputs[OUTPUT_DELTA].setVoltage(std::sqrt(delta) * 10.f);
|
||||
|
||||
// Novelty + Confidence (placeholder — computed on training thread in Phase 7)
|
||||
outputs[OUTPUT_NOVELTY].setVoltage(10.f); // default: everything is novel
|
||||
outputs[OUTPUT_CONFIDENCE].setVoltage(0.f); // default: no confidence
|
||||
// Novelty + Confidence (computed on background thread, cached)
|
||||
outputs[OUTPUT_NOVELTY].setVoltage(cachedNovelty);
|
||||
outputs[OUTPUT_CONFIDENCE].setVoltage(cachedConfidence);
|
||||
}
|
||||
|
||||
// ── Serialization ─────────────────────────────────────────────────
|
||||
json_t* dataToJson() override {
|
||||
json_t* root = json_object();
|
||||
json_object_set_new(root, "version", json_integer(1));
|
||||
json_object_set_new(root, "noiseLevel", json_real(noiseLevel));
|
||||
json_object_set_new(root, "slewMs", json_real(slewMs));
|
||||
|
||||
|
|
@ -435,6 +455,52 @@ struct MEMLNaut : Module {
|
|||
}
|
||||
json_object_set_new(root, "inputRangeUnipolar", inRanges);
|
||||
|
||||
// MLP weights (3D: layer → node → weight)
|
||||
auto weights = iml.get_weights();
|
||||
json_t* jWeights = json_array();
|
||||
for (auto& layer : weights) {
|
||||
json_t* jLayer = json_array();
|
||||
for (auto& node : layer) {
|
||||
json_t* jNode = json_array();
|
||||
for (float w : node) {
|
||||
json_array_append_new(jNode, json_real(w));
|
||||
}
|
||||
json_array_append_new(jLayer, jNode);
|
||||
}
|
||||
json_array_append_new(jWeights, jLayer);
|
||||
}
|
||||
json_object_set_new(root, "weights", jWeights);
|
||||
|
||||
// Training examples
|
||||
auto features = iml.get_example_features();
|
||||
auto labels = iml.get_example_labels();
|
||||
json_t* jExamples = json_object();
|
||||
json_t* jFeatures = json_array();
|
||||
for (auto& f : features) {
|
||||
json_t* jF = json_array();
|
||||
for (float v : f) json_array_append_new(jF, json_real(v));
|
||||
json_array_append_new(jFeatures, jF);
|
||||
}
|
||||
json_t* jLabels = json_array();
|
||||
for (auto& l : labels) {
|
||||
json_t* jL = json_array();
|
||||
for (float v : l) json_array_append_new(jL, json_real(v));
|
||||
json_array_append_new(jLabels, jL);
|
||||
}
|
||||
json_object_set_new(jExamples, "features", jFeatures);
|
||||
json_object_set_new(jExamples, "labels", jLabels);
|
||||
json_object_set_new(root, "examples", jExamples);
|
||||
|
||||
// MLP config (for validation on load)
|
||||
json_t* jConfig = json_object();
|
||||
json_t* jLayers = json_array();
|
||||
// [3, 16, 24, 16, 12] for default config
|
||||
json_array_append_new(jLayers, json_integer(NUM_ML_INPUTS + 1)); // +bias
|
||||
for (int h : {16, 24, 16}) json_array_append_new(jLayers, json_integer(h));
|
||||
json_array_append_new(jLayers, json_integer(NUM_ML_OUTPUTS));
|
||||
json_object_set_new(jConfig, "layers", jLayers);
|
||||
json_object_set_new(root, "mlpConfig", jConfig);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
|
|
@ -445,6 +511,7 @@ struct MEMLNaut : Module {
|
|||
if ((j = json_object_get(root, "slewMs")))
|
||||
slewMs = json_real_value(j);
|
||||
|
||||
// Output ranges
|
||||
json_t* outRanges = json_object_get(root, "outputRangeUnipolar");
|
||||
if (outRanges) {
|
||||
for (int i = 0; i < NUM_ML_OUTPUTS && i < (int)json_array_size(outRanges); i++) {
|
||||
|
|
@ -452,12 +519,58 @@ struct MEMLNaut : Module {
|
|||
}
|
||||
}
|
||||
|
||||
// Input ranges
|
||||
json_t* inRanges = json_object_get(root, "inputRangeUnipolar");
|
||||
if (inRanges) {
|
||||
for (int i = 0; i < MAX_ML_INPUTS && i < (int)json_array_size(inRanges); i++) {
|
||||
inputRangeUnipolar[i] = json_boolean_value(json_array_get(inRanges, i));
|
||||
}
|
||||
}
|
||||
|
||||
// MLP weights
|
||||
json_t* jWeights = json_object_get(root, "weights");
|
||||
if (jWeights && json_is_array(jWeights)) {
|
||||
nisps::MLP<float>::mlp_weights weights;
|
||||
for (size_t li = 0; li < json_array_size(jWeights); li++) {
|
||||
json_t* jLayer = json_array_get(jWeights, li);
|
||||
std::vector<std::vector<float>> layer;
|
||||
for (size_t ni = 0; ni < json_array_size(jLayer); ni++) {
|
||||
json_t* jNode = json_array_get(jLayer, ni);
|
||||
std::vector<float> node;
|
||||
for (size_t wi = 0; wi < json_array_size(jNode); wi++) {
|
||||
node.push_back(json_real_value(json_array_get(jNode, wi)));
|
||||
}
|
||||
layer.push_back(node);
|
||||
}
|
||||
weights.push_back(layer);
|
||||
}
|
||||
iml.set_weights(weights);
|
||||
}
|
||||
|
||||
// Training examples
|
||||
json_t* jExamples = json_object_get(root, "examples");
|
||||
if (jExamples) {
|
||||
json_t* jFeatures = json_object_get(jExamples, "features");
|
||||
json_t* jLabels = json_object_get(jExamples, "labels");
|
||||
if (jFeatures && jLabels) {
|
||||
std::vector<std::vector<float>> features, labels;
|
||||
for (size_t i = 0; i < json_array_size(jFeatures); i++) {
|
||||
json_t* jF = json_array_get(jFeatures, i);
|
||||
std::vector<float> f;
|
||||
for (size_t fi = 0; fi < json_array_size(jF); fi++)
|
||||
f.push_back(json_real_value(json_array_get(jF, fi)));
|
||||
features.push_back(f);
|
||||
}
|
||||
for (size_t i = 0; i < json_array_size(jLabels); i++) {
|
||||
json_t* jL = json_array_get(jLabels, i);
|
||||
std::vector<float> l;
|
||||
for (size_t li = 0; li < json_array_size(jL); li++)
|
||||
l.push_back(json_real_value(json_array_get(jL, li)));
|
||||
labels.push_back(l);
|
||||
}
|
||||
iml.load_examples(features, labels);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -619,6 +732,74 @@ struct MEMLNautWidget : ModuleWidget {
|
|||
));
|
||||
}
|
||||
}));
|
||||
|
||||
// ── Preset save/load ──────────────────────────────────────────
|
||||
menu->addChild(new MenuSeparator);
|
||||
menu->addChild(createMenuLabel("Presets (.nisps)"));
|
||||
|
||||
menu->addChild(createMenuItem("Save .nisps preset...", "", [=]() {
|
||||
osdialog_filters* filters = osdialog_filters_parse("NISPS preset:nisps");
|
||||
char* path = osdialog_file(OSDIALOG_SAVE, nullptr, "preset.nisps", filters);
|
||||
osdialog_filters_free(filters);
|
||||
if (!path) return;
|
||||
|
||||
json_t* root = module->dataToJson();
|
||||
// Also save all param values
|
||||
json_t* jParams = json_array();
|
||||
for (int i = 0; i < MEMLNaut::PARAMS_LEN; i++) {
|
||||
json_array_append_new(jParams, json_real(module->params[i].getValue()));
|
||||
}
|
||||
json_object_set_new(root, "params", jParams);
|
||||
|
||||
char* jsonStr = json_dumps(root, JSON_INDENT(2));
|
||||
json_decref(root);
|
||||
|
||||
std::ofstream file(path);
|
||||
if (file.is_open()) {
|
||||
file << jsonStr;
|
||||
file.close();
|
||||
}
|
||||
free(jsonStr);
|
||||
free(path);
|
||||
}));
|
||||
|
||||
menu->addChild(createMenuItem("Load .nisps preset...", "", [=]() {
|
||||
osdialog_filters* filters = osdialog_filters_parse("NISPS preset:nisps");
|
||||
char* path = osdialog_file(OSDIALOG_OPEN, nullptr, nullptr, filters);
|
||||
osdialog_filters_free(filters);
|
||||
if (!path) return;
|
||||
|
||||
std::ifstream file(path);
|
||||
free(path);
|
||||
if (!file.is_open()) return;
|
||||
|
||||
std::string content((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
file.close();
|
||||
|
||||
json_error_t error;
|
||||
json_t* root = json_loads(content.c_str(), 0, &error);
|
||||
if (!root) return;
|
||||
|
||||
// Validate version
|
||||
json_t* jVersion = json_object_get(root, "version");
|
||||
if (!jVersion || json_integer_value(jVersion) < 1) {
|
||||
json_decref(root);
|
||||
return;
|
||||
}
|
||||
|
||||
module->dataFromJson(root);
|
||||
|
||||
// Restore param values if present
|
||||
json_t* jParams = json_object_get(root, "params");
|
||||
if (jParams && json_is_array(jParams)) {
|
||||
for (size_t i = 0; i < json_array_size(jParams) && i < MEMLNaut::PARAMS_LEN; i++) {
|
||||
module->params[i].setValue(json_real_value(json_array_get(jParams, i)));
|
||||
}
|
||||
}
|
||||
|
||||
json_decref(root);
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue