MatrixCovers
This package computes covers of matrices. Given a matrix A, a cover (more specifically, a hard cover) is a matrix C that can be defined as C = a * b', where a and b are non-negative vectors. C must satisfy
\[C_{ij} \;\geq\; |A_{ij}| \quad \text{for all } i, j.\]
For a symmetric matrix the cover is symmetric (b = a), so a single vector suffices: a[i] * a[j] >= abs(A[i, j]).
An optimal cover is one for which C is "as tight as possible" in bounding A (equivalently, no larger than strictly necessary), by criteria that will be described below.
This package also supports soft covers: these satisfy C[i, j] ⪆ abs(A[i, j]), meaning that C matches or exceeds A at most indexes but not necessarily all; this intuitive notion will be made concrete below.
Why covers?
Covers provide a natural scale-covariant "summary" 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, exactly mirroring how the matrix entries change. Scalar summaries like norm(A) or maximum(abs, A) do not have this property and therefore implicitly encode an arbitrary choice of units.
Most users will employ matrices that store pure numbers, and this package works well with such matrices. But to emphasize the scale-covariance, 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 carry those units itself:
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^-1A[i,j] has units 1/(u[i]*u[j]) (modeling a Hessian matrix for functions of parameter vectors with units u[i]), so a[i] comes back with gradient-like units of 1/u[i]: the cover names each variable's natural scale outright, here 1 mm, 1 m/s, and 1 kN. Had we expressed the original matrix in those units directly, we would have gotten the equivalent cover stated in those units.
Normalizing by the cover cancels the units along with the magnitudes, leaving a matrix that is all-ones, dimensionless, and scale-invariant:
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.0It is worth noting that this yields 1 only for entries where the cover bound is tight; had A been, say, diagonal, then A ./ (a .* a') would also be diagonal.
A cover exists only when the units of A factor as unit(A[i,j]) == unit(a[i])*unit(b[j]), and a matrix that fails this is rejected with a DimensionMismatch naming the entries that conflict. The requirement is not one this package adds: without it the terms in a row of A*x carry different units, so A*x is undefined for every x. 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_{ij} = |A[i,j]| / (a[i] * b[j]): a hard cover has every r ≤ 1, and r = 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),\]
which the solvers minimize. Two penalty families are provided:
AbsLog{p}—ϕ(r) = |log r|^p(andϕ(0) = 0). Convex in log space, which makes them a favorable (and therefore default) penalty for hard covers, wherer ≤ 1and|log r|is the log-excess of a constraint.AbsLog{1}sums the log-excesses (L1),AbsLog{2}sums their squares (L2). Their principal disadvantage is the discontinuity atr = 0.AbsLinear{p}—ϕ(r) = |1 - r|^p. Non-convex, but unlikeAbsLogthese are continuous atr = 0(ϕ(0) = 1), so zero entries ofAcontribute a bounded penalty. This is the penalty used by default for the soft covers, wherer > 1(an uncovered entry) is allowed but penalized.
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.843624111345611Both objectives are zero if and only if every constraint is exactly tight.
You can override the default penalty by supplying it as an argument to the solvers.
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 |
The two soft tiers are separated by a different axis than the two hard ones. For hard covers, symcover trades optimality for speed while guaranteeing feasibility, and symcover_min is optimal. Both soft tiers minimize the same unconstrained objective, and differ instead in what they promise about reaching its minimum:
soft_symcoverandsoft_coverare always native and best-effort. They require no extension for any penalty, and they own their multistart — but what they return is a coordinate-descent fixed point, which for the non-convex and nonsmooth penalties need not be a minimizer.soft_symcover_minandsoft_cover_minreturn a true minimizer of the basin they start in, and may require an extension.AbsLog{2}is native; theAbsLinearpenalties need JuMP and Ipopt;AbsLog{1}is not implemented.
Both reduce to the same trade: cheap and always available, against best quality and possibly an extra dependency.
Under AbsLog{2} the objective is convex with a single minimizer, so the tiers coincide — soft_symcover is soft_symcover_min there, and likewise for the asymmetric pair. That is the degenerate case of the contract rather than an exception to it: with one minimizer there is nothing for a best-effort descent and a minimizer to disagree about. Under AbsLog{1} they part company most sharply — the soft AbsLog{1} covers are coordinate descents that reach a deterministic fixed point rather than a minimizer, and soft_symcover_min/soft_cover_min do not accept AbsLog{1} at all.
symcover, cover, and any native implementation can be recommended for production use, possibly with relaxed convergence bounds. The heuristic solvers are particularly fast: they run in $O(mn)$ time for an $m\times n$ matrix and often land within a few percent of the objective-minimal cover (see the quality tests involving test/testmatrices.jl). Native solvers (both hard and soft) are intermediate, still roughly $O(mn)$ but requiring many iterations (and for non-convex cases, multiple start points by default) for convergence; still, they are much faster than their JuMP-counterparts, which are provided mainly as a reference.
Objective-minimal covers
symcover_min and cover_min return a cover that minimizes the chosen penalty subject to the hard constraint. For the default AbsLog{2} penalty they are solved natively (no external solver) by penalty-continuation with a damped semismooth Newton iteration:
julia> using MatrixCovers
julia> A = [1 2 3; 6 5 4];
julia> a, b = cover(A); # fast heuristic
julia> aq, bq = cover_min(AbsLog{2}(), A); # AbsLog{2}-minimal, native
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.141281The native solver is near-exact (relative objective excess typically a few $\times 10^{-7}$, growing slowly with problem size) and orders of magnitude faster than a general-purpose convex solver. The other penalties — AbsLog{1} (a linear program) and the non-convex AbsLinear variants — are solved through JuMP and are loaded on demand as a package extension:
using JuMP, HiGHS # HiGHS for AbsLog penalties
using MatrixCovers
a = symcover_min(AbsLog{1}(), A) # L1-minimal symmetric hard cover
a, b = cover_min(AbsLog{1}(), A) # L1-minimal general hard coverThe soft *_min solvers divide along the same line, but not at the same place: soft_symcover_min and soft_cover_min solve AbsLog{2} natively and reach for JuMP with Ipopt only for the AbsLinear penalties. They do not accept AbsLog{1}; the soft AbsLog{1} covers are available through soft_symcover and soft_cover, which are native.
Uniqueness
The AbsLog{2}() penalty generally has a unique minimum, with one exception: row/column scaling a → γ*a, b → b/γ does not affect C and thus invisible to the objective function. For non-symmetric (i.e., not symcover) problems, the scaling of each is pinned separately by the balance convention ∑ n_i log a[i] = ∑ m_j log b[j], where n_i, m_j are the the nonzero counts of row i and column j, respectively. This convention is not scale-invariant but has no impact on the cover itself.
Other penalties may be more degenerate. The AbsLog{1}() penalty is identical over a whole face of the feasible polytope, and its members are genuinely different covers — the products a[i]*b[j] differ — that merely happen to score the same objective. To make the result deterministic, we select the one that additionally minimizes the AbsLog{2} objective.
AbsLinear penalties typically have isolated minima, so are not as degenerate as AbsLog{1}(), but these minima occur in separate basins. There is no guarantee of global optimality.
Starting points: initialize and refine
For objectives with multiple minima, the solver starts from a specified point and descends. At a lower level, this package's interface is organized in three layers:
Initializers name the starting points.
initialize_symcoverandinitialize_covertake astrategy—:geomean,:leaveout,:diagfeasible, or:hardcover— and return that point. Each is a property ofAalone; no objective is involved, so an initializer takes no penalty. A second keyword,feasible, says how the point is brought up to coveringA::inflate(the default) scales it bodily by one common factor,:boostraises only the rows touching a violated entry, and:noneleaves it as it is. The hard-cover solvers need a cover, so they take one of the first two; the soft covers want:none, since forcing the geometric mean to coverAwould spoil the very property that makes it the softAbsLog{2}optimum.The two feasible routes land on the boundary at different points, hence in different basins — which is why the choice is a named part of the start rather than an internal detail. The heuristic
coveris itself a composition of these: the geometric mean, boosted, then tightened.Refiners improve a starting point in place, and are the
!-suffixed forms of the solvers:symcover_min!,cover_min!,soft_symcover!,soft_cover!,soft_symcover_min!, andsoft_cover_min!validate the start, then optimize from it. Which basin they reach is the caller's choice, by construction, and supplying the start is the caller's job. The hard refiners require a start that coversA; the soft ones do not, since their objective constrains nothing — build theirs withfeasible=:none.Solvers bundle the two.
symcover_min,cover_min,soft_symcover,soft_cover,soft_symcover_min, andsoft_cover_minrefine every start on a menu (thestrategieskeyword, or the multistart's own list) and return the best cover bycover_objective, so their result depends onAand not on an initialization the caller never chose.
That is the rule for the whole grid: the plain form owns the menu, so its result is a property of A; the ! form refines the one start you give it, so its result is a property of A and that start. symcover! and cover! are the exception that proves it — they are initializers, not refiners, and construct their cover from scratch rather than reading the vector passed in.
For finer control, you can run these manually:
using JuMP, Ipopt # Ipopt for the AbsLinear penalties
using MatrixCovers
a = symcover_min(AbsLinear{2}(), A) # multistart over the whole menu
a = symcover_min(AbsLinear{2}(), A; strategies=(:geomean,)) # or commit to one start
a0 = initialize_symcover(A; strategy=:geomean) # or drive it yourself
symcover_min!(AbsLinear{2}(), a0, A)The same menu supplies the starting points of the soft_symcover and soft_cover multistarts, adding (by default) a few randomized perturbations of a base point up to a user-controllable number of starts.
For the convex AbsLog penalties the start cannot change the result, and the refiners accept one only so that the two families share an interface.
Worked example: roundoff in A \ b
Because a cover names each variable's natural scale, it also says how to measure a solution in units that do not depend on how the problem was parameterized.
Solving x = A \ b is contravariant: rescaling A → D*A*D and b → D*b sends x → x ./ d, while the cover is covariant, a → d .* a. The products x .* a are therefore unchanged, and ∑ᵢ |xᵢ * aᵢ| is a measure of the solution's size that is the same in every frame.
That quantity can be estimated from the magnitudes of A and b alone, without forming x at all:
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))
4.5The cover reports natural scales of 1000 and 2, and mag estimates the size of the solution measured against them — here within a factor of 1.5 of the truth:
julia> x = A \ b;
julia> sum(abs.(x .* a))
3.0Both numbers are scale-invariant, so the estimate is unchanged by any diagonal rescaling of the problem:
julia> d = [0.05, 3.0];
julia> Ad, bd = d .* A .* d', d .* b;
julia> ad = symcover(Ad);
julia> sum(abs(bi / ai) for (bi, ai) in zip(bd, ad))
4.5This makes eps(mag) a scale-invariant estimate of the roundoff floor of the sum. For a well-conditioned A, the error of the Float64 solve meets that floor:
julia> xbig = big.(A) \ big.(b);
julia> abs(sum(abs.(x .* a)) - sum(abs.(Float64.(xbig) .* a))) <= 2 * eps(mag)
trueThe estimate is built from magnitudes only, so it knows nothing about the conditioning of A or about cancellation during the solve. When A is ill-conditioned the true error sits far above the floor:
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)
trueFolding in the condition number of the normalized matrix A ./ (a .* a') — itself scale-invariant, since normalizing cancels the frame — restores a usable bound:
julia> κ = cond(Aill ./ (aill .* aill'));
julia> err <= 1e3 * eps(κ * magill)
trueIndex of available tools
MatrixCovers.AbsLinearMatrixCovers.AbsLogMatrixCovers.AbstractCoverPenaltyMatrixCovers.coverMatrixCovers.cover!MatrixCovers.cover_minMatrixCovers.cover_min!MatrixCovers.cover_objectiveMatrixCovers.foreach_supportMatrixCovers.foreach_support_symMatrixCovers.initialize_coverMatrixCovers.initialize_cover!MatrixCovers.initialize_symcoverMatrixCovers.initialize_symcover!MatrixCovers.iscoverMatrixCovers.soft_coverMatrixCovers.soft_cover!MatrixCovers.soft_cover_minMatrixCovers.soft_cover_min!MatrixCovers.soft_symcoverMatrixCovers.soft_symcover!MatrixCovers.soft_symcover_minMatrixCovers.soft_symcover_min!MatrixCovers.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 discontinuity at r=0 prevents zero entries in A from sending the objective value to infinity.
This leads to convex optimization problems in log space. AbsLog{1} typically has a flat minimum-basin in which members of an entire family of solutions are equally good. AbsLog{2}, except in degenerate cases like [0 1; 1 0], has a unique minimum.
See also: AbsLinear.
MatrixCovers.AbstractCoverPenalty — Type
AbstractCoverPenalty <: FunctionSupertype of the penalty functions ϕ that score a cover, and the type of the first argument of most of this package's API. The built-in subtypes are AbsLog and AbsLinear.
A penalty is a function of the single ratio r = |A[i,j]| / (a[i]*b[j]), and cover_objective sums it over the entries of A. Because ϕ sees only that ratio, and every diagonal rescaling of A leaves it fixed, any objective built from a penalty is automatically scale-invariant.
Extending
A subtype must be callable on a nonnegative real:
(::MyPenalty)(r::Real)r ranges over [0, Inf]. Both endpoints occur and neither may error: r = 0 whenever A[i,j] is zero, and cover_objective passes typemax for an entry left uncovered by a zero scale. Penalties are conventionally singleton structs.
That call is the whole contract, and it buys exactly one thing: cover_objective works for any subtype. The solvers do not. Every solver in this package dispatches on a concrete built-in penalty — AbsLog{2} is solved natively, the AbsLinear penalties through JuMP — so a custom subtype passed to symcover_min, soft_symcover, or any other solver raises a MethodError. Scoring covers with your own penalty is supported; minimizing it is not.
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 initialization is the AbsLog{2} unconstrained minimum (geometric mean of nonzero entries per row/column). It is then boosted to feasibility by a greedy max-deficit rule (the most-violated entries are covered first), and maxiter tightening iterations are applied.
Only the products a[i] * b[j] are determined by the problem: a -> c*a, b -> b/c leaves every one of them unchanged. The split is fixed by the balance convention ∑ nzaᵢ log a[i] = ∑ nzbⱼ log b[j] (nzaᵢ, nzbⱼ = nonzero counts of row i, column j), as it is throughout the package; see cover_min.
ϕ names the penalty the caller would like the cover to do well on. Currently, the heuristic covers ignore ϕ, although this behavior may change in future versions. 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.1917861235036982, 3.3022439546112836], [1.8169463196750033, 1.707049797767425, 2.5172301815198064])
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: the vectors a, b minimizing ∑_{i,j} ϕ(|A[i,j]|/(a[i]*b[j])) subject to a[i]*b[j] >= |A[i,j]| for every nonzero entry of A. The row/column scales are pinned to the balance convention ∑ nzaᵢ log a[i] = ∑ nzbⱼ log b[j] (nzaᵢ, nzbⱼ = nonzero counts of row i, column j) so the result is deterministic. The no-ϕ form defaults to AbsLog{2}(), matching cover.
Supported ϕ values:
AbsLog{2}(): solved natively (no external solver). Accepts keyword argumentsκs(the penalty-continuation schedule, default(1e2, 1e4, 1e6, 1e8)),maxiter(Newton steps per stage, default40), andlinsolve(the inner linear solve::auto/:denseuse a dense factorization of the reweighted normal equations;:lsqruses matrix-free LSQR (per-iteration cost O(nnz), intended for large sparse supports)).linsolvedefaults to:autofor denseA; theSparseMatrixCSCsparse method (from the SparseArrays extension) defaults to:lsqrinstead, since a dense factorization of the reweighted normal equations is the wrong solve whennnz ≪ n².AbsLog{1}(): requires JuMP and HiGHS.AbsLinear{1}(),AbsLinear{2}(): requires JuMP and Ipopt. These objectives are non-convex, so the solver returns the minimum of the basin it starts in. Rather than commit to one start, these methods refine each ofstrategies— theinitialize_covermenu, by default(:hardcover, :geomean)— and return the best cover found, at a cost of one solve per start. The result is the best local minimum on that menu: the multistart is a hedge against a poor basin, not a certificate of global optimality.
The AbsLog penalties are convex in the log-scales, so for them the minimum value is unique and no such hedge is needed. AbsLog{2} has a unique minimizer too. AbsLog{1} does not: its optimum is a whole face of the feasible polytope, whose members are genuinely different covers that happen to score alike. The one returned is the member of that face minimizing the AbsLog{2} objective.
Even the native solver is more expensive than the cover heuristic.
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 starting cover (a, b) into the ϕ-minimal asymmetric hard cover of A, in place. This is the asymmetric counterpart of symcover_min!, and carries the same contract on the start: strict positivity on every supported row and column, coverage of A to within roundoff, and inert scales on the unsupported rows and columns. The no-ϕ form defaults to AbsLog{2}(), matching cover_min, whose keyword arguments and supported ϕ values these methods share.
The product a[i]*b[j] is unchanged by a -> c*a, b -> b/c, so the start is read only up to that gauge: (a, b) and (2a, b/2) give the same result, and the result itself is pinned to the balance convention of cover_min.
See also: initialize_cover, cover_min, symcover_min!.
MatrixCovers.cover_objective — Method
cover_objective(ϕ, a, b, A)
cover_objective(ϕ, a, A)Compute the cover objective ∑_{i,j} ϕ(|A[i,j]| / (a[i] * b[j])) for the given penalty function ϕ. The two-argument form is for symmetric matrices where the cover is a*a'.
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).
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, v) once for every entry of A whose magnitude v = abs(A[i, j]) is nonzero, and return nothing. Entries that are zero are skipped, so f never sees v == 0. The order is whatever suits A's storage and is not part of the contract; i and j are A's own indices, so offset axes are honored.
This is the hook through which cover algorithms read a matrix. Specializing it is what lets a storage type be covered in time proportional to its support rather than to length(A) — the package's own SparseMatrixCSC methods, which walk nzrange instead of the full grid, are the model.
Extending
To support a new matrix type, define
MatrixCovers.foreach_support(f, A::MyMatrix)which must call f(i, j, abs(A[i, j])) exactly once for each (i, j) with abs(A[i, j]) != 0, must not call f for any other entry (a stored zero is still a zero), and must return nothing. Emitting an entry twice double-counts it in the objective; omitting one silently drops a constraint, yielding a "cover" that does not cover.
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 unordered index pair rather than once per entry, and return nothing. Pairs are reported in the canonical orientation i <= j, the diagonal included, with v = abs(A[i, j]); zero pairs are skipped. A must be square, or a DimensionMismatch is thrown.
A must also be symmetric in value, not merely square — this is a precondition the function cannot check cheaply and does not try to. It is what makes reporting one member of each pair sufficient: a symmetric cover constrains a[i]*a[j] by a single magnitude, so visiting (j, i) as well would only duplicate it. Handing this an asymmetric matrix does not error; it silently covers a symmetrization of it, and which one is not specified.
(The Bidiagonal/Tridiagonal methods, whose two off-diagonal bands are stored separately and need not agree, report max(|A[i,j]|, |A[j,i]|) — the value a full-grid tighten would enforce on both entries. Under the precondition the two bands agree and this is just abs(A[i,j]); outside it, the choice is robustness, not a promise.)
Extending
To support a new matrix type, define
MatrixCovers.foreach_support_sym(f, A::MyMatrix)which must call f(i, j, v) exactly once for each pair i <= j with v = abs(A[i, j]) != 0, must not call f for zero pairs, and must return nothing. Reporting the same pair in both orientations double-counts it. A type whose storage is triangular (Symmetric{<:Any,<:SparseMatrixCSC} in this package's own extension) must map stored (i, j) with i > j back to (j, i) rather than emit it as found.
See also: foreach_support.
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.
MatrixCovers.initialize_cover — Method
a, b = initialize_cover(A; strategy=:hardcover, feasible=:inflate, kwargs...)Build a starting point for the cover of A, as consumed by cover_min and by the soft_cover multistart. This is the asymmetric analog of initialize_symcover, and takes the same feasible keyword, under which the result covers A as a[i]*b[j] >= abs(A[i,j]).
Two of the strategies carry over: :hardcover (the tightened hard cover of cover, forwarding maxiter) and :geomean (the AbsLog{2} unconstrained minimum). :leaveout and :diagfeasible have no asymmetric formulation and raise an ArgumentError, as does any unrecognized strategy or feasible.
Under every feasible setting the result is strictly positive on every supported row and column and exactly zero on the unsupported ones, and the split between a and b is fixed by the balance convention ∑ nzaᵢ log a[i] = ∑ nzbⱼ log b[j] that every asymmetric cover in the package uses (see 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 starting point for the symmetric cover of A, as consumed by symcover_min and by the soft_symcover multistart.
No penalty is taken: every strategy below is a property of A alone, so the starting point does not depend on the objective it will be refined against.
strategy names the point:
:geomean— the geometric mean of the nonzero entries of each row, and not a cover. It minimizes the soft AbsLog{2} objective exactly when every entry ofAis nonzero; on a sparse support it approximates that minimum, whichsoft_symcover_min(AbsLog{2}(), A)returns exactly.:leaveout— the geometric mean recomputed with the most-underweighted support entry dropped, which lands in the basin that treats that entry as effectively zero. Raises anArgumentErrorwhen no entry can be dropped (empty support, or dropping it would empty a row). Not a cover.:diagfeasible— a cover grown from the diagonal by nearest-neighbor propagation. Feasible by construction.:hardcover— the tightened hard cover ofsymcover, which is:geomeanboosted to feasibility and then tightened. Forwardsmaxiterto the tightening pass. Feasible by construction, sofeasiblehas no effect on it.
feasible names how the point is brought up to covering A — that is, to a[i]*a[j] >= abs(A[i,j]), up to the roundoff of the log-domain arithmetic:
:inflate(the default) multiplies every scale by the smallest common factor that achieves coverage, moving the point bodily and leaving its shape intact.:boostraises only the rows that touch a violated entry, so it changes the shape of the point. This is the routesymcoveritself takes.:nonereturns the strategy's own point, with no coverage guarantee. This is what the soft covers want: forcing the geometric mean to coverAwould destroy the very property that makes it the soft AbsLog{2} optimum.
The two feasible routes land on the boundary at different points, and so in different basins of the non-convex AbsLinear objectives — which is exactly why a menu of starts is worth having, and why the choice is exposed rather than fixed.
Under every setting the result is strictly positive on every row that carries support and exactly zero on every row that carries none.
An unrecognized strategy or feasible raises an ArgumentError.
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.
This is the inequality the whole package is organized around. cover, symcover, and the *_min solvers all satisfy it by construction — up to the roundoff noted below — so the predicate earns its keep mainly on covers that carry no such guarantee: those from soft_cover and soft_symcover, which penalize under-coverage rather than forbidding it, and covers a caller has adjusted by hand.
rtol and atol supply the slack the producing algorithm warrants, testing a[i]*b[j] >= abs(A[i,j])*(1 - rtol) - atol. Neither defaults to any slack at all, so the bare call is the exact inequality. Use rtol for roundoff that scales with entry magnitude (the log-domain arithmetic behind the heuristics warrants a few multiples of eps), and atol for the convergence tolerance of an iterative solver. atol is subtracted only when it is nonzero: abs(A[i,j]) - atol is undefined when the two carry different units, so an rtol-only check never forms it. That makes rtol the only one of the two meaningful for a dimensional A, whose entries need not share units — no single scalar atol is commensurate with all of them.
a and b must be nonnegative; a negative scale raises an ArgumentError. Zero is allowed, and is what every solver here returns for a row or column with no support. The requirement is not cosmetic: only nonnegativity makes it sound to skip the zero entries of A, which is what lets this run in time proportional to the support rather than to length(A).
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.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, and return it. This is the asymmetric counterpart of soft_symcover! and the refiner half of soft_cover, carrying the same contract on the start: finite and strictly positive on every supported row and column, inert (and zero on output) elsewhere, and under no obligation to cover A. Build one with initialize_cover and feasible=:none. The no-ϕ form defaults to AbsLinear{2}(), matching soft_cover.
The product a[i]*b[j] is unchanged by a -> c*a, b -> b/c, so the start is read only up to that gauge: (a, b) and (2a, b/2) give the same result. The result itself is pinned to the balance convention of cover_min, as every asymmetric cover in this package is.
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))Given a matrix A, return vectors a and b approximately minimizing the soft-cover objective ∑_{i,j} ϕ(|A[i,j]| / (a[i]*b[j])). This is the asymmetric analog of soft_symcover.
Unlike cover, there is no hard coverage constraint: a[i]*b[j] may be less than |A[i,j]|, with violations penalized by ϕ.
Supported penalty functions:
AbsLog{2}(): convex, and returns its exact unconstrained minimum from a single linear solve. Identical tosoft_cover_min(AbsLog{2}(), A)— with one minimizer there is nothing for a heuristic and a minimizer to disagree about.AbsLog{1}(): initializes from theAbsLog{2}()minimum, then refines by alternating weighted-median row and column updates, reaching a deterministic and scale-covariant fixed point. As insoft_symcover, that point is not in general a minimizer: each half-sweep minimizes exactly, but the objective's nonsmoothness couplesa[i]withb[j], so the descent can settle where no such sweep improves and the objective still sits materially above its minimum.soft_cover_mindoes not yet offer an exactAbsLog{1}alternative.AbsLinear{2}()(default): in the inverse-scale variablesu = 1 ./ a,v = 1 ./ b, the objective∑_{i,j∈S} (1 - |A[i,j]| u[i] v[j])²(sum over the nonzero supportS) is biconvex, so alternating least squares with the closed-form half-sweepsu[i] = ∑_j |A[i,j]| v[j] / ∑_j (|A[i,j]| v[j])² (dually for v[j])is monotone, stopping when the relative objective decrease drops below
1e-14or aftermaxitersweeps.AbsLinear{1}(): initializes from theAbsLinear{2}()result, then refines by alternating weighted-median updates — each row/column block is minimized exactly, so the descent is monotone. Its flat basins are broken by a deterministic lower-median tie-break, giving a scale-covariant representative.
Rows or columns of A that are entirely zero receive scale 0. As with cover, only the products a[i] * b[j] are determined by the problem; the split is fixed by the balance convention ∑ nzaᵢ log a[i] = ∑ nzbⱼ log b[j].
The objective is non-convex, so starts starting points are tried and the lowest-objective result kept: the geometric mean boosted until it covers A, the tightened hard cover cover, and — for the remaining starts — multiplicative log-normal perturbations a .* exp.(σ .* ξ), with spread σ, of that boosted point, ξ drawn from rng. Every start co-varies with an independent row/column rescaling of A and the objective is scale-invariant, so the selection is scale-covariant. The default rng is a fresh MersenneTwister(0) per call, making repeated calls (and the two frames of a covariance check) agree; pass your own rng for reproducibility you control, since default RNG streams are not stable across Julia versions. sigma is accepted as an ASCII 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 the ϕ-minimal asymmetric soft cover of A: minimizes ∑_{i,j} ϕ(|A[i,j]|/(a[i]*b[j])) with no coverage constraints. This is the asymmetric analog of soft_symcover_min. The no-ϕ form defaults to AbsLinear{2}(), matching soft_cover.
The row/column scales are pinned to the balance convention ∑ nzaᵢ log a[i] = ∑ nzbⱼ log b[j] (nzaᵢ, nzbⱼ = nonzero counts of row i, column j), as in cover_min: the objective depends on a and b only through the products a[i]*b[j], so without a convention the split between them would be arbitrary.
Supported ϕ values and required extensions:
AbsLog{2}(): solved natively (no external solver) — the same analytic geometric-mean minimumcovercomputes as its initial point. Convex, so the minimizer is unique.AbsLinear{1}(),AbsLinear{2}(): requires JuMP and Ipopt. These objectives are non-convex, so the solver returns the minimum of the basin it starts in. Rather than commit to one start, these methods refine each ofstrategies— theinitialize_covermenu, by default(:hardcover, :geomean), taken raw (feasible=:none, since the soft objective constrains nothing) — and return the best cover found, at a cost of one solve per start. The result is the best local minimum on that menu: the multistart is a hedge against a poor basin, not a certificate of global optimality.AbsLog{1}(): not yet implemented. The objective is an LP in log space, but its optimum is a face, and the lexicographic AbsLog{2} selection thatsymcover_minuses to pin one member of the corresponding hard face does not carry over: the hard face is bounded by the coverage constraints, while this one is a level set of an unconstrained piecewise- linear objective, across which the quadratic pulls far enough to cost most of the exactly tight residuals that makeAbsLog{1}worth choosing.
Every start on the menu co-varies with a rescaling of A and the objective is scale-invariant, so the selection — and hence the result — is scale-covariant.
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 the starting point (a, b) into the ϕ-minimal asymmetric soft cover of A, in place. This is the asymmetric counterpart of soft_symcover_min!. The no-ϕ form defaults to AbsLinear{2}(), matching soft_cover_min, whose supported ϕ values these methods share.
a and b must be strictly positive on every supported row and column; scales on unsupported rows and columns are inert, and are zero on output. As with soft_symcover_min! — and unlike cover_min! — the start need not cover A. Build one with feasible=:none.
The product a[i]*b[j] is unchanged by a -> c*a, b -> b/c, so the start is read only up to that gauge, and the result is pinned to the balance convention of soft_cover_min.
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 the starting point a into a symmetric soft cover of A, in place, and return it. The no-ϕ form defaults to AbsLinear{2}(), matching soft_symcover, whose supported ϕ values these methods share.
This is the refiner half of soft_symcover: the non-mutating form owns a multistart menu, so its result is a property of A, while this one descends from the single start you hand it, so its result is a property of A and that start. Building the start is the caller's job — see initialize_symcover, and pass feasible=:none, since a soft cover is under no obligation to cover A.
a must be finite and strictly positive on every row of A that carries support; scales on rows carrying no support are inert, and are zero on output. Unlike symcover_min!, a need not cover A — the soft objective imposes no coverage constraint.
maxiter bounds the descent sweeps; its default matches the corresponding soft_symcover method. Under AbsLog{2} the objective is convex with a unique minimizer, so the start is honored but not visible in the result.
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))Given a square matrix A assumed to be symmetric, return a vector a approximately minimizing the soft-cover objective ∑_{i,j} ϕ(|A[i,j]| / (a[i]*a[j])).
Unlike symcover, there is no hard coverage constraint: a[i]*a[j] may be less than |A[i,j]|, with violations penalized by ϕ.
Supported penalty functions:
AbsLog{2}(): convex, and returns its exact unconstrained minimum from a single linear solve. Identical tosoft_symcover_min(AbsLog{2}(), A)— with one minimizer there is nothing for a heuristic and a minimizer to disagree about.AbsLog{1}(): initializes from the AbsLog{2} minimum, then refines by coordinate descent with a log-space weighted-median step, reaching a deterministic and scale-covariant fixed point. That point is not in general a minimizer: each step minimizes exactly over one coordinate, but the objective's nonsmoothness couplesa[i]witha[j], so the descent can settle where no single-coordinate move improves and the objective still sits materially above its minimum.soft_symcover_mindoes not yet offer an exactAbsLog{1}alternative.AbsLinear{2}()(default): non-convex; refined by coordinate descent fromstartsscale-covariant starting points, keeping the lowest-objective result (see below).AbsLinear{1}(): initializes from theAbsLinear{2}()result, coordinate descent uses a weighted-median step.
For the AbsLinear penalties the objective is non-convex, so starts starting points are tried and the best kept, taken in this order: the geometric-mean minimum, the tightened hard cover, the geometric-mean minimum inflated uniformly until it covers A, a leave-one-out geometric mean that drops the support entry with the most negative log-residual (this start keeps the result continuous as an entry |A[i,j]| approaches zero), and — only when A has a zero entry — a greedy feasible cover. Any slots left over are multiplicative log-normal perturbations a .* exp.(σ .* ξ) of the geometric-mean point with spread σ, ξ drawn from rng; at the default starts=5 there is at most one such perturbation. Every start co-varies with a diagonal rescaling of A and the objective is scale-invariant, so the selection is scale-covariant. The default rng is a fresh MersenneTwister(0) per call, making repeated calls (and the two frames of a covariance check) agree; pass your own rng for reproducibility you control, since default RNG streams are not stable across Julia versions. sigma is accepted as an ASCII alias for σ.
See also: symcover, cover_objective, soft_symcover_min.
Examples
The multistart converges to the covariant minimizer to within its objective tolerance; round to compare against 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 the ϕ-minimal symmetric soft cover of A: minimizes ∑_{i,j} ϕ(|A[i,j]|/(a[i]*a[j])) with no coverage constraints. The no-ϕ form defaults to AbsLinear{2}(), matching soft_symcover.
Supported ϕ values and required extensions:
AbsLog{2}(): solved natively (no external solver). In log space the objective is a linear least-squares, so one solve settles it, and being convex it has a unique minimizer that no start can influence.linsolveselects the inner solve, exactly as insymcover_min.AbsLinear{1}(),AbsLinear{2}(): requires JuMP and Ipopt. These objectives are non-convex, so the solver returns the minimum of the basin it starts in. Rather than commit to one start, these methods refine each ofstrategies— theinitialize_symcovermenu, by default(:hardcover, :geomean, :leaveout), without forcing feasibility — and return the best cover found, at a cost of one solve per start.AbsLog{1}(): not yet implemented. The objective is an LP in log space, but its optimum is a face, and the lexicographic AbsLog{2} selection thatsymcover_minuses to pin one member of the corresponding hard face does not carry over: the hard face is bounded by the coverage constraints, while this one is a level set of an unconstrained piecewise- linear objective, across which the quadratic pulls far enough to cost most of the exactly tight residuals that makeAbsLog{1}worth choosing.
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 the starting point a into the ϕ-minimal symmetric soft cover of A, in place. This is the soft counterpart of symcover_min!, and the second half of the initialize/refine pair whose first half is initialize_symcover. The no-ϕ form defaults to AbsLinear{2}(), matching soft_symcover_min, whose supported ϕ values these methods share.
a must be strictly positive on every row of A that carries support; scales on rows carrying no support are inert, and are zero on output. Unlike symcover_min!, a need not cover A — the soft objective imposes no coverage constraint, and the natural starts do not satisfy one. Pass feasible=:none when building a start with initialize_symcover.
The AbsLinear penalties are non-convex, so the start selects the local minimum the solver descends into; that is why soft_symcover_min tries several rather than committing to one. Under AbsLog{2} the objective is convex with a unique minimizer, so the start is honored but not visible in the result.
See also: initialize_symcover, soft_symcover_min, symcover_min!.
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 initialization is the AbsLog{2} unconstrained minimum (geometric mean of nonzero entries per row). It is then boosted to feasibility by a greedy max-deficit rule (the most-violated entries are covered first), and maxiter iterations of the tightening algorithm (Algorithm 1 of the manuscript) are applied.
ϕ names the penalty the caller would like the cover to do well on. Currently, the heuristic covers ignore ϕ, although this behavior may change in future versions. 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 ∑_{i,j} ϕ(|A[i,j]|/(a[i]*a[j])) subject to a[i]*a[j] >= |A[i,j]| for every nonzero entry of A. The no-ϕ form defaults to AbsLog{2}(), matching symcover.
Supported ϕ values:
AbsLog{2}(): solved natively (no external solver). Accepts keyword argumentsκs(the penalty-continuation schedule, default(1e2, 1e4, 1e6, 1e8)),maxiter(Newton steps per stage, default40), andlinsolve(the inner linear solve::auto/:denseuse a dense factorization of the reweighted normal equations;:lsqruses matrix-free LSQR (per-iteration cost O(nnz), intended for large sparse supports)).linsolvedefaults to:autofor denseA; theSparseMatrixCSC/Symmetric/Hermitiansparse methods (from the SparseArrays extension) default to:lsqrinstead, since a dense factorization of the reweighted normal equations is the wrong solve whennnz ≪ n².AbsLog{1}(): requires JuMP and HiGHS.AbsLinear{1}(),AbsLinear{2}(): requires JuMP and Ipopt. These objectives are non-convex, so the solver returns the minimum of the basin it starts in. Rather than commit to one start, these methods refine each ofstrategies— theinitialize_symcovermenu, by default(:hardcover, :geomean, :leaveout)— and return the best cover found, at a cost of one solve per start. A strategy thatAadmits no start for is skipped. The result is the best local minimum on that menu: the multistart is a hedge against a poor basin, not a certificate of global optimality.
The AbsLog penalties are convex in the log-scales, so for them the minimum value is unique and no such hedge is needed. AbsLog{2} has a unique minimizer too. AbsLog{1} does not: its optimum is a whole face of the feasible polytope, whose members are genuinely different covers that happen to score alike. The one returned is the member of that face minimizing the AbsLog{2} objective.
Even the native solver is more expensive than the symcover heuristic.
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 starting cover a into the ϕ-minimal symmetric hard cover of A, in place. This is the second half of the initialize/refine pair: a must already be a starting point, as produced by initialize_symcover (or by symcover). The no-ϕ form defaults to AbsLog{2}(), matching symcover_min, whose keyword arguments and supported ϕ values these methods share.
a must be strictly positive on every row of A that carries support, and must cover A — a[i]*a[j] >= abs(A[i,j]) — to within the roundoff of the log-domain arithmetic; otherwise an ArgumentError is raised. Scales on rows carrying no support are inert: whatever they hold on input, they are zero on output.
How much the start matters depends on ϕ. Under the AbsLog penalties the result is start-independent: they are convex in the log-scales, AbsLog{2} has a unique minimizer, and AbsLog{1} — whose optimum is a whole face of equally-scoring covers — is pinned to the member of that face minimizing the AbsLog{2} objective. The AbsLinear penalties are non-convex, and the identified local minima depend on the start(s).
See also: initialize_symcover, symcover_min, cover_min!.