From d2f1605c974fb24e1b2b966391819716828ba49f Mon Sep 17 00:00:00 2001 From: SE Choi Date: Mon, 18 May 2026 00:30:46 +0200 Subject: [PATCH] fix a bug on DecompositionComponent accessor --- src/api/btd.jl | 19 ++++++++++------- src/core/types.jl | 36 +++++++++++++++++++++++++++++--- src/join/cpd_backend.jl | 21 ++++--------------- test/basic_tests.jl | 46 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 27 deletions(-) diff --git a/src/api/btd.jl b/src/api/btd.jl index 94c1b02..3846f4f 100644 --- a/src/api/btd.jl +++ b/src/api/btd.jl @@ -50,6 +50,9 @@ end return solver == :als ? BTDHOSVDMultistartInit() : :alswarm end +@inline _btd_solver_symbol(solver::AbstractSolver) = + solver isa ALSSolver ? :als : solver_symbol(solver) + function _btd_effective_init( solver::Symbol, init, @@ -246,7 +249,9 @@ function btd( restart_seed = nothing, kwargs..., ) where {T<:AbstractFloat,N} - init_resolved = _resolve_btd_init(init, solver) + solver_obj = _solver_object(solver, stepsize; kwargs...) + solver_sym = _btd_solver_symbol(solver_obj) + init_resolved = _resolve_btd_init(init, solver_sym) init_eff = init_resolved == :alswarm ? BTDALSWarmStartInit( @@ -264,12 +269,12 @@ function btd( warm_info = (;) short_circuited = Ref(false) result = with_phase_progress() do - if solver ∉ (:als,) && init_eff isa BTDALSWarmStartInit + if solver_sym ∉ (:als,) && init_eff isa BTDALSWarmStartInit warm = _btd_warm_start_result( model, b, init_eff, - solver; + solver_sym; verbose = verbose, warm_rel_error_gate, ) @@ -285,7 +290,7 @@ function btd( model; init = solve_init, p0 = solve_p0, - solver = solver, + solver = solver_obj, maxiter = maxiter, stepsize = stepsize, tol = tol, @@ -311,15 +316,15 @@ function btd( isempty(propertynames(warm_info)) ? result : _merge_btd_solver_info(result, warm_info) polish_n = if btd_als_polish_maxiter === nothing - solver == :als ? 0 : clamp(maxiter ÷ 2, 20, 500) + solver_sym == :als ? 0 : clamp(maxiter ÷ 2, 20, 500) else btd_als_polish_maxiter end - if polish_n > 0 && solver ∉ (:als,) + if polish_n > 0 && solver_sym ∉ (:als,) result = _polish_btd_with_als( b, result, - solver; + solver_sym; block_method = block_method, block_maxiter = block_maxiter, polish_maxiter = polish_n, diff --git a/src/core/types.jl b/src/core/types.jl index d997073..fb51092 100644 --- a/src/core/types.jl +++ b/src/core/types.jl @@ -1,6 +1,7 @@ # core/types.jl — RankOneTensor, CPDResult, ApproxResult, TuckerResult export RankOneTensor, CPDResult, + CPDComponent, ApproxResult, BTDResult, TuckerResult, @@ -37,6 +38,20 @@ struct RankOneTensor{T<:AbstractFloat} vectors::Vector{Vector{T}} end +""" + CPDComponent{T} + +Lazy public component wrapper for CPD-backed join results. + +Each component stores its component-specific rank-one point and decoded +rank-one tensor data. Dense tensors are reconstructed on demand through +`component.tensor` or [`tensor(component)`](@ref). +""" +struct CPDComponent{T<:AbstractFloat,P} + point::P + component::RankOneTensor{T} +end + """ CPDResult{T} @@ -244,7 +259,8 @@ end Return the factor vectors of a rank-one tensor component. """ vectors(c::RankOneTensor) = c.vectors -kind(c::DecompositionComponent) = c.kind +kind(::CPDComponent) = :Segre +kind(c::DecompositionComponent) = typeof(c.manifold) """ point(x) @@ -254,6 +270,7 @@ Return the optimization point stored in a result or component. Supported inputs include [`CPDResult`](@ref), [`ApproxResult`](@ref), [`BTDResult`](@ref), and [`DecompositionComponent`](@ref). """ +point(c::CPDComponent) = c.point point(c::DecompositionComponent) = c.point """ @@ -261,6 +278,8 @@ point(c::DecompositionComponent) = c.point Reconstruct a component into its ambient tensor representation. """ +reconstruct(c::CPDComponent) = reconstruct_cp_rank1(λ(c.component), vectors(c.component)) +tensor(c::CPDComponent) = reconstruct(c) tensor(c::DecompositionComponent) = c.tensor """ @@ -268,14 +287,25 @@ tensor(c::DecompositionComponent) = c.tensor Return the Tucker core stored in a Tucker component. """ -core(c::DecompositionComponent) = c.core +core(c::DecompositionComponent) = _component_core(c.point) """ factors(x) Return factor matrices for a CP or Tucker result/component. """ -factors(c::DecompositionComponent) = c.factors +weights(c::CPDComponent) = [λ(c.component)] +factors(c::CPDComponent) = [reshape(v, :, 1) for v in vectors(c.component)] +factors(c::DecompositionComponent) = _component_factors(c.point) + +function Base.getproperty(c::CPDComponent, name::Symbol) + name === :kind && return kind(c) + name === :weights && return weights(c) + name === :factors && return factors(c) + name === :tensor && return reconstruct(c) + return getfield(c, name) +end + point(r::CPDResult) = cpd_point(r) point(r::ApproxResult) = r.point point(r::BTDResult) = r.point diff --git a/src/join/cpd_backend.jl b/src/join/cpd_backend.jl index 62ba488..5b9f4e2 100644 --- a/src/join/cpd_backend.jl +++ b/src/join/cpd_backend.jl @@ -29,7 +29,7 @@ function JoinModel( scale_by_lambda = scale_by_lambda, lambda_eps = lambda_eps, nonnegative = nonnegative, - use_pullback_metric = use_pullback_metric, + use_pullback_metric = (geometry_eff == :squaring_metric) || use_pullback_metric, pullback_eps = pullback_eps, ) b = CPDBackend(inner) @@ -172,15 +172,7 @@ function extract_components(model::JoinModel{<:AbstractFloat,<:CPDBackend}, p) λ, U = _decode_nonnegative_cpd(m, λ̃, Ũ) components_from_factors(λ, U) end : unpack_point_rankr_components(p, m.dims, m.r) - return [ - ( - kind = :Segre, - point = p, - weights = [c.λ], - factors = [reshape(c.vectors[j], :, 1) for j = 1:length(c.vectors)], - tensor = reconstruct_cp_rank1(c.λ, c.vectors), - ) for c in comps - ] + return [CPDComponent(pack_point_rank1(c.λ, c.vectors), c) for c in comps] elseif m isa Rank1CPDModel λ, U = unpack_point_rank1(p, m.dims) if m.nonnegative @@ -192,13 +184,8 @@ function extract_components(model::JoinModel{<:AbstractFloat,<:CPDBackend}, p) U = [u .^ 2 for u in U] end end - return [( - kind = :Segre, - point = p, - weights = [λ], - factors = [reshape(U[j], :, 1) for j = 1:length(U)], - tensor = reconstruct_cp_rank1(λ, U), - )] + c = RankOneTensor(λ, U) + return [CPDComponent(pack_point_rank1(λ, U), c)] end throw( ArgumentError( diff --git a/test/basic_tests.jl b/test/basic_tests.jl index e49344a..0c5c28e 100644 --- a/test/basic_tests.jl +++ b/test/basic_tests.jl @@ -223,6 +223,21 @@ end @test size(Ahat) == size(A) @test rel_error(A, res) == TensorKitchen.relative_frobenius_error(A, Ahat) @test rel_error(A, Ahat) == rel_error(A, res) + + model = JoinModel(A, r; geometry = :canonical) + p = TensorKitchen.initial_point(model, :random) + comps = TensorKitchen.extract_components(model, p) + @test length(comps) == r + @test comps[1] isa TensorKitchen.CPDComponent + @test !(:tensor in fieldnames(typeof(comps[1]))) + @test comps[1].point !== p + @test comps[1].kind == :Segre + @test size(comps[1].tensor) == size(A) + Xparts = zero(A) + for c in comps + Xparts .+= c.tensor + end + @test TensorKitchen.cost(model, p) ≈ 0.5 * sum(abs2, A .- Xparts) end @testset "frontend defaults through public APIs" begin @@ -562,6 +577,10 @@ end @test all( isapprox(norm(q_sep.factors[m][:, k]), 1; atol = 1e-10) for m = 1:3 for k = 1:r ) + U_sep, λ_sep = normalize_components(U, λ, SeparateLambdaNormalization()) + @test U_sep isa Vector{Matrix{Float64}} + @test λ_sep isa Vector{Float64} + @test reconstruct_cpd_rankr(λ_sep, U_sep) ≈ A_ref q_last = normalize_components(CPDPoint(λ, U), :last_mode) @test reconstruct_cpd_rankr(q_last.lambda, q_last.factors) ≈ A_ref @@ -886,6 +905,11 @@ end ) @test TensorKitchen.manifold(model_sm) isa ProductManifold @test all(m -> m isa SqEuclidean, TensorKitchen.manifold(model_sm).manifolds) + join_model_sm = JoinModel(A, r; nonnegative = true, geometry = :squaring_metric) + @test all( + m -> m isa SqEuclidean, + TensorKitchen.manifold(TensorKitchen.cpd_model(join_model_sm)).manifolds, + ) @test getproperty(model_sm, :scale_by_lambda) == false res_nn_sm = cpd( A, @@ -1712,6 +1736,28 @@ end length(res_btd_tsd.solver_info.line_search_trial_history) @test isfinite(res_btd_tsd.rel_error) @test res_btd_tsd.rel_error ≈ norm(A - reconstruct(res_btd_tsd)) / norm(A) + res_btd_tsd_object = btd( + A, + 2, + (2, 2, 2); + solver = BTDTSDSolver(stepsize = 1.0), + init = :hosvd_multistart, + maxiter = 1, + schedule = :cyclic, + btd_als_polish_maxiter = 0, + tol = 1e-6, + verbose = false, + ) + @test res_btd_tsd_object isa BTDResult + @test res_btd_tsd_object.solver == :btd_tsd + @test_throws ArgumentError btd( + A, + 2, + (2, 2, 2); + solver = :tsd, + maxiter = 1, + verbose = false, + ) manifolds = TensorKitchen._as_join_manifold_tuple(TuckerJoin(size(A), (2, 2, 2), 2)) backend = TensorKitchen._sum_backend_instance(TensorKitchen.BTDBackend, manifolds, A)