MatrixCovers
Given a matrix A, a hard cover is C = a * b', where a and b are nonnegative vectors satisfying
\[C_{ij} \;\geq\; |A_{ij}| \quad \text{for all } i, j.\]
For symmetric A, a single vector suffices (b = a). A minimal cover minimizes a chosen penalty, while a soft cover penalizes violations instead of enforcing every inequality.
Why covers?
Covers provide the "natural scales" of a matrix. If you rescale rows by a positive diagonal factor D_r and columns by D_c, the optimal cover transforms as a → D_r * a, b → D_c * b, so the product a * b' transforms identically to A. Moreover, Ahat = A ./ (a * b') is scale-invariant. Scalar metrics like norm(A) or maximum(abs, A) implicitly encode an arbitrary choice of units, but applying them to Ahat rather than A fixes this deficiency.
While most users will employ matrices that store pure numbers, we'll start with an example of a 3×3 matrix whose rows and columns correspond to physical variables with different units — position in meters, velocity in m/s, force in Newtons. Loading Unitful lets the matrix express those units directly:
julia> using MatrixCovers, Unitful
julia> L, V, F = u"m", u"m/s", u"N"; # length, velocity, force
julia> A = [1e6/L^2 1e3/(L*V) 1.0/(L*F)
1e3/(L*V) 1.0/V^2 1e-3/(V*F)
1.0/(L*F) 1e-3/(V*F) 1e-6/F^2];
julia> a = symcover(A);
julia> round.(typeof.(a), a; digits=6)
3-element Vector{Quantity{Float64}}:
1000.0 m^-1
1.0 s m^-1
0.001 N^-1Here A[i,j] has units 1/(u[i]*u[j]), as in a Hessian. Its cover identifies scales of 1 mm, 1 m/s, and 1 kN. Normalization removes both units and the magnitudes affected by choice of units:
julia> round.(A ./ (a .* a'); digits=6)
3×3 Matrix{Float64}:
1.0 1.0 1.0
1.0 1.0 1.0
1.0 1.0 1.0An entry is 1 only where the cover bound is tight, and this is not guaranteed for all matrices. For example, given diagonal A, the normalized matrix is also diagonal.
A cover exists only when the units of A factor as unit(A[i,j]) == unit(a[i])*unit(b[j]). But this is not an onerous requirement, as it is the same one that lets expressions like A*x be well-defined. If a matrix can be used in matrix-vector multiplication, it has a cover.
Penalty functions
A cover is valid as long as every constraint is satisfied, but tighter covers better capture the scaling of A. Cover quality is measured through the ratios r[i, j] = |A[i,j]| / (a[i] * b[j]): a hard cover has every 0 ≤ r[i, j] ≤ 1, and r[i, j] == 1 means the constraint is exactly tight.
A penalty function ϕ combines those ratios into a scalar objective
\[\sum_{i,j} \phi\!\left(\frac{|A_{ij}|}{a_i\, b_j}\right),\]
Two penalty families are provided:
AbsLog{p}:ϕ(r) = |log r|^p, withϕ(0) = 0. It is convex in log space and is the default for hard covers.AbsLinear{p}:ϕ(r) = |1-r|^p. It is nonconvex, finite at zero, and is the default for soft covers.
cover_objective evaluates either penalty for a given cover:
julia> using MatrixCovers
julia> A = [4.0 2.0; 2.0 16.0];
julia> a = symcover(A)
2-element Vector{Float64}:
2.0
4.0
julia> cover_objective(AbsLog{1}(), a, A) # sum of log-excesses (L1)
2.772588722239781
julia> cover_objective(AbsLog{2}(), a, A) # sum of squared log-excesses (L2)
3.843624111345611Pass a penalty as the first solver argument to override the default.
Choosing a cover algorithm
| Function | Symmetric | Constraint | Default (or alternative) objective | Requires |
|---|---|---|---|---|
symcover | yes | hard (r ≤ 1) | heuristic | — |
cover | no | hard (r ≤ 1) | heuristic | — |
symcover_min | yes | hard (r ≤ 1) | AbsLog{2} (or AbsLog{1}, AbsLinear) | native for AbsLog{2}; else JuMP |
cover_min | no | hard (r ≤ 1) | AbsLog{2} (or AbsLog{1}, AbsLinear) | native for AbsLog{2}; else JuMP |
soft_symcover | yes | soft (penalized) | AbsLinear{2} (or AbsLog, AbsLinear{1}) | native for AbsLog; else — |
soft_cover | no | soft (penalized) | AbsLinear{2} (or AbsLog, AbsLinear{1}) | native for AbsLog; else — |
soft_symcover_min | yes | soft (penalized) | AbsLog{2}, AbsLinear | native for AbsLog{2}; else JuMP |
soft_cover_min | no | soft (penalized) | AbsLog{2}, AbsLinear | native for AbsLog{2}; else JuMP |
For hard covers, symcover and cover are fast heuristics; their _min counterparts minimize the selected objective. For soft covers:
soft_symcoverandsoft_coveruse native coordinate descent and multistart. For nonconvex or nonsmooth penalties they may stop at a fixed point that is not a local minimum.soft_symcover_minandsoft_cover_minfind a local minimum.AbsLog{2}is native;AbsLinearrequires JuMP and Ipopt;AbsLog{1}is not implemented.
Under AbsLog{2}, the objective is convex, so both soft tiers reach the same minimum. The heuristics cost $O(mn)$; native iterative solvers cost roughly $O(mn)$ per iteration.
Covariance of the heuristics
The heuristic solvers are exactly covariant when every row and column has the same nonzero pattern, including dense matrices without zeros. On irregular sparse support they may be only approximately covariant:
julia> using MatrixCovers, LinearAlgebra
julia> A = [1.0 1 0; 1 1 1; 0 1 1];
julia> d = [1.0, 6.0, 0.5]; D = Diagonal(d);
julia> a1 = symcover(A); a2 = symcover(D * A * D);
julia> P1 = (d .* a1) * (d .* a1)'; P2 = a2 * a2';
julia> round.(extrema(P2 ./ P1); digits=3)
(1.0, 1.077)Use symcover_min or cover_min when exact covariance is required.
Objective-minimal covers
symcover_min and cover_min minimize the chosen penalty subject to the hard constraint. The built-in AbsLog{2} solver uses penalty continuation with a damped semismooth Newton iteration:
julia> using MatrixCovers
julia> A = [1 2 3; 6 5 4];
julia> a, b = cover(A);
julia> aq, bq = cover_min(AbsLog{2}(), A);
julia> a * b'
2×3 Matrix{Float64}:
2.16541 2.03444 3.0
6.0 5.63709 8.31251
julia> aq * bq'
2×3 Matrix{Float64}:
2.21042 2.0 3.0
6.0 5.42884 8.14325
julia> round(cover_objective(AbsLog{2}(), a, b, A); digits=6)
1.146646
julia> round(cover_objective(AbsLog{2}(), aq, bq, A); digits=6)
1.141281AbsLog{1} and AbsLinear use JuMP with HiGHS and Ipopt, respectively:
julia> using MatrixCovers, JuMP, HiGHS
julia> S = [4 1 0; 1 1 5; 0 5 2];
julia> round.(symcover_min(AbsLog{1}(), S); digits=6)
3-element Vector{Float64}:
2.0
1.0
5.0
julia> A = [1 2 3; 6 5 4];
julia> a, b = cover_min(AbsLog{1}(), A);
julia> round.(a * b'; digits=6)
2×3 Matrix{Float64}:
2.4 2.0 3.0
6.0 5.0 7.5soft_symcover_min and soft_cover_min solve AbsLog{2} natively and use JuMP with Ipopt for AbsLinear. They do not accept AbsLog{1}; use soft_symcover or soft_cover instead.
Uniqueness
For asymmetric covers, a → γ*a, b → b/γ leaves a*b' unchanged. The package chooses a unique representative using ∑ n_i log a[i] = ∑ m_j log b[j], where n_i, m_j are the nonzero counts of row i and column j. The convention is applied separately to each connected component of the bipartite support graph. It affects the factors but not their products.
AbsLog{2} has a unique minimizer except when the support pattern leaves a scaling freedom, as in [0 1; 1 0], where every a with a[1]*a[2] = 1 is optimal. If AbsLog{1}() has multiple minima, the implementation chooses the one with the smallest AbsLog{2} objective. AbsLinear may have several local minima.
Starting points: initialize and refine
For objectives with multiple minima, the result can depend on its starting point. The interface separates initialization, refinement, and multistart selection:
- Initializers
initialize_symcoverandinitialize_coverbuild a namedstrategy. Theirfeasiblekeyword selects uniform inflation, selective boosting, or no feasibility step. - Refiners are the
!-suffixed forms of the solvers:symcover_min!,cover_min!,soft_symcover!,soft_cover!,soft_symcover_min!, andsoft_cover_min!optimize a supplied point. Hard refiners require a cover; soft refiners do not. - Solvers
symcover_min,cover_min,soft_symcover,soft_cover,soft_symcover_min, andsoft_cover_minrefine several starts and return the best objective.
Plain forms choose their starts; ! forms refine the supplied start, except symcover! and cover!, which are in-place heuristics.
julia> using MatrixCovers, JuMP, Ipopt
julia> S = [4 1 0; 1 1 5; 0 5 2];
julia> round.(symcover_min(AbsLinear{2}(), S); digits=6)
3-element Vector{Float64}:
2.0
1.0
5.0
julia> round.(symcover_min(AbsLinear{2}(), S; strategies=(:geomean,)); digits=6)
3-element Vector{Float64}:
2.0
1.0
5.0
julia> a0 = initialize_symcover(S; strategy=:geomean);
julia> symcover_min!(AbsLinear{2}(), a0, S);
julia> round.(a0; digits=6)
3-element Vector{Float64}:
2.0
1.0
5.0For convex AbsLog penalties, the start does not change the result.
Consuming one factor alone: gauges and Gram covers
The balanced factors of an asymmetric cover are deterministic but not individually covariant under one-sided scaling. This matters when consuming one factor, for example when covering J'*J from a cover of J.
gramcover(a, b, J[, W]) constructs a symmetric cover of J'*W*J that covaries with right-scaling of J.
julia> using MatrixCovers, LinearAlgebra
julia> J = [1.0 2; 3 4; 5 6];
julia> D = Diagonal([100.0, 1.0]);
julia> a1, b1 = cover(J); a2, b2 = cover(J * D);
julia> r = b2 ./ (D.diag .* b1); all(x -> x ≈ first(r), r)
true
julia> first(r) ≈ 1
false
julia> s1 = gramcover(a1, b1, J); s2 = gramcover(a2, b2, J * D);
julia> s2 ≈ D.diag .* s1
trueWorked example: roundoff in A \ b
For x = A \ b, diagonal rescaling sends x → x ./ d and a symmetric cover a → d .* a. Thus sum(abs.(x .* a)) is invariant. The quantity can be estimated without solving for x:
julia> using MatrixCovers, LinearAlgebra
julia> A = [1e6 1e3; 1e3 4.0];
julia> b = [1.5e3, 6.0];
julia> a = symcover(A);
julia> round.(a; digits=6)
2-element Vector{Float64}:
1000.0
2.0
julia> mag = sum(abs(bi / ai) for (bi, ai) in zip(b, a));
julia> round(mag; digits=6)
4.5Here mag is within a factor of 1.5 of the scaled solution norm:
julia> x = A \ b;
julia> round(sum(abs.(x .* a)); digits=6)
3.0The estimate is unchanged by diagonal rescaling:
julia> d = [0.05, 3.0];
julia> Ad, bd = d .* A .* d', d .* b;
julia> ad = symcover(Ad);
julia> round(sum(abs(bi / ai) for (bi, ai) in zip(bd, ad)); digits=6)
4.5For well-conditioned A, eps(mag) estimates the roundoff floor:
julia> xbig = big.(A) \ big.(b);
julia> abs(sum(abs.(x .* a)) - sum(abs.(Float64.(xbig) .* a))) <= 2 * eps(mag)
trueFor ill-conditioned A, the error can be much larger:
julia> Aill = [1.0 -0.9999; -0.9999 1.0];
julia> bill = [0.75, 7.0];
julia> aill = symcover(Aill);
julia> magill = sum(abs(bi / ai) for (bi, ai) in zip(bill, aill));
julia> xill = Aill \ bill;
julia> xbigill = big.(Aill) \ big.(bill);
julia> err = abs(sum(abs.(xill .* aill)) - sum(abs.(Float64.(xbigill) .* aill)));
julia> err > 1e6 * eps(magill)
trueThe condition number of the normalized matrix provides a corresponding bound:
julia> κ = cond(Aill ./ (aill .* aill'));
julia> err <= 1e3 * eps(κ * magill)
trueIndex of available tools
MatrixCovers.AbsLinearMatrixCovers.AbsLogMatrixCovers.AbstractCoverPenaltyMatrixCovers.SupportComponentsMatrixCovers.colcomponentMatrixCovers.coverMatrixCovers.cover!MatrixCovers.cover_minMatrixCovers.cover_min!MatrixCovers.cover_objectiveMatrixCovers.foreach_supportMatrixCovers.foreach_support_symMatrixCovers.gramcoverMatrixCovers.gramcover!MatrixCovers.initialize_coverMatrixCovers.initialize_cover!MatrixCovers.initialize_symcoverMatrixCovers.initialize_symcover!MatrixCovers.iscoverMatrixCovers.ncomponentsMatrixCovers.rowcomponentMatrixCovers.scalar_typeMatrixCovers.soft_coverMatrixCovers.soft_cover!MatrixCovers.soft_cover_minMatrixCovers.soft_cover_min!MatrixCovers.soft_symcoverMatrixCovers.soft_symcover!MatrixCovers.soft_symcover_minMatrixCovers.soft_symcover_min!MatrixCovers.support_componentsMatrixCovers.symcoverMatrixCovers.symcover!MatrixCovers.symcover_minMatrixCovers.symcover_min!
Reference documentation
MatrixCovers.AbsLinear — Type
AbsLinear{p}Penalty type for φ(r) = |1 - r|^p. Unlike AbsLog, this penalty is continuous at r = 0 (φ(0) = 1), so zero entries in A naturally contribute a constant penalty.
The resulting optimization problems are non-convex and may have multiple local minima.
MatrixCovers.AbsLog — Type
AbsLog{p}Penalty type for
φ(r) = |log(r)|^p if r > 0
0 if r = 0The r=0 convention keeps zero entries finite. The objective is convex in log space; AbsLog{1} may have multiple minima.
See also: AbsLinear.
MatrixCovers.AbstractCoverPenalty — Type
AbstractCoverPenalty <: FunctionSupertype of cover penalties. Built-in subtypes are AbsLog and AbsLinear.
cover_objective applies the penalty to r = |A[i,j]|/(a[i]*b[j]) and sums over A.
Extending
A subtype must be callable on a nonnegative real:
(::MyPenalty)(r::Real)The method must accept r = 0 and r = typemax(...). Penalties are usually singleton structs.
cover_objective works for any subtype, but solvers support only specific built-in penalties: AbsLog{2} natively and AbsLinear through JuMP. Passing a custom subtype to a solver raises a MethodError.
MatrixCovers.SupportComponents — Type
SupportComponentsConnected components of a matrix's bipartite support graph, as returned by support_components: one vertex per row and one per column, one edge per stored nonzero.
Component ids run 1:ncomponents(sc); unsupported rows and columns report 0. Use rowcomponent and colcomponent with the matrix's own indices. Pass this object to gramcover to reuse the traversal.
MatrixCovers.colcomponent — Method
colcomponent(sc::SupportComponents, j) -> IntComponent id of column j, or 0 if that column has empty support. j is the matrix's own column index.
MatrixCovers.cover! — Method
a, b = cover!(ϕ, a, b, A; maxiter=3)
a, b = cover!(a, b, A; maxiter=3)Mutating counterpart of cover: writes the hard cover into a and b and returns them, rather than allocating new vectors. eachindex(a) must match axes(A, 1) and eachindex(b) must match axes(A, 2). ϕ has the same meaning as in cover, and is likewise ignored by the current heuristic covers.
See also: cover.
MatrixCovers.cover — Method
a, b = cover(ϕ, A; maxiter=3)
a, b = cover(A; maxiter=3)Given a matrix A, return vectors a and b such that a[i] * b[j] >= abs(A[i, j]) for all i, j. The method initializes from row and column geometric means, covers the most-violated entries first, then applies maxiter tightening iterations.
The factors use the per-component balance convention described by cover_min.
ϕ is accepted for API compatibility but is currently ignored. For a cover that provably minimizes a given ϕ, use cover_min.
See also: cover!, cover_min, symcover.
Examples
julia> A = [1 2 3; 6 5 4];
julia> a, b = cover(A)
([1.2544610775677627, 3.475905976749231], [1.7261686708831454, 1.621762761307448, 2.3914651906272066])
julia> a * b'
2×3 Matrix{Float64}:
2.16541 2.03444 3.0
6.0 5.63709 8.31251MatrixCovers.cover_min — Function
a, b = cover_min(ϕ, A)
a, b = cover_min(A)Return the ϕ-minimal asymmetric hard cover of A: vectors a, b minimizing ∑ ϕ(|A[i,j]|/(a[i]*b[j])) subject to a[i]*b[j] >= |A[i,j]|. The default penalty is AbsLog{2}(). Within each support component, the factors use the balance convention ∑ nzaᵢ log a[i] = ∑ nzbⱼ log b[j] (nzaᵢ, nzbⱼ = nonzero counts of row i, column j).
Supported ϕ values:
AbsLog{2}(): native.AbsLog{1}(): requires JuMP and HiGHS.AbsLinear{1}()andAbsLinear{2}(): require JuMP and Ipopt and return the best local minimum found fromstrategies.
AbsLog is convex in the log scales. If AbsLog{1} has multiple minima, the method selects the one with the smallest AbsLog{2} objective.
Extended help
The native solver accepts the same κ, maxouter, maxiter, fillbudget, and linsolve keywords as symcover_min. For :woodbury, an m × n matrix may omit at most min(m,n) ÷ 4 entries per row or column and 4 * max(m,n) entries in total. :dense costs O((m+n)³) per Newton step; sparse matrices default to :lsqr.
See also: symcover_min, cover, cover_min!.
MatrixCovers.cover_min! — Function
a, b = cover_min!(ϕ, a, b, A; kwargs...)
a, b = cover_min!(a, b, A; kwargs...)Refine the asymmetric hard cover (a, b) in place. The start must cover A and be positive on supported rows and columns; unsupported scales are zeroed. The no-ϕ form uses AbsLog{2}(); supported penalties and keywords match cover_min.
Equivalent starts (c*a, b/c) give the same balanced result.
See also: initialize_cover, cover_min, symcover_min!.
MatrixCovers.cover_objective — Method
cover_objective(ϕ, a, b, A)
cover_objective(ϕ, a, A)Compute ∑ ϕ(|A[i,j]|/(a[i]*b[j])). The shorter form uses the symmetric cover a*a'.
Both forms use full-grid weighting: symmetric off-diagonal pairs contribute twice and diagonal entries once.
Zero entries of A are handled according to ϕ:
AbsLog{p}: zero entries contribute 0 (φ(0) = 0 by convention).AbsLinear{p}: zero entries contribute 1 (φ(0) = |1-0|^p = 1).
eachindex(a) must match axes(A, 1) and eachindex(b) must match axes(A, 2).
A is read through foreach_support.
See also:
- Penalty types (options for
ϕ):AbsLog,AbsLinear. - Solvers:
symcover,cover,soft_symcover,soft_cover.
MatrixCovers.foreach_support — Method
foreach_support(f, A)Call f(i, j, abs(A[i,j])) once per nonzero entry and return nothing. Traversal order is unspecified; indices follow axes(A).
Specialize this hook to support custom sparse storage in O(nnz) time.
Extending
To support a new matrix type, define
MatrixCovers.foreach_support(f, A::MyMatrix)It must emit each nonzero entry exactly once, skip stored zeros, ignore callback return values, and return nothing.
See also: foreach_support_sym.
MatrixCovers.foreach_support_sym — Method
foreach_support_sym(f, A)Symmetric counterpart of foreach_support: call f(i, j, v) once per nonzero unordered pair in canonical order i <= j, including the diagonal.
A must be square and abs.(A) symmetric. Public symmetric solvers check this precondition before calling the traversal.
Objective weighting
For full-grid objective weighting, use multiplicity 1 on the diagonal and 2 off-diagonal. Constraints need no multiplicity.
Extending
To support a new matrix type, define
MatrixCovers.foreach_support_sym(f, A::MyMatrix)It must emit each nonzero pair once in canonical order, skip zero pairs, ignore callback return values, and return nothing. Triangular storage must map lower entries back to (j, i).
See also: foreach_support.
MatrixCovers.gramcover! — Method
s = gramcover!(s, a, b, A)
s = gramcover!(s, a, b, A, w::AbstractVector)
s = gramcover!(s, a, b, A, W::AbstractMatrix; degenerate=:error)
s = gramcover!(s, a, b, sc::SupportComponents)
s = gramcover!(s, a, b, sc::SupportComponents, w::AbstractVector)
s = gramcover!(s, a, b, sc::SupportComponents, W::AbstractMatrix; degenerate=:error)Mutating counterpart of gramcover: writes the symmetric cover of the (weighted) Gram matrix into s and returns it, rather than allocating a new vector. eachindex(s) must match axes(A, 2) — sc.colax for the SupportComponents forms. The degenerate keyword is shared with gramcover.
See also: gramcover.
MatrixCovers.gramcover — Method
s = gramcover(a, b, A)
s = gramcover(a, b, A, w::AbstractVector)
s = gramcover(a, b, A, W::AbstractMatrix; degenerate=:error)
s = gramcover(a, b, sc::SupportComponents)
s = gramcover(a, b, sc::SupportComponents, w::AbstractVector)
s = gramcover(a, b, sc::SupportComponents, W::AbstractMatrix; degenerate=:error)Given a cover (a, b) of A, return a symmetric cover of A'*A, A'*Diagonal(w)*A, or A'*W*A without forming the product. Only abs.(W) enters the bound. Passing Diagonal(w) is equivalent to passing w.
(a, b) must cover A; use iscover(a, b, A) to check it.
Pass SupportComponents to reuse a previous support_components(A) computation.
The result is invariant under componentwise rescaling of (a, b). If W makes this impossible, the matrix form throws an ArgumentError; use degenerate=:uniform to allow a gauge-dependent result.
Extended help
For diagonal W, each support component has the scale
s[j] = sqrt(Σ_{i ∈ rows(comp(j))} abs(w[i])*a[i]^2) * b[j]The unweighted form uses w[i] = 1. Off-diagonal entries of W may join components. For each joined group, define M[p,q] = Σ_{i ∈ rows(p), i' ∈ rows(q)} a[i]*abs(W[i,i'])*a[i'] for the block sum, and symmetrize it as Ms[p,q] = max(M[p,q], M[q,p]). A cover σ of Ms yields
s[j] = σ[p]*b[j], j ∈ pThe implementation computes σ with symcover_min(AbsLog{2}(), Ms). Unsupported columns receive zero.
Under componentwise rescaling, Ms[p,q] and σ[p] transform so that σ[p]*b[j] remains unchanged.
An invariant cover does not exist when a nontrivial connected component of Ms is loopless and bipartite. With degenerate=:uniform, the fallback is σ[p] = sqrt(Σ_{p,q} Ms[p,q]), which depends on the gauge of (a, b). A loop or odd cycle removes this degeneracy.
See also: gramcover!, symcover, cover, iscover.
Examples
julia> J = [4 1; 1 3];
julia> a, b = cover(J);
julia> s = gramcover(a, b, J);
julia> all(s * s' .>= abs.(J' * J))
true
julia> w = [1.0, -2.0];
julia> sw = gramcover(a, b, J, w); # covers J'*Diagonal(w)*J
julia> all(sw * sw' .>= abs.(J' * (w .* J)))
trueMatrixCovers.initialize_cover! — Method
a, b = initialize_cover!(a, b, A; strategy=:hardcover, feasible=:inflate, kwargs...)Mutating counterpart of initialize_cover: writes the starting cover into a and b and returns them, rather than allocating new vectors. eachindex(a) must match axes(A, 1) and eachindex(b) must match axes(A, 2).
See also: initialize_cover.
MatrixCovers.initialize_cover — Method
a, b = initialize_cover(A; strategy=:hardcover, feasible=:inflate, kwargs...)Build an asymmetric starting point for cover_min or soft_cover. The feasible keyword matches initialize_symcover.
Supported strategies are :hardcover (the result of cover, forwarding maxiter) and :geomean (the unconstrained AbsLog{2} minimum).
Supported rows and columns receive positive scales; unsupported ones receive zero. The factors use the balance convention of cover_min.
See also: initialize_cover!, initialize_symcover, cover, cover_min.
MatrixCovers.initialize_symcover! — Method
a = initialize_symcover!(a, A; strategy=:hardcover, feasible=:inflate, kwargs...)Mutating counterpart of initialize_symcover: writes the starting cover into a and returns it, rather than allocating a new vector. eachindex(a) must match axes(A, 1) (and A must be square).
See also: initialize_symcover.
MatrixCovers.initialize_symcover — Method
a = initialize_symcover(A; strategy=:hardcover, feasible=:inflate, kwargs...)Build a symmetric starting point for symcover_min or soft_symcover. Strategies depend only on A:
strategy names the point:
:geomean— geometric means of the nonzero entries in each row.:leaveout— the geometric mean recomputed with the most-underweighted support entry omitted. It fails if removing that entry empties a row.:diagfeasible— a cover grown from the diagonal by nearest-neighbor propagation.:hardcover— the result ofsymcover. It forwardsmaxiterand ignoresfeasible.
feasible controls whether and how the point is made into a cover:
:inflatemultiplies every scale by the smallest common factor that coversA.:boostraises scales that touch violated entries.:nonereturns the strategy's point without a coverage guarantee.
Supported rows receive positive scales; unsupported rows receive zero.
See also: initialize_symcover!, initialize_cover, symcover, symcover_min.
MatrixCovers.iscover — Method
iscover(a, b, A; rtol=0, atol=0)
iscover(a, A; rtol=0, atol=0)Test whether a and b cover A, that is, whether a[i]*b[j] >= abs(A[i,j]) for every entry. The two-argument form tests the symmetric cover a*a', and requires A to be square.
rtol and atol allow for small violations, testing
a[i]*b[j] >= abs(A[i,j])*(1 - rtol) - atolBoth tolerances default to zero. A nonzero atol breaks scale invariance.
cover, symcover, and native AbsLog{2} minimal covers certify their results at zero tolerance. Initializers and extension solvers may require a nonzero rtol.
a and b must be nonnegative; a negative scale raises an ArgumentError. Zero is allowed for unsupported rows and columns.
eachindex(a) must match axes(A, 1) and eachindex(b) must match axes(A, 2).
See also: cover_objective, cover, symcover.
Examples
julia> A = [1.0 2.0; 3.0 4.0];
julia> a, b = cover(A);
julia> iscover(a, b, A; rtol=8eps())
true
julia> iscover([1.0, 1.0], [1.0, 1.0], A) # a*b' = ones, which does not reach A[2,2]
falseMatrixCovers.ncomponents — Method
ncomponents(sc::SupportComponents) -> IntNumber of connected components, so component ids run 1:ncomponents(sc).
MatrixCovers.rowcomponent — Method
rowcomponent(sc::SupportComponents, i) -> IntComponent id of row i, or 0 if that row has empty support. i is the matrix's own row index.
MatrixCovers.scalar_type — Method
MatrixCovers.scalar_type(T)Return the unitless floating-point type underlying T. Unit-carrying element types should specialize this method.
MatrixCovers.soft_cover! — Function
a, b = soft_cover!(ϕ, a, b, A; maxiter=...)
a, b = soft_cover!(a, b, A; maxiter=...)Refine the starting point (a, b) into a soft cover of A in place. Scales must be finite and positive on supported rows and columns; unsupported scales are zeroed. The start need not cover A. Build one with initialize_cover and feasible=:none. The no-ϕ form uses AbsLinear{2}().
The result uses the balance convention of cover_min, so equivalent rescalings (c*a, b/c) give the same result.
See also: soft_cover, soft_cover_min!, initialize_cover, soft_symcover!.
MatrixCovers.soft_cover — Method
a, b = soft_cover(ϕ, A; maxiter=200, starts=4, σ=2.0, rng=MersenneTwister(0))
a, b = soft_cover(A; maxiter=200, starts=4, σ=2.0, rng=MersenneTwister(0))Approximately minimize ∑ ϕ(|A[i,j]|/(a[i]*b[j])) without a hard coverage constraint. This is the asymmetric form of soft_symcover.
Supported penalties are:
AbsLog{2}(): the convex minimum, computed by one linear solve.AbsLog{1}(): alternating weighted-median updates to a fixed point, which need not be a local minimum.AbsLinear{2}()(default): alternating least squares.AbsLinear{1}(): alternating weighted-median updates initialized from theAbsLinear{2}result.
Unsupported rows and columns receive zero scale. The factors use the balance convention of cover_min.
For AbsLinear, starts controls the number of starting points and σ the spread of perturbations. Pass rng for reproducibility. sigma is an alias for σ.
See also: cover, soft_symcover, cover_objective.
Examples
julia> A = [1 2 3; 6 5 4];
julia> a, b = soft_cover(A);
julia> a * b'
2×3 Matrix{Float64}:
1.93288 1.97239 2.50673
4.97144 5.07307 6.44741MatrixCovers.soft_cover_min — Function
a, b = soft_cover_min(ϕ, A)
a, b = soft_cover_min(A)Return a local minimum of ∑ ϕ(|A[i,j]|/(a[i]*b[j])) without coverage constraints. The no-ϕ form uses AbsLinear{2}(). Factors use the balance convention of cover_min.
Supported ϕ values and required extensions:
AbsLog{2}(): solved natively.AbsLinear{1}(),AbsLinear{2}(): require JuMP and Ipopt. Each strategy instrategiesis refined, and the best local minimum is returned.AbsLog{1}(): not implemented.
See also: soft_cover_min!, soft_symcover_min, soft_cover.
MatrixCovers.soft_cover_min! — Function
a, b = soft_cover_min!(ϕ, a, b, A)
a, b = soft_cover_min!(a, b, A)Refine (a, b) into a local minimum of the asymmetric soft-cover objective, in place. The no-ϕ form uses AbsLinear{2}().
a and b must be positive on supported rows and columns; unsupported scales are zeroed. The start need not cover A. Build one with feasible=:none.
The result uses the balance convention of soft_cover_min, so equivalent rescalings (c*a, b/c) give the same result.
See also: initialize_cover, soft_cover_min, soft_symcover_min!.
MatrixCovers.soft_symcover! — Function
a = soft_symcover!(ϕ, a, A; maxiter=...)
a = soft_symcover!(a, A; maxiter=...)Refine one symmetric soft-cover start in place. The no-ϕ form uses AbsLinear{2}(). Build a start with initialize_symcover and feasible=:none.
a must be finite and positive on supported rows. It need not cover A, and unsupported scales are set to zero.
maxiter bounds the descent sweeps.
See also: soft_symcover, soft_symcover_min!, initialize_symcover, soft_cover!.
MatrixCovers.soft_symcover — Method
a = soft_symcover(ϕ, A; maxiter=32, starts=5, σ=2.0, rng=MersenneTwister(0))
a = soft_symcover(A; maxiter=32, starts=5, σ=2.0, rng=MersenneTwister(0))Approximately minimize ∑ ϕ(|A[i,j]|/(a[i]*a[j])) for symmetric A, without a hard coverage constraint.
Supported penalties are:
AbsLog{2}(): the convex minimum, computed by one linear solve.AbsLog{1}(): weighted-median coordinate descent to a fixed point, which need not be a local minimum.AbsLinear{2}()(default): multistart coordinate descent.AbsLinear{1}(): weighted-median descent initialized from theAbsLinear{2}result.
For AbsLinear, starts controls the number of starting points and σ the spread of log-normal perturbations. Pass rng for reproducibility. sigma is an alias for σ.
See also: symcover, cover_objective, soft_symcover_min.
Examples
Round the multistart result when comparing it with exact values.
julia> A = [4 -1; -1 0];
julia> round.(soft_symcover(A); digits=4)
2-element Vector{Float64}:
2.0
0.5
julia> round.(soft_symcover([0 1; 1 0]); digits=4)
2-element Vector{Float64}:
1.0
1.0MatrixCovers.soft_symcover_min — Function
a = soft_symcover_min(ϕ, A)
a = soft_symcover_min(A)Return a local minimum of ∑ ϕ(|A[i,j]|/(a[i]*a[j])) without coverage constraints. The no-ϕ form uses AbsLinear{2}().
Supported ϕ values and required extensions:
AbsLog{2}(): solved natively as linear least squares;linsolvehas the same meaning as insymcover_min.AbsLinear{1}(),AbsLinear{2}(): require JuMP and Ipopt. Each strategy instrategiesis refined, and the best local minimum is returned.AbsLog{1}(): not implemented.
See also: soft_symcover_min!, soft_symcover, symcover_min.
MatrixCovers.soft_symcover_min! — Function
a = soft_symcover_min!(ϕ, a, A)
a = soft_symcover_min!(a, A)Refine a into a local minimum of the symmetric soft-cover objective, in place. The no-ϕ form uses AbsLinear{2}().
a must be positive on supported rows; unsupported scales are zeroed. It need not cover A. Use feasible=:none with initialize_symcover.
For AbsLinear, the result can depend on the start.
See also: initialize_symcover, soft_symcover_min, symcover_min!.
MatrixCovers.support_components — Method
support_components(A) -> sc::SupportComponentsReturn the connected components of A's bipartite support graph. The matrix is read through foreach_support.
See also: SupportComponents.
MatrixCovers.symcover! — Method
a = symcover!(ϕ, a, A; maxiter=3)
a = symcover!(a, A; maxiter=3)Mutating counterpart of symcover: writes the symmetric hard cover into a and returns it, rather than allocating a new vector. eachindex(a) must match axes(A, 1) (and A must be square). ϕ has the same meaning as in symcover, and is likewise ignored by the current heuristic covers.
See also: symcover.
MatrixCovers.symcover — Method
a = symcover(ϕ, A; maxiter=3)
a = symcover(A; maxiter=3)Given a square matrix A assumed to be symmetric, return a vector a representing a symmetric hard cover of A: a[i] * a[j] >= abs(A[i, j]) for all i, j.
The method initializes from per-row geometric means, covers the most-violated entries first, then applies maxiter tightening iterations.
ϕ is accepted for API compatibility but is currently ignored. For a cover that provably minimizes a given ϕ, use symcover_min.
See also: symcover!, symcover_min, soft_symcover, cover.
Examples
julia> A = [4 1; 1 4];
julia> a = symcover(A)
2-element Vector{Float64}:
2.0
2.0
julia> a * a' # covers |A|: a[i]*a[j] >= abs(A[i, j])
2×2 Matrix{Float64}:
4.0 4.0
4.0 4.0MatrixCovers.symcover_min — Function
a = symcover_min(ϕ, A; kwargs...)
a = symcover_min(A; kwargs...)Return the ϕ-minimal symmetric hard cover of A: the vector a minimizing ∑ ϕ(|A[i,j]|/(a[i]*a[j])) subject to a[i]*a[j] >= |A[i,j]|. The default penalty is AbsLog{2}().
Supported ϕ values:
AbsLog{2}(): native.AbsLog{1}(): requires JuMP and HiGHS.AbsLinear{1}()andAbsLinear{2}(): require JuMP and Ipopt and return the best local minimum found fromstrategies.
AbsLog is convex in the log scales. If AbsLog{1} has multiple minima, the method selects the one with the smallest AbsLog{2} objective.
Extended help
The native solver accepts κ (initial augmented-Lagrangian penalty, default 1e2), maxouter (multiplier updates, default 32; 0 returns the unconstrained fit), maxiter (Newton steps per update), fillbudget (see below), and linsolve:
:densefactorizes dense normal equations at O(n³) per Newton step.:woodburyhandles nearly denseFloat64support as a sparse correction. It requires at mostn ÷ 4missing entries per row and4nin total.:lsqris matrix-free with O(nnz) work per iteration and is the sparse-matrix default.:autochooses:woodburywhen supported,:lsqrwhen the stored support fills at most a quarter of the grid, and:denseotherwise.
The solver increases κ when the KKT residual contracts slowly. Statistics include the penalty weights (κs) and KKT residuals (kkt).
For Float64, :lsqr uses a Cholesky preconditioner when its predicted storage does not exceed fillbudget bytes (default 2^30). Otherwise it uses a diagonal preconditioner. The returned statistics identify the choice as precond.
If the solver warns that the result may not minimize the objective, increase maxouter or, rarely, κ.
The native solver computes in Float64 for narrower input types, then converts the result to the required element type.
See also: cover_min, symcover, symcover_min!.
MatrixCovers.symcover_min! — Function
a = symcover_min!(ϕ, a, A; kwargs...)
a = symcover_min!(a, A; kwargs...)Refine the symmetric hard cover a in place. The no-ϕ form uses AbsLog{2}(); supported penalties and keywords match symcover_min.
a must cover A and be positive on supported rows. Unsupported scales are ignored on input and set to zero. Use initialize_symcover or symcover to construct a start.
The AbsLog result is independent of the start. For AbsLog{1}, ties are broken by the AbsLog{2} objective. Local minima under AbsLinear can depend on the start.
See also: initialize_symcover, symcover_min, cover_min!.