11# Objective-minimal hard covers. `AbsLog{2}` is native; extensions provide the
22# other penalties.
33
4+ # ============================================================
5+ # Solver defaults
6+ # ============================================================
7+
8+ # Augmented-Lagrangian defaults. The KKT tolerance is in units of `eps` of the
9+ # working type.
10+ const AL_PENALTY = 1e2
11+ const AL_MAXOUTER = 32
12+ const AL_MAXITER = 40
13+ const AL_TOL_EPS = 1000
14+
15+ # Default storage limit, in bytes, for the LSQR Cholesky preconditioner.
16+ # Tripping this switches to spanning-forest preconditioning, which has no fill
17+ # but needs more iterations for convergence.
18+ const LSQR_FILL_BUDGET = 1 << 30
19+
20+ # Largest predicted numeric-factorization flop count per stored entry of the
21+ # support for which the LSQR Cholesky preconditioner is used. Above it,
22+ # repeated refactorization of the preconditioner costs more than the extra
23+ # LSQR iterations of spanning-forest preconditioning.
24+ const LSQR_FLOP_BUDGET = 8.0e3
25+
26+ # The keywords of the native `AbsLog{2}` solvers, with their defaults: those that
27+ # choose the linear solver, which the soft covers accept, and the full set of the
28+ # minimal covers and `cover_transversal`. Public methods pass user keywords
29+ # through these functions, so that any other keyword throws a `MethodError`; the
30+ # workers `_symcover_min_abslog2` and `_cover_min_abslog2` apply `_auglag_options`
31+ # again to fill in the defaults for internal callers. `tol = nothing` selects the
32+ # tolerance `AL_TOL_EPS * eps` of the working type.
33+ _auglag_linsolve_options (; linsolve:: Symbol = :auto , fillbudget:: Real = LSQR_FILL_BUDGET,
34+ flopbudget:: Real = LSQR_FLOP_BUDGET) = (; linsolve, fillbudget, flopbudget)
35+ _auglag_options (; θ:: Real = AL_PENALTY, maxouter:: Int = AL_MAXOUTER, maxiter:: Int = AL_MAXITER,
36+ tol:: Union{Real,Nothing} = nothing , kwargs... ) =
37+ (; θ, maxouter, maxiter, tol, _auglag_linsolve_options (; kwargs... )... )
38+
439# ============================================================
540# Public interface
641# ============================================================
@@ -36,10 +71,10 @@ solves throw a [`SolverFailure`](@ref MatrixCovers.SolverFailure) instead.
3671# Extended help
3772
3873The native solver accepts `θ` (initial augmented-Lagrangian penalty, default
39- `1e2 `), `maxouter` (multiplier updates, default `32 `; `0` returns the
74+ `$(AL_PENALTY) `), `maxouter` (multiplier updates, default `$(AL_MAXOUTER) `; `0` returns the
4075unconstrained fit, shifted to cover `A`, with `converged(stats) == false` and no
4176warning), `maxiter` (Newton steps per update), `tol` (the tolerance
42- on the KKT residual, default `1000 *eps` of the working type), `fillbudget` and
77+ on the KKT residual, default `$(AL_TOL_EPS) *eps` of the working type), `fillbudget` and
4378`flopbudget` (see below), and `linsolve`:
4479
4580- `:dense` factorizes dense normal equations at O(n³) per Newton step.
@@ -55,8 +90,8 @@ positive multiplier remains on an entry with slack.
5590
5691The budgets choose the preconditioner of `:lsqr`. For `Float64`, it is a sparse
5792Cholesky factorization when the factor's predicted storage is at most
58- `fillbudget` bytes (default `2^30 `) and its predicted flop count is at most
59- `flopbudget` (default `8e3 `) times the number of stored entries in the support
93+ `fillbudget` bytes (default `$(LSQR_FILL_BUDGET) `) and its predicted flop count is at most
94+ `flopbudget` (default `$(LSQR_FLOP_BUDGET) `) times the number of stored entries in the support
6095of `A`, counting each symmetric off-diagonal pair twice, as `nnz` does for a
6196fully stored matrix. Otherwise, and for every other element type, it is a
6297factorization of a maximum-weight spanning forest of the couplings among
@@ -201,42 +236,34 @@ cover_min!(a::AbstractVector, b::AbstractVector, A::AbstractMatrix; kwargs...) =
201236#
202237# Each `θ` stage freezes the weights, solves the normal equations, and uses a
203238# backtracking line search. A final uniform shift restores feasibility.
204- function symcover_min (:: AbsLog{2} , A:: AbstractMatrix ; θ:: Real = AL_PENALTY, maxouter:: Int = AL_MAXOUTER,
205- maxiter:: Int = AL_MAXITER, linsolve:: Symbol = :auto ,
206- fillbudget:: Real = LSQR_FILL_BUDGET, flopbudget:: Real = LSQR_FLOP_BUDGET,
207- tol:: Union{Real,Nothing} = nothing )
208- a, nt = _symcover_min_abslog2 (A; θ, maxouter, maxiter, linsolve, fillbudget, flopbudget, tol)
239+ # The solver keywords of the `AbsLog{2}` methods pass through to
240+ # `_auglag_options`, which holds their defaults.
241+ function symcover_min (:: AbsLog{2} , A:: AbstractMatrix ; kwargs... )
242+ a, nt = _symcover_min_abslog2 (A; _auglag_options (; kwargs... )... )
209243 return a, AugmentedLagrangianStats (A, nt)
210244end
211245
212246# Asymmetric counterpart on stacked log scales `(α; β)`. The solve pins the
213247# global gauge; `_cover_min_abslog2` regularizes and balances the remaining
214248# component gauges.
215- function cover_min (:: AbsLog{2} , A:: AbstractMatrix ; θ:: Real = AL_PENALTY, maxouter:: Int = AL_MAXOUTER,
216- maxiter:: Int = AL_MAXITER, linsolve:: Symbol = :auto ,
217- fillbudget:: Real = LSQR_FILL_BUDGET, flopbudget:: Real = LSQR_FLOP_BUDGET,
218- tol:: Union{Real,Nothing} = nothing )
219- a, b, nt = _cover_min_abslog2 (A; θ, maxouter, maxiter, linsolve, fillbudget, flopbudget, tol)
249+ function cover_min (:: AbsLog{2} , A:: AbstractMatrix ; kwargs... )
250+ a, b, nt = _cover_min_abslog2 (A; _auglag_options (; kwargs... )... )
220251 return a, b, AugmentedLagrangianStats (A, nt)
221252end
222253
223254# The convex objective has the same result from every valid start.
224- function symcover_min! (:: AbsLog{2} , a:: AbstractVector , A:: AbstractMatrix ; θ:: Real = AL_PENALTY,
225- maxouter:: Int = AL_MAXOUTER, maxiter:: Int = AL_MAXITER, linsolve:: Symbol = :auto ,
226- fillbudget:: Real = LSQR_FILL_BUDGET, flopbudget:: Real = LSQR_FLOP_BUDGET,
227- tol:: Union{Real,Nothing} = nothing )
255+ function symcover_min! (:: AbsLog{2} , a:: AbstractVector , A:: AbstractMatrix ; kwargs... )
256+ opts = _auglag_options (; kwargs... )
228257 _prepare_symcover_start! (a, A)
229- anew, nt = _symcover_min_abslog2 (A; start= a, θ, maxouter, maxiter, linsolve, fillbudget, flopbudget, tol )
258+ anew, nt = _symcover_min_abslog2 (A; start= a, opts ... )
230259 a .= anew
231260 return a, AugmentedLagrangianStats (A, nt)
232261end
233262
234- function cover_min! (:: AbsLog{2} , a:: AbstractVector , b:: AbstractVector , A:: AbstractMatrix ; θ:: Real = AL_PENALTY,
235- maxouter:: Int = AL_MAXOUTER, maxiter:: Int = AL_MAXITER, linsolve:: Symbol = :auto ,
236- fillbudget:: Real = LSQR_FILL_BUDGET, flopbudget:: Real = LSQR_FLOP_BUDGET,
237- tol:: Union{Real,Nothing} = nothing )
263+ function cover_min! (:: AbsLog{2} , a:: AbstractVector , b:: AbstractVector , A:: AbstractMatrix ; kwargs... )
264+ opts = _auglag_options (; kwargs... )
238265 _prepare_cover_start! (a, b, A)
239- anew, bnew, nt = _cover_min_abslog2 (A; start= (a, b), θ, maxouter, maxiter, linsolve, fillbudget, flopbudget, tol )
266+ anew, bnew, nt = _cover_min_abslog2 (A; start= (a, b), opts ... )
240267 a .= anew
241268 b .= bnew
242269 return a, b, AugmentedLagrangianStats (A, nt)
@@ -1218,10 +1245,6 @@ function _symmul!(y::AbstractVector{T}, Cu::SparseMatrixCSC{T}, x::AbstractVecto
12181245 return y
12191246end
12201247
1221- # Augmented-Lagrangian defaults.
1222- const AL_PENALTY = 1e2
1223- const AL_MAXOUTER = 32
1224- const AL_MAXITER = 40
12251248# At the penalty cap the multiplier iteration contracts at a fixed rate. When
12261249# the active-set finish follows, two consecutive updates that contract the KKT
12271250# residual by less than this factor end the iteration.
@@ -1291,17 +1314,6 @@ _precond_pattern(::Type{T}, ::Union{Grid,DiffGrid}, v0, N::Int, mult) where {T}
12911314# Scale-relative ridge for a positive-definite preconditioner.
12921315_precond_ridge (dmax:: T ) where {T} = (dmax > 0 ? dmax : oneunit (T)) * sqrt (eps (T))
12931316
1294- # Default storage limit, in bytes, for the LSQR Cholesky preconditioner.
1295- # Tripping this switches to spanning-forest preconditioning, which has no fill
1296- # but needs more iterations for convergence.
1297- const LSQR_FILL_BUDGET = 1 << 30
1298-
1299- # Largest predicted numeric-factorization flop count per stored entry of the
1300- # support for which the LSQR Cholesky preconditioner is used. Above it,
1301- # repeated refactorization of the preconditioner costs more than the extra
1302- # LSQR iterations of spanning-forest preconditioning.
1303- const LSQR_FLOP_BUDGET = 8.0e3
1304-
13051317# The Newton steps of the multiplier iteration need only inexact solves while
13061318# the active pattern is still changing. A stage starts LSQR at tolerance
13071319# `LSQR_FORCING` times the KKT residual of the previous update, capped at
@@ -1425,9 +1437,8 @@ _dense_factor_type(::Type{T}) where {T} = LinearAlgebra.LU{T,Matrix{T},Vector{In
14251437# active-set method of `_polish_cover` or `_polish_symcover`.
14261438function _abslog2_auglag (sys:: SupportSystem{T} , x0;
14271439 θ:: Real , maxouter:: Int , maxiter:: Int , linsolve:: Symbol , boost:: Bool ,
1428- fillbudget:: Real = LSQR_FILL_BUDGET,
1429- flopbudget:: Real = LSQR_FLOP_BUDGET, zeroslack:: Bool = true ,
1430- tol:: Union{Real,Nothing} = nothing ) where {T}
1440+ fillbudget:: Real , flopbudget:: Real , zeroslack:: Bool ,
1441+ tol:: Union{Real,Nothing} ) where {T}
14311442 θ > 1 || throw (ArgumentError (" θ must exceed 1; got $θ " ))
14321443 tol === nothing || (isfinite (tol) && tol >= 0 ) ||
14331444 throw (ArgumentError (" tol must be finite and nonnegative; got $tol " ))
@@ -1840,7 +1851,7 @@ function _abslog2_auglag(sys::SupportSystem{T}, x0;
18401851 x = x0 === nothing ? solve_weighted (zeros (T, N), nothing ) : x0
18411852 # Absolute thresholds preserve covariance under rescaling because the
18421853 # residuals are logarithmic.
1843- vtol = tol === nothing ? 1000 * eps (T) : T (tol)
1854+ vtol = tol === nothing ? AL_TOL_EPS * eps (T) : T (tol)
18441855 # Precision bounds the usable penalty: the cap is `1e6` (LSQR) or `1e8`
18451856 # (exact solves) in `Float64` and grows as `1/sqrt(eps(T))`.
18461857 θcap = T (use_lsqr ? 1e6 : 1e8 ) * sqrt (T (eps (Float64)) / eps (T))
@@ -1980,12 +1991,9 @@ end
19801991# `_polish_difference_qp` (`primal`, `ptol`, `dual`, `dtol`), and
19811992# `stats.converged == stats.polish.certified`. A certified finish replaces the
19821993# multiplier iterate, so `stats.kkt` then describes the iterate, not the result.
1983- function _symcover_min_abslog2 (A:: AbstractMatrix ; θ:: Real = AL_PENALTY, maxouter:: Int = AL_MAXOUTER,
1984- maxiter:: Int = AL_MAXITER, linsolve:: Symbol = :auto , start= nothing ,
1985- boost:: Bool = true , fillbudget:: Real = LSQR_FILL_BUDGET,
1986- flopbudget:: Real = LSQR_FLOP_BUDGET, polish:: Bool = true ,
1987- zeroslack:: Bool = ! polish, tol:: Union{Real,Nothing} = nothing ,
1988- fname= :symcover_min )
1994+ function _symcover_min_abslog2 (A:: AbstractMatrix ; start= nothing , boost:: Bool = true , polish:: Bool = true ,
1995+ zeroslack:: Bool = ! polish, fname= :symcover_min , kwargs... )
1996+ (; θ, maxouter, maxiter, linsolve, fillbudget, flopbudget, tol) = _auglag_options (; kwargs... )
19891997 linsolve in (:auto , :dense , :lsqr , :woodbury ) ||
19901998 throw (ArgumentError (" linsolve must be :auto, :dense, :lsqr, or :woodbury; got :$linsolve " ))
19911999 # Shared symmetry check for native symmetric minimal covers.
@@ -2112,13 +2120,11 @@ end
21122120# stops at its first stall at the penalty cap rather than zeroing slack
21132121# multipliers (`zeroslack`, which defaults to `!polish`). `stats` is as for
21142122# `_symcover_min_abslog2`.
2115- function _cover_min_abslog2 (A:: AbstractMatrix ; θ:: Real = AL_PENALTY, maxouter:: Int = AL_MAXOUTER,
2116- maxiter:: Int = AL_MAXITER, linsolve:: Symbol = :auto , start= nothing ,
2117- boost:: Bool = true , fillbudget:: Real = LSQR_FILL_BUDGET,
2118- flopbudget:: Real = LSQR_FLOP_BUDGET,
2123+ function _cover_min_abslog2 (A:: AbstractMatrix ; start= nothing , boost:: Bool = true ,
21192124 transversal:: Union{Nothing,Bool,AbstractVector{<:Integer}} = nothing ,
2120- polish:: Bool = true , zeroslack:: Bool = ! polish, tol:: Union{Real,Nothing} = nothing ,
2121- fname:: Symbol = :cover_min )
2125+ polish:: Bool = true , zeroslack:: Bool = ! polish, fname:: Symbol = :cover_min ,
2126+ kwargs... )
2127+ (; θ, maxouter, maxiter, linsolve, fillbudget, flopbudget, tol) = _auglag_options (; kwargs... )
21222128 tt = transversal != = nothing && transversal != = false
21232129 if tt
21242130 linsolve in (:auto , :dense , :lsqr , :woodbury ) ||
@@ -2417,10 +2423,11 @@ end
24172423
24182424# Soft `AbsLog{2}` covers are the unweighted initial solve with no feasibility
24192425# shift. Convexity makes the constrained iteration and multistart unnecessary.
2426+ # They accept only the keywords of `_auglag_linsolve_options`.
24202427_soft_symcover_abslog2 (A:: AbstractMatrix ; kwargs... ) =
2421- _symcover_min_abslog2 (A; maxouter= 0 , boost= false , fname= :soft_symcover , kwargs... )
2428+ _symcover_min_abslog2 (A; maxouter= 0 , boost= false , fname= :soft_symcover , _auglag_linsolve_options (; kwargs... ) ... )
24222429_soft_cover_abslog2 (A:: AbstractMatrix ; kwargs... ) =
2423- _cover_min_abslog2 (A; maxouter= 0 , boost= false , kwargs... )
2430+ _cover_min_abslog2 (A; maxouter= 0 , boost= false , _auglag_linsolve_options (; kwargs... ) ... )
24242431
24252432# JuMP reference used to test the native symmetric solver.
24262433function symcover_min_jump end
0 commit comments