Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions src/api/btd.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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,
)
Expand All @@ -285,7 +290,7 @@ function btd(
model;
init = solve_init,
p0 = solve_p0,
solver = solver,
solver = solver_obj,
maxiter = maxiter,
stepsize = stepsize,
tol = tol,
Expand All @@ -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,
Expand Down
36 changes: 33 additions & 3 deletions src/core/types.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# core/types.jl — RankOneTensor, CPDResult, ApproxResult, TuckerResult
export RankOneTensor,
CPDResult,
CPDComponent,
ApproxResult,
BTDResult,
TuckerResult,
Expand Down Expand Up @@ -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}

Expand Down Expand Up @@ -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)
Expand All @@ -254,28 +270,42 @@ 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

"""
tensor(c::DecompositionComponent)

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

"""
core(x)

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
Expand Down
21 changes: 4 additions & 17 deletions src/join/cpd_backend.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down
46 changes: 46 additions & 0 deletions test/basic_tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
Loading