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
2 changes: 1 addition & 1 deletion docs/src/invariants.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ For symmetric support components without a nonzero diagonal entry, use
[`cover`](@ref) is covariant on any support: positive diagonal rescaling of
`A` rescales the cover products identically wherever `A` is nonzero. This holds
for both the default transversal start (square, structurally nonsingular `A`)
and the `:covariant` start, and for any number of conjugate-gradient refinement
and the `:unconstrained` start, and for any number of conjugate-gradient refinement
iterations (`cgiter`):

```jldoctest covariance
Expand Down
2 changes: 1 addition & 1 deletion docs/src/objectives.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ julia> a, b = cover(A); scaledcond(a, b)
```

For a square, structurally nonsingular matrix, [`cover`](@ref) is tight on a
transversal by default (`start=:transversal`). The `:covariant` and `:geomean`
transversal by default (`start=:transversal`). The `:unconstrained` and `:geomean`
starts, and the `_min` solvers, do not guarantee this. Tightness on a
transversal prevents only this one cause of ill-conditioning, and it comes at
the cost of a somewhat larger objective than that of the minimal cover.
2 changes: 1 addition & 1 deletion src/dense_heuristic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ function _symcover_dense!(a::AbstractVector, A::AbstractMatrix, ::Type{T}, maxit
end

# `cover!` over a dense log-magnitude grid, up to but not including the balance
# convention. `start` is `:covariant` or `:geomean`; `fname` and `hint` label the
# convention. `start` is `:unconstrained` or `:geomean`; `fname` and `hint` label the
# error thrown when the scales exceed the exponent range of `T`.
function _cover_dense!(a::AbstractVector, b::AbstractVector, A::AbstractMatrix,
::Type{T}, maxiter::Int, cgiter::Int, start::Symbol,
Expand Down
40 changes: 24 additions & 16 deletions src/heuristic_covers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,14 @@ entry is covered, and then applies `maxiter` improvement passes.
transversal of the square matrix `A` (see the extended help), a heuristic
counterpart of [`cover_transversal`](@ref). `A` must be square and structurally
nonsingular. Defaults: `cgiter=20`, `maxiter=5`.
- `:covariant` ensures the result is scale-covariant (typically at the cost of
permutation-equivariance). Defaults: `cgiter=16`, `maxiter=3`.
- `:unconstrained` approximates the unconstrained least-squares fit of the log
scales and is scale-covariant (typically at the cost of
permutation-equivariance). `:covariant` is a deprecated alias. Defaults: `cgiter=16`, `maxiter=3`.
- `:geomean` makes the result permutation-equivariant up to floating-point
round-off (typically at the cost of scale-covariance). Defaults as for
`:covariant`.
`:unconstrained`.
- `:auto` (the default) uses `:transversal` when `A` is square and
structurally nonsingular, and `:covariant` otherwise.
structurally nonsingular, and `:unconstrained` otherwise.

The factors use the per-component balance convention described by
[`cover_min`](@ref). `ϕ` is accepted for API compatibility but is ignored.
Expand Down Expand Up @@ -147,7 +148,7 @@ the largest product `∏ᵢ |A[i,σ(i)]|` over permutations `σ`. Equality holds
exactly when the cover is tight on every entry of every transversal attaining
`π*`, and these covers maximize `|det(A ./ (a .* b'))|`. A cover that leaves
such a transversal loose can make the scaled matrix far more ill-conditioned
than necessary. The `:transversal` start prevents this; the `:covariant` start
than necessary. The `:transversal` start prevents this; the `:unconstrained` start
does not.

The `:transversal` start first finds a maximum-product transversal by shortest
Expand All @@ -159,17 +160,17 @@ iterations on the least-squares fit, a feasibility boost, and `maxiter`
projected Gauss–Seidel sweeps that keep every entry covered. The transversal
search typically dominates the cost.

With `start=:covariant`, initialization and refinement approximate the
With `start=:unconstrained`, initialization and refinement approximate the
least-squares fit of `log(a[i]) + log(b[j])` to `log(abs(A[i, j]))` over
nonzero entries; coverage is then restored by boosting and `maxiter`
tightening passes. The initialization alone gives the exact least-squares fit
when each connected component of the support is a tree or is complete
bipartite, so complete support needs no refinement. Each refinement iteration
makes one pass over the support. The `:covariant` and `:geomean` starts
makes one pass over the support. The `:unconstrained` and `:geomean` starts
coincide on complete support, where the geometric mean already solves the
least-squares problem.

With `start=:transversal` or `:covariant` the cover products are
With `start=:transversal` or `:unconstrained` the cover products are
scale-covariant on the support: for positive diagonal `D1`, `D2`, covering
`D1 * A * D2` multiplies each supported product by `D1[i, i] * D2[j, j]`. This
holds in exact arithmetic for every `cgiter` and `maxiter`. In floating point,
Expand Down Expand Up @@ -223,7 +224,7 @@ end
function _cover!(a::AbstractVector, b::AbstractVector, A::AbstractMatrix;
maxiter::Union{Int,Nothing}=nothing, cgiter::Union{Int,Nothing}=nothing,
start::Symbol=:auto, fname::Symbol=:cover)
_check_cover_start(start)
start = _normalize_cover_start(start)
T = float(promote_type(eltype(a), eltype(b)))
if start === :auto || start === :transversal
if length(axes(A, 1)) == length(axes(A, 2))
Expand All @@ -237,18 +238,18 @@ function _cover!(a::AbstractVector, b::AbstractVector, A::AbstractMatrix;
elseif start === :transversal
throw(ArgumentError("$fname with start=:transversal requires a square matrix, got size $(size(A))"))
end
start = :covariant
start = :unconstrained
end
cgiter = something(cgiter, 16)
maxiter = something(maxiter, 3)
cgiter >= 0 || throw(ArgumentError("cgiter must be nonnegative, got $cgiter"))
# The geometric-mean start propagates scales only locally.
hint = fname === :cover && start === :covariant ? "; `start=:geomean` may also avoid it" : ""
hint = fname === :cover && start === :unconstrained ? "; `start=:geomean` may also avoid it" : ""
if _use_dense_grid(A, T)
_cover_dense!(a, b, A, T, maxiter, cgiter, start, fname, hint)
else
sup = flat_support(A, T)
if start === :covariant
if start === :unconstrained
covariant_start!(a, b, sup; fname, hint)
else
unconstrained_min!(AbsLog{2}(), a, b, sup)
Expand Down Expand Up @@ -279,11 +280,18 @@ end
# ============================================================

# The starting points `cover!` accepts.
const COVER_STARTS = (:auto, :transversal, :covariant, :geomean)
const COVER_STARTS = (:auto, :transversal, :unconstrained, :geomean)

_check_cover_start(start::Symbol) =
start in COVER_STARTS ? nothing :
throw(ArgumentError("unknown start :$start; expected one of :auto, :transversal, :covariant, :geomean"))
# Validate `start` and map the deprecated `:covariant` to `:unconstrained`.
function _normalize_cover_start(start::Symbol)
if start === :covariant
Base.depwarn("`start=:covariant` is deprecated; use `start=:unconstrained`", :cover)
return :unconstrained
end
start in COVER_STARTS ||
throw(ArgumentError("unknown start :$start; expected one of :auto, :transversal, :unconstrained, :geomean"))
return start
end

# Matrix support flattened in traversal order as row, column, and `log|A_ij|`
# arrays.
Expand Down
16 changes: 11 additions & 5 deletions src/initializers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Default starts for nonconvex `AbsLinear` solvers.
const SYMCOVER_MIN_STRATEGIES = (:hardcover, :geomean, :leaveout)
const COVER_MIN_STRATEGIES = (:hardcover, :covariant)
const COVER_MIN_STRATEGIES = (:hardcover, :unconstrained)

# ============================================================
# Public interface
Expand Down Expand Up @@ -78,8 +78,10 @@ Build an asymmetric starting point for [`cover_min`](@ref) or
`strategy` names the point:

- `:geomean` — geometric means of the nonzero entries in each row and column.
- `:covariant` — the scale-covariant start of [`cover`](@ref), without
- `:unconstrained` — the scale-covariant start of [`cover`](@ref) that
approximates the unconstrained least-squares fit, without
conjugate-gradient refinement. It agrees with `:geomean` on complete support.
`:covariant` is a deprecated alias.
- `:hardcover` — the result of [`cover`](@ref), which is already a cover, so
`feasible` is ignored. Additional keywords (`maxiter`, `cgiter`, `start`) are
passed to `cover`; the other strategies accept none.
Expand Down Expand Up @@ -110,18 +112,22 @@ function initialize_cover!(a::AbstractVector, b::AbstractVector, A::AbstractMatr
strategy::Symbol=:hardcover, feasible::Symbol=:inflate, kwargs...)
axes(A, 1) == eachindex(a) || throw(DimensionMismatch("indices of `a` must match row-indexing of `A`, got eachindex(a)=$(string(eachindex(a))), axes(A, 1)=$(string(axes(A, 1)))"))
axes(A, 2) == eachindex(b) || throw(DimensionMismatch("indices of `b` must match column-indexing of `A`, got eachindex(b)=$(string(eachindex(b))), axes(A, 2)=$(string(axes(A, 2)))"))
if strategy === :covariant
Base.depwarn("`strategy=:covariant` is deprecated; use `strategy=:unconstrained`", :initialize_cover)
strategy = :unconstrained
end
if strategy === :hardcover
cover!(a, b, A; kwargs...)
elseif strategy === :geomean
_reject_kwargs(strategy, kwargs)
unconstrained_min!(AbsLog{2}(), a, b, A)
elseif strategy === :covariant
elseif strategy === :unconstrained
_reject_kwargs(strategy, kwargs)
covariant_start!(a, b, flat_support(A, float(promote_type(eltype(a), eltype(b)))); fname=:initialize_cover)
elseif strategy === :leaveout || strategy === :diagfeasible
throw(ArgumentError("strategy=:$strategy has no asymmetric formulation; expected one of :hardcover, :geomean, :covariant"))
throw(ArgumentError("strategy=:$strategy has no asymmetric formulation; expected one of :hardcover, :geomean, :unconstrained"))
else
throw(ArgumentError("unknown strategy :$strategy; expected one of :hardcover, :geomean, :covariant"))
throw(ArgumentError("unknown strategy :$strategy; expected one of :hardcover, :geomean, :unconstrained"))
end
_make_feasible!(feasible, a, b, A)
# Restore the package's balance convention after a selective boost.
Expand Down
4 changes: 2 additions & 2 deletions src/minimal_covers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ Supported ϕ values, `stats`, and the failure behavior are as for
- `AbsLog{1}()`: requires JuMP and HiGHS.
- `AbsLinear{1}()` and `AbsLinear{2}()`: require JuMP and Ipopt and return the
best local minimum found from `strategies`, a collection of
[`initialize_cover`](@ref) strategy names (default `(:hardcover, :covariant)`).
[`initialize_cover`](@ref) strategy names (default `(:hardcover, :unconstrained)`).

The `AbsLog` objectives are convex in the logarithms of the scales, so their
local minima are global. When the `AbsLog{1}` minimum is not unique, the method
Expand Down Expand Up @@ -2178,7 +2178,7 @@ function _cover_min_abslog2(A::AbstractMatrix; κ::Real=AL_PENALTY, maxouter::In
use_grid = use_woodbury && !tt
# Start the LSQR iteration from the heuristic cover.
if start === nothing && use_lsqr && maxouter > 0 && !tt
start = _cover!(similar(Array{T}, axr), similar(Array{T}, axc), A; start=:covariant, fname=:cover_min)
start = _cover!(similar(Array{T}, axr), similar(Array{T}, axc), A; start=:unconstrained, fname=:cover_min)
end
x0 = nothing
# Woodbury uses a grid and the transversal-tight Woodbury solve a difference
Expand Down
2 changes: 1 addition & 1 deletion src/powermean.jl
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ end
# minimizer then has `a[i]*b[j] == a[j]*b[i]` on the support, and the balance
# convention selects `b == a`. The test is exact, since a nearly symmetric matrix
# has a genuinely asymmetric minimizer.
# The `:covariant` start makes a truncated iteration scale-covariant as well.
# The `:unconstrained` start makes a truncated iteration scale-covariant as well.
function _soft_cover_powermean(ϕ::PowerMean, A::AbstractMatrix, fname::Symbol; kwargs...)
if _abs_symmetric_exact(A)
a, stats = _soft_symcover_powermean(ϕ, A, fname; kwargs...)
Expand Down
8 changes: 4 additions & 4 deletions src/soft_covers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ Supported penalties and their keywords are:
- `AbsLinear{1}()`, `AbsLinear{2}()`: requires JuMP and Ipopt. The objective is
not convex, so several starts are each refined to a local minimum, and the
result with the least objective is returned. `starts` (default 4) is the
number of starts: the boosted covariant start of [`cover`](@ref) and the hard cover
number of starts: the boosted unconstrained start of [`cover`](@ref) and the hard cover
tightened from it, then log-normal perturbations of the first with spread `σ`
(default 2.0; `sigma` is an alias), drawn from `rng` (default
`MersenneTwister(0)`). A perturbed start whose refinement throws a
Expand Down Expand Up @@ -404,14 +404,14 @@ function _soft_symcover_abslinear(ϕ::AbsLinear, A::AbstractMatrix, starts::Int,
A, starts, σ, rng)
end

# Asymmetric `AbsLinear` starts: boosted covariant start, tightened cover, and
# Asymmetric `AbsLinear` starts: boosted unconstrained start, tightened cover, and
# log-normal perturbations. Every start is covariant, so the selection is too.
function _soft_cover_abslinear_inits(A::AbstractMatrix, starts::Int, σ::Real, rng)
T = float(real(eltype(A)))
ag, bg = initialize_cover(A; strategy=:covariant, feasible=:boost)
ag, bg = initialize_cover(A; strategy=:unconstrained, feasible=:boost)
labels = [:boost]
inits = [(copy(ag), copy(bg))]
# Reuse the covariant start and boost pass when constructing the hard start.
# Reuse the unconstrained start and boost pass when constructing the hard start.
length(inits) < starts && (push!(labels, :hardcover); push!(inits, tighten_cover!(copy(ag), copy(bg), A)))
nfixed, k = length(inits), 0
while length(inits) < starts
Expand Down
6 changes: 3 additions & 3 deletions test/extensions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ end
# No start on the menu beats the driver.
for ϕ in (AbsLinear{1}(), AbsLinear{2}())
a, b, _ = soft_cover(ϕ, A)
for strategy in (:hardcover, :covariant)
for strategy in (:hardcover, :unconstrained)
sa, sb, _ = soft_cover!(ϕ, initialize_cover(A; strategy, feasible=:none)..., A)
@test cover_objective(ϕ, a, b, A) <= cover_objective(ϕ, sa, sb, A) * (1 + 1e-6) + 1e-8
end
Expand Down Expand Up @@ -344,7 +344,7 @@ end

ab, bb, _ = cover_min(ϕ, Aasym)
@test iscover(ab, bb, Aasym; rtol=1e-6)
for strategy in (:hardcover, :covariant)
for strategy in (:hardcover, :unconstrained)
sa, sb, _ = cover_min!(ϕ, initialize_cover(Aasym; strategy)..., Aasym)
@test cover_objective(ϕ, ab, bb, Aasym) <=
cover_objective(ϕ, sa, sb, Aasym) * (1 + 1e-6) + 1e-8
Expand Down Expand Up @@ -617,7 +617,7 @@ end
_, ms = symcover_min(ϕ, [4.0 0.0; 0.0 9.0])
@test ms.labels == [:hardcover, :geomean]
a, b, ms = @inferred cover_min(AbsLinear{1}(), Aasym)
@test ms.labels == [:hardcover, :covariant]
@test ms.labels == [:hardcover, :unconstrained]
@test ms.objectives[ms.selected] == cover_objective(AbsLinear{1}(), a, b, Aasym)

# Random multistart: failed random starts are recorded and left out.
Expand Down
Loading
Loading