From 127b6b69f028731a4373abca83bc186b2051d0c0 Mon Sep 17 00:00:00 2001 From: SE Choi Date: Mon, 18 May 2026 04:14:49 +0200 Subject: [PATCH] README examples tests and trim Project.toml deps --- Project.toml | 6 +-- README.md | 8 ++-- test/basic_tests.jl | 107 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 8 deletions(-) diff --git a/Project.toml b/Project.toml index 0049dd1..bd4414c 100644 --- a/Project.toml +++ b/Project.toml @@ -3,7 +3,6 @@ uuid = "3630a16b-0f2f-4d88-afbf-c7d59eccf553" version = "0.1.0" [deps] -JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Manifolds = "1cead3c2-87b3-11e9-0ccd-23c62b72b94e" ManifoldsBase = "3362f125-f0bb-47a3-aa74-596ffd7ef2fb" @@ -11,11 +10,9 @@ Manopt = "0fc0a36d-df90-57f3-8f93-d78a9fc72bb5" ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" RecursiveArrayTools = "731186ca-8d62-57ce-b412-fbd966d074cd" -Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" TensorOperations = "6aa20fa7-93e2-5fca-9bc0-fbd0db3c71a2" [compat] -JuliaFormatter = "1.0.33" Manifolds = "0.11.20" ManifoldsBase = "2.3.5" Manopt = "0.5.37" @@ -25,7 +22,8 @@ TensorOperations = "5.6" julia = "1.10" [extras] +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test"] +test = ["Statistics", "Test"] diff --git a/README.md b/README.md index 86eecb9..deef2a8 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Â = reconstruct(res) Per default, `cpd` finds the initial point by running ALS and then refines it using Riemannian optimization. To use ALS-only one can set the `:solver` flag: ```julia cpd(A, r; solver = :als) -```` +``` For a detailed overview on all the options see the documentation.
@@ -138,7 +138,7 @@ CPDResult{Float64} or run ```julia -nncpd(B, r) +nn_res = nncpd(B, r) ``` We access the decomposition as follows. @@ -172,13 +172,13 @@ BTDResult{Float64} The blocks of `btd_res` can be obtained as follows: ```julia -blocks = blocks(btd_res) +btd_blocks = blocks(btd_res) ``` Each block is represented as a Tucker decomposition, so we can access its core and factor matrices via: ```julia -blk = blocks[1] +blk = btd_blocks[1] core(blk) factors(blk) ``` diff --git a/test/basic_tests.jl b/test/basic_tests.jl index 0c5c28e..3ba6fe3 100644 --- a/test/basic_tests.jl +++ b/test/basic_tests.jl @@ -2151,6 +2151,113 @@ end @test res_btd_zero.rel_error ≥ 0 end +@testset "README workflow examples stay executable" begin + rng = MersenneTwister(5151) + A = randn(rng, 8, 6, 5) + r = 3 + + res = cpd(A, r; init = TuckerInit(), maxiter = 5, verbose = false) + @test res isa CPDResult + λ = weights(res) + U = factors(res) + @test length(λ) == r + @test length(U) == ndims(A) + Â = reconstruct(res) + @test size(Â) == size(A) + @test cpd(A, r; solver = :als, maxiter = 3, verbose = false) isa CPDResult + + mlrank = (4, 3, 2) + tucker_res = tucker(A, mlrank) + @test tucker_res isa TuckerResult + @test size(core(tucker_res)) == mlrank + @test length(factors(tucker_res)) == ndims(A) + + B = abs.(A) + nn_res = nncpd(B, r; solver = :als, maxiter = 3, verbose = false) + @test nn_res isa CPDResult + @test length(weights(nn_res)) == r + @test length(factors(nn_res)) == ndims(B) + @test all(w -> w >= -1e-12, weights(nn_res)) + @test all(F -> all(x -> x >= -1e-12, F), factors(nn_res)) + @test cpd(B, r; nonnegative = true, solver = :als, maxiter = 3, verbose = false) isa + CPDResult + + block_count = 2 + block_rank = (3, 2, 2) + btd_res = btd( + A, + block_count, + block_rank; + solver = :als, + init = BTDHOSVDMultistartInit(2; screening_steps = 0, block_maxiter = 1), + maxiter = 2, + block_maxiter = 1, + verbose = false, + ) + @test btd_res isa BTDResult + btd_blocks = blocks(btd_res) + @test length(btd_blocks) == block_count + blk = btd_blocks[1] + @test size(core(blk)) == block_rank + @test length(factors(blk)) == ndims(A) + + p = [1.2, 0.4] + S = Manifolds.Sphere(1) + join_res = approx( + (S, S), + p; + init = :deterministic, + solver = :rgd, + maxiter = 60, + verbose = false, + ) + @test join_res isa ApproxResult + @test length(components(join_res)) == 2 + @test size(reconstruct(join_res)) == size(p) +end + +@testset "result conversion consistency for CPD, NNCPD, and BTD" begin + rng = MersenneTwister(6262) + A = randn(rng, 6, 5, 4) + r = 2 + + res_cpd = cpd(A, r; solver = :als, init = TuckerInit(), maxiter = 4, verbose = false) + Ahat_cpd = reconstruct(res_cpd) + @test Ahat_cpd ≈ reconstruct_cpd_rankr(weights(res_cpd), factors(res_cpd)) + @test rel_error(A, res_cpd) ≈ rel_error(A, Ahat_cpd) + @test length(components(res_cpd)) == r + @test factors(res_cpd) == TensorKitchen.factors_from_components(components(res_cpd)) + + B = abs.(A) + res_nn = nncpd(B, r; solver = :als, init = TuckerInit(), maxiter = 4, verbose = false) + Ahat_nn = reconstruct(res_nn) + @test Ahat_nn ≈ reconstruct_cpd_rankr(weights(res_nn), factors(res_nn)) + @test rel_error(B, res_nn) ≈ rel_error(B, Ahat_nn) + @test length(components(res_nn)) == r + @test all(w -> w >= -1e-12, weights(res_nn)) + @test all(F -> all(x -> x >= -1e-12, F), factors(res_nn)) + + res_btd = btd( + A, + 2, + (2, 2, 2); + solver = :als, + init = BTDHOSVDMultistartInit(2; screening_steps = 0, block_maxiter = 1), + maxiter = 3, + block_maxiter = 1, + verbose = false, + ) + Ahat_btd = reconstruct(res_btd) + block_sum = zero(A) + for blk in blocks(res_btd) + @test size(core(blk)) == (2, 2, 2) + @test length(factors(blk)) == ndims(A) + block_sum .+= tensor(blk) + end + @test Ahat_btd ≈ block_sum + @test rel_error(A, res_btd) ≈ rel_error(A, Ahat_btd) +end + # ========================================================================= # utils: pack_unpack, cp_init_tucker, tensor_contractions # =========================================================================