From 6a78e58450a93f962163069bc56c65856d70b9d0 Mon Sep 17 00:00:00 2001 From: Owen Green Date: Tue, 25 Nov 2025 15:19:35 +0000 Subject: [PATCH 1/6] BufNMF Update random number mechanism and add seeding to client --- include/flucoma/algorithms/public/NMF.hpp | 18 +++++++++--------- include/flucoma/clients/nrt/NMFClient.hpp | 7 +++++-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/include/flucoma/algorithms/public/NMF.hpp b/include/flucoma/algorithms/public/NMF.hpp index 9cc6e1e0b..a8da7b98d 100644 --- a/include/flucoma/algorithms/public/NMF.hpp +++ b/include/flucoma/algorithms/public/NMF.hpp @@ -11,6 +11,7 @@ under the European Union’s Horizon 2020 research and innovation programme #pragma once #include "../util/AlgorithmUtils.hpp" +#include "../util/EigenRandom.hpp" #include "../util/FluidEigenMappings.hpp" #include "../../data/FluidIndex.hpp" #include "../../data/TensorTypes.hpp" @@ -42,8 +43,8 @@ class NMF // processFrame computes activations of a dictionary W in a given frame void processFrame(const RealVectorView x, const RealMatrixView W0, - RealVectorView out, index nIterations, - RealVectorView v, Allocator& alloc) + RealVectorView out, index nIterations, RealVectorView v, + index randomSeed, Allocator& alloc) { using namespace Eigen; using namespace _impl; @@ -51,8 +52,7 @@ class NMF FluidEigenMap W = asEigen(W0); ScopedEigenMap h(rank, alloc); - h = VectorXd::Random(rank) * 0.5 + VectorXd::Constant(rank, 0.5); - + h = EigenRandom(rank, RandomSeed{randomSeed}, Range{0.0, 1.0}); ScopedEigenMap v0(x.size(), alloc); v0 = asEigen(x); W = W.array().max(epsilon).matrix(); @@ -90,7 +90,7 @@ class NMF void process(const RealMatrixView X, RealMatrixView W1, RealMatrixView H1, RealMatrixView V1, index rank, index nIterations, bool updateW, - bool updateH = false, + bool updateH = false, index randomSeed = -1, RealMatrixView W0 = RealMatrixView(nullptr, 0, 0, 0), RealMatrixView H0 = RealMatrixView(nullptr, 0, 0, 0)) { @@ -101,8 +101,8 @@ class NMF MatrixXd W; if (W0.extent(0) == 0 && W0.extent(1) == 0) { - W = MatrixXd::Random(nBins, rank) * 0.5 + - MatrixXd::Constant(nBins, rank, 0.5); + W = EigenRandom(nBins, rank, RandomSeed{randomSeed}, + Range{0.0, 1.0}); } else { @@ -113,8 +113,8 @@ class NMF MatrixXd H; if (H0.extent(0) == 0 && H0.extent(1) == 0) { - H = MatrixXd::Random(rank, nFrames) * 0.5 + - MatrixXd::Constant(rank, nFrames, 0.5); + H = EigenRandom(rank, nFrames, RandomSeed{randomSeed}, + Range{0.0, 1.0}); } else { diff --git a/include/flucoma/clients/nrt/NMFClient.hpp b/include/flucoma/clients/nrt/NMFClient.hpp index 0abca69c6..5e9c2384e 100644 --- a/include/flucoma/clients/nrt/NMFClient.hpp +++ b/include/flucoma/clients/nrt/NMFClient.hpp @@ -47,6 +47,7 @@ enum NMFParamIndex { kEnvelopesUpdate, kRank, kIterations, + kRandomSeed, kFFT }; @@ -66,6 +67,7 @@ constexpr auto BufNMFParams = defineParameters( "Fixed"), LongParam("components", "Number of Components", 1, Min(1)), LongParam("iterations", "Number of Iterations", 100, Min(1)), + LongParam("seed", "Random Seem", -1), FFTParam("fftSettings", "FFT Settings", 1024, -1, -1)); class NMFClient : public FluidBaseClient, public OfflineIn, public OfflineOut @@ -264,8 +266,9 @@ class NMFClient : public FluidBaseClient, public OfflineIn, public OfflineOut : true; }); nmf.process(magnitude, outputFilters, outputEnvelopes, outputMags, - get(), get() * needsAnalysis, !fixFilters, !fixEnvelopes, - seededFilters, seededEnvelopes); + get(), get() * needsAnalysis, !fixFilters, + !fixEnvelopes, get(), seededFilters, + seededEnvelopes); if (c.task() && c.task()->cancelled()) return {Result::Status::kCancelled, ""}; From 5ec06a44c48d9d3af7acd6ce3856aec807b0f8df Mon Sep 17 00:00:00 2001 From: Owen Green Date: Tue, 25 Nov 2025 16:52:00 +0000 Subject: [PATCH 2/6] Add test for NMF repeatability with seeding --- tests/CMakeLists.txt | 2 ++ tests/algorithms/public/TestNMF.cpp | 44 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 tests/algorithms/public/TestNMF.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 74ec9efa3..dceacf3dc 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -116,6 +116,7 @@ add_test_executable(TestTransientSlice algorithms/public/TestTransientSlice.cpp) add_test_executable(TestMLP algorithms/public/TestMLP.cpp) add_test_executable(TestKMeans algorithms/public/TestKMeans.cpp) +add_test_executable(TestNMF algorithms/public/TestNMF.cpp) target_link_libraries(TestNoveltySeg PRIVATE TestSignals) target_link_libraries(TestOnsetSeg PRIVATE TestSignals) @@ -153,5 +154,6 @@ catch_discover_tests(TestBufferedProcess WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" catch_discover_tests(TestMLP WORKING_DIRECTORY "${CMAKE_BINARY_DIR}") catch_discover_tests(TestKMeans WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) catch_discover_tests(TestEigenRandom WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) +catch_discover_tests(TestNMF WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) add_compile_tests("FluidTensor Compilation Tests" data/compile_tests/TestFluidTensor_Compile.cpp) diff --git a/tests/algorithms/public/TestNMF.cpp b/tests/algorithms/public/TestNMF.cpp new file mode 100644 index 000000000..3b7d742a5 --- /dev/null +++ b/tests/algorithms/public/TestNMF.cpp @@ -0,0 +1,44 @@ +#define CATCH_CONFIG_MAIN +#include +#include +#include +#include +#include +#include + +TEST_CASE("NMF is repeatable with user-supplied random seed") +{ + + using fluid::algorithm::NMF; + using Tensor = fluid::FluidTensor; + NMF algo; + + Tensor input{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; + + std::vector Vs(4, Tensor(3, 3)); + std::vector Ws(4, Tensor(2, 3)); + std::vector Hs(4, Tensor(3, 2)); + + algo.process(input, Ws[0], Hs[0], Vs[0], 2, 1, true, true, 42); + algo.process(input, Ws[1], Hs[1], Vs[1], 2, 1, true, true, 42); + algo.process(input, Ws[2], Hs[2], Vs[2], 2, 1, true, true, 5063); + algo.process(input, Ws[3], Hs[3], Vs[3], 2, 1, true, true, 5063); + + using Catch::Matchers::RangeEquals; + + SECTION("Calls with the same seed have the same output") + { + REQUIRE_THAT(Ws[1], RangeEquals(Ws[0])); + REQUIRE_THAT(Hs[1], RangeEquals(Hs[0])); + REQUIRE_THAT(Vs[1], RangeEquals(Vs[0])); + REQUIRE_THAT(Ws[3], RangeEquals(Ws[2])); + REQUIRE_THAT(Hs[3], RangeEquals(Hs[2])); + REQUIRE_THAT(Vs[3], RangeEquals(Vs[2])); + } + SECTION("Calls with different seeds have different outputs") + { + REQUIRE_THAT(Ws[1], !RangeEquals(Ws[2])); + REQUIRE_THAT(Hs[1], !RangeEquals(Hs[2])); + REQUIRE_THAT(Vs[1], !RangeEquals(Vs[2])); + } +} From 404be4febf905221a6d2a0112f3640b8d3147f94 Mon Sep 17 00:00:00 2001 From: Pierre Alexandre Tremblay Date: Tue, 25 Nov 2025 19:07:41 +0100 Subject: [PATCH 3/6] typo fix --- include/flucoma/clients/nrt/NMFClient.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/flucoma/clients/nrt/NMFClient.hpp b/include/flucoma/clients/nrt/NMFClient.hpp index 5e9c2384e..5cdaf9245 100644 --- a/include/flucoma/clients/nrt/NMFClient.hpp +++ b/include/flucoma/clients/nrt/NMFClient.hpp @@ -67,7 +67,7 @@ constexpr auto BufNMFParams = defineParameters( "Fixed"), LongParam("components", "Number of Components", 1, Min(1)), LongParam("iterations", "Number of Iterations", 100, Min(1)), - LongParam("seed", "Random Seem", -1), + LongParam("seed", "Random Seed", -1), FFTParam("fftSettings", "FFT Settings", 1024, -1, -1)); class NMFClient : public FluidBaseClient, public OfflineIn, public OfflineOut From 243b5d4b1406def8923ac9778abbe8da26c4a8a2 Mon Sep 17 00:00:00 2001 From: Pierre Alexandre Tremblay Date: Wed, 26 Nov 2025 21:03:23 +0100 Subject: [PATCH 4/6] minimum clang-formatted --- include/flucoma/algorithms/public/NMF.hpp | 4 ++-- include/flucoma/clients/nrt/NMFClient.hpp | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/include/flucoma/algorithms/public/NMF.hpp b/include/flucoma/algorithms/public/NMF.hpp index a8da7b98d..6c6a4d161 100644 --- a/include/flucoma/algorithms/public/NMF.hpp +++ b/include/flucoma/algorithms/public/NMF.hpp @@ -14,8 +14,8 @@ under the European Union’s Horizon 2020 research and innovation programme #include "../util/EigenRandom.hpp" #include "../util/FluidEigenMappings.hpp" #include "../../data/FluidIndex.hpp" -#include "../../data/TensorTypes.hpp" #include "../../data/FluidMemory.hpp" +#include "../../data/TensorTypes.hpp" #include #include @@ -50,7 +50,7 @@ class NMF using namespace _impl; index rank = W0.extent(0); FluidEigenMap W = asEigen(W0); - + ScopedEigenMap h(rank, alloc); h = EigenRandom(rank, RandomSeed{randomSeed}, Range{0.0, 1.0}); ScopedEigenMap v0(x.size(), alloc); diff --git a/include/flucoma/clients/nrt/NMFClient.hpp b/include/flucoma/clients/nrt/NMFClient.hpp index 5cdaf9245..4feaaa940 100644 --- a/include/flucoma/clients/nrt/NMFClient.hpp +++ b/include/flucoma/clients/nrt/NMFClient.hpp @@ -18,8 +18,8 @@ under the European Union’s Horizon 2020 research and innovation programme #include "../../algorithms/public/NMF.hpp" #include "../../algorithms/public/RatioMask.hpp" #include "../../algorithms/public/STFT.hpp" -#include "../../data/FluidTensor.hpp" #include "../../data/FluidMemory.hpp" +#include "../../data/FluidTensor.hpp" #include //for max_element #include #include //for ostringstream @@ -47,7 +47,7 @@ enum NMFParamIndex { kEnvelopesUpdate, kRank, kIterations, - kRandomSeed, + kRandomSeed, kFFT }; @@ -58,7 +58,7 @@ constexpr auto BufNMFParams = defineParameters( LongParam("startChan", "Start Channel", 0, Min(0)), LongParam("numChans", "Number Channels", -1), BufferParam("resynth", "Resynthesis Buffer"), - LongParam("resynthMode","Resynthesise components", 0,Min(0),Max(1)), + LongParam("resynthMode", "Resynthesise components", 0, Min(0), Max(1)), BufferParam("bases", "Bases Buffer"), EnumParam("basesMode", "Bases Buffer Update Mode", 0, "None", "Seed", "Fixed"), @@ -100,7 +100,7 @@ class NMFClient : public FluidBaseClient, public OfflineIn, public OfflineOut index nFrames = get(); index nChannels = get(); auto rangeCheck = bufferRangeCheck(get().get(), get(), - nFrames, get(), nChannels); + nFrames, get(), nChannels); if (!rangeCheck.ok()) return rangeCheck; From 644e9a34e57f2ca1785baa98a9ac16da1242ae25 Mon Sep 17 00:00:00 2001 From: Pierre Alexandre Tremblay Date: Fri, 28 Nov 2025 14:17:59 +0100 Subject: [PATCH 5/6] two more clients needed updating --- include/flucoma/clients/rt/NMFFilterClient.hpp | 6 ++++-- include/flucoma/clients/rt/NMFMatchClient.hpp | 14 +++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/include/flucoma/clients/rt/NMFFilterClient.hpp b/include/flucoma/clients/rt/NMFFilterClient.hpp index 85c8c9980..6a1a455fa 100644 --- a/include/flucoma/clients/rt/NMFFilterClient.hpp +++ b/include/flucoma/clients/rt/NMFFilterClient.hpp @@ -22,12 +22,13 @@ namespace fluid { namespace client { namespace nmffilter { -enum NMFFilterIndex { kFilterbuf, kMaxRank, kIterations, kFFT }; +enum NMFFilterIndex { kFilterbuf, kMaxRank, kIterations, kRandomSeed, kFFT }; constexpr auto NMFFilterParams = defineParameters( InputBufferParam("bases", "Bases Buffer"), LongParamRuntimeMax("maxComponents", "Maximum Number of Components", 20, Min(1)), LongParam("iterations", "Number of Iterations", 10, Min(1)), + LongParam("seed", "Random Seed", -1), FFTParam("fftSettings", "FFT Settings", 1024, -1, -1)); class NMFFilterClient : public FluidBaseClient, public AudioIn, public AudioOut @@ -103,7 +104,8 @@ class NMFFilterClient : public FluidBaseClient, public AudioIn, public AudioOut [&](ComplexMatrixView in, ComplexMatrixView out) { algorithm::STFT::magnitude(in, tmpMagnitude); mNMF.processFrame(tmpMagnitude.row(0), tmpFilt, tmpOut, - get(), tmpEstimate.row(0), c.allocator()); + get(), tmpEstimate.row(0), + get(), c.allocator()); mMask.init(tmpEstimate); for (index i = 0; i < rank; ++i) { diff --git a/include/flucoma/clients/rt/NMFMatchClient.hpp b/include/flucoma/clients/rt/NMFMatchClient.hpp index 0d5774729..89f3d7a66 100644 --- a/include/flucoma/clients/rt/NMFMatchClient.hpp +++ b/include/flucoma/clients/rt/NMFMatchClient.hpp @@ -25,6 +25,7 @@ enum NMFMatchParamIndex { kFilterbuf, kMaxRank, kIterations, + kRandomSeed, kFFT }; @@ -33,6 +34,7 @@ constexpr auto NMFMatchParams = defineParameters( LongParamRuntimeMax("maxComponents", "Maximum Number of Components", 20, Min(1)), LongParam("iterations", "Number of Iterations", 10, Min(1)), + LongParam("seed", "Random Seed", -1), FFTParam("fftSettings", "FFT Settings", 1024, -1, -1)); class NMFMatchClient : public FluidBaseClient, public AudioIn, public ControlOut @@ -104,14 +106,16 @@ class NMFMatchClient : public FluidBaseClient, public AudioIn, public ControlOut for (index i = 0; i < filter.rows(); ++i) filter.row(i) <<= filterBuffer.samps(i); - mSTFTProcessor.processInput(get(), input, c, [&](ComplexMatrixView in) { - algorithm::STFT::magnitude(in, mags); - mNMF.processFrame(mags.row(0), filter, activations, - 10, FluidTensorView{nullptr, 0, 0}, c.allocator()); - }); output[0](Slice(0,rank)) <<= activations; output[0](Slice(rank,get().max() - rank)).fill(0); + mSTFTProcessor.processInput( + get(), input, c, [&](ComplexMatrixView in) { + algorithm::STFT::magnitude(in, mags); + mNMF.processFrame(mags.row(0), filter, activations, 10, + FluidTensorView{nullptr, 0, 0}, + get(), c.allocator()); + }); } } From 3bc22a6ac109eea2d7ba3329e1482191bb2ac77f Mon Sep 17 00:00:00 2001 From: Owen Green Date: Fri, 28 Nov 2025 14:32:54 +0000 Subject: [PATCH 6/6] Add NMF::processFrame random seed test --- tests/algorithms/public/TestNMF.cpp | 34 +++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/tests/algorithms/public/TestNMF.cpp b/tests/algorithms/public/TestNMF.cpp index 3b7d742a5..4c139e706 100644 --- a/tests/algorithms/public/TestNMF.cpp +++ b/tests/algorithms/public/TestNMF.cpp @@ -6,11 +6,13 @@ #include #include +namespace fluid { + TEST_CASE("NMF is repeatable with user-supplied random seed") { - using fluid::algorithm::NMF; - using Tensor = fluid::FluidTensor; + using algorithm::NMF; + using Tensor = FluidTensor; NMF algo; Tensor input{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; @@ -42,3 +44,31 @@ TEST_CASE("NMF is repeatable with user-supplied random seed") REQUIRE_THAT(Vs[1], !RangeEquals(Vs[2])); } } + +TEST_CASE("NMF processFrame() is repeatable with user-supplied random seed") +{ + using fluid::algorithm::NMF; + using Tensor = fluid::FluidTensor; + using Vector = fluid::FluidTensor; + NMF algo; + + Vector input{{1, 0, 1, 0}}; + Tensor bases{{0, 0, 1, 0}, {1, 0, 0, 0}}; + Vector v(4); + + std::vector outputs(3, Vector(2)); + + index nIter{0}; + algo.processFrame(input, bases, outputs[0], nIter, v, 42, + FluidDefaultAllocator()); + algo.processFrame(input, bases, outputs[1], nIter, v, 42, + FluidDefaultAllocator()); + algo.processFrame(input, bases, outputs[2], nIter, v, 7863, + FluidDefaultAllocator()); + + using Catch::Matchers::RangeEquals; + + REQUIRE_THAT(outputs[1], RangeEquals(outputs[0])); + REQUIRE_THAT(outputs[1], !RangeEquals(outputs[2])); +} +} // namespace fluid \ No newline at end of file