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^-1

Here 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.0

An 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.843624111345611

Pass a penalty as the first solver argument to override the default.

Choosing a cover algorithm

FunctionSymmetricConstraintDefault (or alternative) objectiveRequires
symcoveryeshard (r ≤ 1)heuristic
covernohard (r ≤ 1)heuristic
symcover_minyeshard (r ≤ 1)AbsLog{2} (or AbsLog{1}, AbsLinear)native for AbsLog{2}; else JuMP
cover_minnohard (r ≤ 1)AbsLog{2} (or AbsLog{1}, AbsLinear)native for AbsLog{2}; else JuMP
soft_symcoveryessoft (penalized)AbsLinear{2} (or AbsLog, AbsLinear{1})native for AbsLog; else —
soft_covernosoft (penalized)AbsLinear{2} (or AbsLog, AbsLinear{1})native for AbsLog; else —
soft_symcover_minyessoft (penalized)AbsLog{2}, AbsLinearnative for AbsLog{2}; else JuMP
soft_cover_minnosoft (penalized)AbsLog{2}, AbsLinearnative 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_symcover and soft_cover use 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_min and soft_cover_min find a local minimum. AbsLog{2} is native; AbsLinear requires 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.141281

AbsLog{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.5

soft_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:

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.0

For 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
true

Worked 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.5

Here 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.0

The 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.5

For 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)
true

For 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)
true

The condition number of the normalized matrix provides a corresponding bound:

julia> κ = cond(Aill ./ (aill .* aill'));

julia> err <= 1e3 * eps(κ * magill)
true

Index of available tools

Reference documentation

MatrixCovers.AbsLinearType
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.

source
MatrixCovers.AbsLogType
AbsLog{p}

Penalty type for

φ(r) = |log(r)|^p  if r > 0
       0           if r = 0

The r=0 convention keeps zero entries finite. The objective is convex in log space; AbsLog{1} may have multiple minima.

See also: AbsLinear.

source
MatrixCovers.AbstractCoverPenaltyType
AbstractCoverPenalty <: Function

Supertype 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.

source
MatrixCovers.colcomponentMethod
colcomponent(sc::SupportComponents, j) -> Int

Component id of column j, or 0 if that column has empty support. j is the matrix's own column index.

source
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.

source
MatrixCovers.coverMethod
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.31251
source
MatrixCovers.cover_minFunction
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}() and AbsLinear{2}(): require JuMP and Ipopt and return the best local minimum found from strategies.

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!.

source
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!.

source
MatrixCovers.cover_objectiveMethod
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:

source
MatrixCovers.foreach_supportMethod
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.

source
MatrixCovers.foreach_support_symMethod
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.

source
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.

source
MatrixCovers.gramcoverMethod
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 ∈ p

The 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)))
true
source
MatrixCovers.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.

source
MatrixCovers.initialize_coverMethod
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.

source
MatrixCovers.initialize_symcoverMethod
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 of symcover. It forwards maxiter and ignores feasible.

feasible controls whether and how the point is made into a cover:

  • :inflate multiplies every scale by the smallest common factor that covers A.
  • :boost raises scales that touch violated entries.
  • :none returns 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.

source
MatrixCovers.iscoverMethod
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) - atol

Both 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]
false
source
MatrixCovers.ncomponentsMethod
ncomponents(sc::SupportComponents) -> Int

Number of connected components, so component ids run 1:ncomponents(sc).

source
MatrixCovers.rowcomponentMethod
rowcomponent(sc::SupportComponents, i) -> Int

Component id of row i, or 0 if that row has empty support. i is the matrix's own row index.

source
MatrixCovers.scalar_typeMethod
MatrixCovers.scalar_type(T)

Return the unitless floating-point type underlying T. Unit-carrying element types should specialize this method.

source
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!.

source
MatrixCovers.soft_coverMethod
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 the AbsLinear{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.44741
source
MatrixCovers.soft_cover_minFunction
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 in strategies is refined, and the best local minimum is returned.
  • AbsLog{1}(): not implemented.

See also: soft_cover_min!, soft_symcover_min, soft_cover.

source
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!.

source
MatrixCovers.soft_symcoverMethod
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 the AbsLinear{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.0
source
MatrixCovers.soft_symcover_minFunction
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; linsolve has the same meaning as in symcover_min.
  • AbsLinear{1}(), AbsLinear{2}(): require JuMP and Ipopt. Each strategy in strategies is refined, and the best local minimum is returned.
  • AbsLog{1}(): not implemented.

See also: soft_symcover_min!, soft_symcover, symcover_min.

source
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.

source
MatrixCovers.symcoverMethod
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.0
source
MatrixCovers.symcover_minFunction
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}() and AbsLinear{2}(): require JuMP and Ipopt and return the best local minimum found from strategies.

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:

  • :dense factorizes dense normal equations at O(n³) per Newton step.
  • :woodbury handles nearly dense Float64 support as a sparse correction. It requires at most n ÷ 4 missing entries per row and 4n in total.
  • :lsqr is matrix-free with O(nnz) work per iteration and is the sparse-matrix default.
  • :auto chooses :woodbury when supported, :lsqr when the stored support fills at most a quarter of the grid, and :dense otherwise.

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!.

source
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!.

source