Skip to content

Commit 912b442

Browse files
authored
Define each solver keyword default in one place (#108)
The public solver functions forward their keywords to one options function per solver family, which holds the defaults: `_powermean_options` for the PowerMean soft covers and `noise_cover`, `_auglag_options` (with `_auglag_linsolve_options`) for the minimal covers, `cover_transversal`, and the AbsLog{2} soft covers, and `_balanced_options` for `cover_transversal(::PowerMean, A)`. A keyword outside a function's documented set, including the internal keywords of the workers, throws a MethodError before any work is done. Docstrings interpolate the constants behind their stated defaults, so the documented values follow the code. `soft_covers.jl` is included after `minimal_covers.jl` so that those constants are defined first. Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 0b8d00b commit 912b442

13 files changed

Lines changed: 173 additions & 142 deletions

‎src/MatrixCovers.jl‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ include("heuristic_covers.jl")
3131
include("dense_heuristic.jl") # full-grid kernels for the heuristic covers
3232
include("gram_covers.jl") # symmetric covers of A'*W*A, and the cover of A for a fixed row scale
3333
include("initializers.jl") # the start menu; consumed by both solver families below
34-
include("soft_covers.jl")
3534
include("powermean.jl") # the PowerMean soft covers, the soft-cover default
3635
include("sparse_cholesky.jl") # CHOLMOD factorizations for the minimal-cover solvers
3736
include("tree_cholesky.jl") # fill-free factorization of forest-patterned matrices, for preconditioning
3837
include("minimal_covers.jl")
38+
include("soft_covers.jl") # soft-cover entry points; their docstrings show constants defined above
3939
include("active_set.jl") # exact solution of the AbsLog{2} problem from a near-optimal point
4040
include("transversal.jl") # transversal-tight minimal covers
4141
include("balanced.jl") # transversal-tight covers pushed toward a target ratio

‎src/balanced.jl‎

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,16 @@ const BALANCED_CG_RTOL = 1e-4
2727
const BALANCED_NRIDGE = 4
2828
const BALANCED_RIDGE_GROWTH = 1e4
2929

30+
# The solver keywords of `cover_transversal(::PowerMean, A)`, with their defaults;
31+
# `maxiter = nothing` selects a limit that grows with the size of `A`. Public methods
32+
# pass user keywords through this function, so that any other keyword throws a
33+
# `MethodError`.
34+
_balanced_options(; maxbalance::Bool=false, tol::Real=BALANCED_TOL,
35+
maxiter::Union{Integer,Nothing}=nothing, fillbudget::Real=LSQR_FILL_BUDGET,
36+
flopbudget::Real=LSQR_FLOP_BUDGET) = (; maxbalance, tol, maxiter, fillbudget, flopbudget)
37+
3038
"""
31-
a, b, stats = cover_transversal(ρ::PowerMean{p}, A; target=1e-2, maxbalance=false, tol=1e-9, maxiter, fillbudget, flopbudget)
39+
a, b, stats = cover_transversal(ρ::PowerMean{p}, A; target=1e-2, maxbalance=false, tol=$(BALANCED_TOL), maxiter, fillbudget, flopbudget)
3240
3341
Return a hard cover of the square matrix `A` whose aim is to make the scaled
3442
matrix `A ./ (a .* b')` as diagonally dominant as possible, given a permutation
@@ -145,7 +153,7 @@ one scale per block.
145153
146154
## Keywords
147155
148-
`tol` (default `1e-9`) is the stopping tolerance: the groups are stationary when
156+
`tol` (default `$(BALANCED_TOL)`) is the stopping tolerance: the groups are stationary when
149157
the derivative of the objective with respect to each group's scale is at most
150158
`tol` times the sum of the magnitudes of its terms. The solve stops unconverged
151159
when roundoff keeps the Newton step from reducing the objective before then:
@@ -162,12 +170,9 @@ times that of the graph, and of order `r^(-p)` where cycles of entries hold
162170
ratios `r` far below `target`, so a very small `target^p` or a large `p`
163171
degrades the linear solves and the convergence.
164172
"""
165-
function cover_transversal(ρ::PowerMean{p}, A::AbstractMatrix; target::Real=1e-2,
166-
maxbalance::Bool=false,
167-
tol::Real=BALANCED_TOL, maxiter::Union{Integer,Nothing}=nothing,
168-
fillbudget::Real=LSQR_FILL_BUDGET,
169-
flopbudget::Real=LSQR_FLOP_BUDGET) where {p}
170-
a, b, nt = _cover_transversal_balanced(A, p, target; maxbalance, tol, maxiter, fillbudget, flopbudget)
173+
function cover_transversal(ρ::PowerMean{p}, A::AbstractMatrix; target::Real=1e-2, kwargs...) where {p}
174+
# The solver keywords pass through to `_balanced_options`, which holds their defaults.
175+
a, b, nt = _cover_transversal_balanced(A, p, target; _balanced_options(; kwargs...)...)
171176
return a, b, BalancedCoverStats{_auglag_stats_type(A)}(nt)
172177
end
173178

@@ -195,12 +200,10 @@ cover_transversal!(::AbsLog{2}, a::AbstractVector, b::AbstractVector, A::Abstrac
195200
# Worker for `cover_transversal(::PowerMean{p}, A; target)`, returning `(a, b, nt)`
196201
# with the statistics as a named tuple. `transversal` is as for
197202
# `_transversal_system`.
198-
function _cover_transversal_balanced(A::AbstractMatrix, p::Real, target::Real; maxbalance::Bool=false,
199-
tol::Real=BALANCED_TOL,
200-
maxiter::Union{Integer,Nothing}=nothing,
201-
fillbudget::Real=LSQR_FILL_BUDGET, flopbudget::Real=LSQR_FLOP_BUDGET,
203+
function _cover_transversal_balanced(A::AbstractMatrix, p::Real, target::Real;
202204
transversal::Union{Bool,AbstractVector{<:Integer}}=true,
203-
fname::Symbol=:cover_transversal)
205+
fname::Symbol=:cover_transversal, kwargs...)
206+
(; maxbalance, tol, maxiter, fillbudget, flopbudget) = _balanced_options(; kwargs...)
204207
isfinite(p) || throw(ArgumentError("$fname requires a finite exponent, got PowerMean{$p}"))
205208
0 < target <= 1 || throw(ArgumentError("$fname: target must satisfy 0 < target <= 1, got $target"))
206209
tol > 0 || throw(ArgumentError("$fname: tol must be positive, got $tol"))
@@ -454,8 +457,7 @@ end
454457
#
455458
# Returns `(y, nt)`, with `nt` the fields of `BalancedCoverStats`.
456459
function _balanced_newton!(y::Vector{T}, sys::EdgeList{T}, p::T, ℓ::T; tol::T, maxiter::Int,
457-
fillbudget::Real=LSQR_FILL_BUDGET,
458-
flopbudget::Real=LSQR_FLOP_BUDGET) where {T}
460+
fillbudget::Real, flopbudget::Real) where {T}
459461
nc = length(y)
460462
ne = length(sys.edges)
461463
P = first.(sys.edges)

‎src/minimal_covers.jl‎

Lines changed: 64 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,41 @@
11
# Objective-minimal hard covers. `AbsLog{2}` is native; extensions provide the
22
# other penalties.
33

4+
# ============================================================
5+
# Solver defaults
6+
# ============================================================
7+
8+
# Augmented-Lagrangian defaults. The KKT tolerance is in units of `eps` of the
9+
# working type.
10+
const AL_PENALTY = 1e2
11+
const AL_MAXOUTER = 32
12+
const AL_MAXITER = 40
13+
const AL_TOL_EPS = 1000
14+
15+
# Default storage limit, in bytes, for the LSQR Cholesky preconditioner.
16+
# Tripping this switches to spanning-forest preconditioning, which has no fill
17+
# but needs more iterations for convergence.
18+
const LSQR_FILL_BUDGET = 1 << 30
19+
20+
# Largest predicted numeric-factorization flop count per stored entry of the
21+
# support for which the LSQR Cholesky preconditioner is used. Above it,
22+
# repeated refactorization of the preconditioner costs more than the extra
23+
# LSQR iterations of spanning-forest preconditioning.
24+
const LSQR_FLOP_BUDGET = 8.0e3
25+
26+
# The keywords of the native `AbsLog{2}` solvers, with their defaults: those that
27+
# choose the linear solver, which the soft covers accept, and the full set of the
28+
# minimal covers and `cover_transversal`. Public methods pass user keywords
29+
# through these functions, so that any other keyword throws a `MethodError`; the
30+
# workers `_symcover_min_abslog2` and `_cover_min_abslog2` apply `_auglag_options`
31+
# again to fill in the defaults for internal callers. `tol = nothing` selects the
32+
# tolerance `AL_TOL_EPS * eps` of the working type.
33+
_auglag_linsolve_options(; linsolve::Symbol=:auto, fillbudget::Real=LSQR_FILL_BUDGET,
34+
flopbudget::Real=LSQR_FLOP_BUDGET) = (; linsolve, fillbudget, flopbudget)
35+
_auglag_options(; θ::Real=AL_PENALTY, maxouter::Int=AL_MAXOUTER, maxiter::Int=AL_MAXITER,
36+
tol::Union{Real,Nothing}=nothing, kwargs...) =
37+
(; θ, maxouter, maxiter, tol, _auglag_linsolve_options(; kwargs...)...)
38+
439
# ============================================================
540
# Public interface
641
# ============================================================
@@ -36,10 +71,10 @@ solves throw a [`SolverFailure`](@ref MatrixCovers.SolverFailure) instead.
3671
# Extended help
3772
3873
The native solver accepts `θ` (initial augmented-Lagrangian penalty, default
39-
`1e2`), `maxouter` (multiplier updates, default `32`; `0` returns the
74+
`$(AL_PENALTY)`), `maxouter` (multiplier updates, default `$(AL_MAXOUTER)`; `0` returns the
4075
unconstrained fit, shifted to cover `A`, with `converged(stats) == false` and no
4176
warning), `maxiter` (Newton steps per update), `tol` (the tolerance
42-
on the KKT residual, default `1000*eps` of the working type), `fillbudget` and
77+
on the KKT residual, default `$(AL_TOL_EPS)*eps` of the working type), `fillbudget` and
4378
`flopbudget` (see below), and `linsolve`:
4479
4580
- `:dense` factorizes dense normal equations at O(n³) per Newton step.
@@ -55,8 +90,8 @@ positive multiplier remains on an entry with slack.
5590
5691
The budgets choose the preconditioner of `:lsqr`. For `Float64`, it is a sparse
5792
Cholesky factorization when the factor's predicted storage is at most
58-
`fillbudget` bytes (default `2^30`) and its predicted flop count is at most
59-
`flopbudget` (default `8e3`) times the number of stored entries in the support
93+
`fillbudget` bytes (default `$(LSQR_FILL_BUDGET)`) and its predicted flop count is at most
94+
`flopbudget` (default `$(LSQR_FLOP_BUDGET)`) times the number of stored entries in the support
6095
of `A`, counting each symmetric off-diagonal pair twice, as `nnz` does for a
6196
fully stored matrix. Otherwise, and for every other element type, it is a
6297
factorization of a maximum-weight spanning forest of the couplings among
@@ -201,42 +236,34 @@ cover_min!(a::AbstractVector, b::AbstractVector, A::AbstractMatrix; kwargs...) =
201236
#
202237
# Each `θ` stage freezes the weights, solves the normal equations, and uses a
203238
# backtracking line search. A final uniform shift restores feasibility.
204-
function symcover_min(::AbsLog{2}, A::AbstractMatrix; θ::Real=AL_PENALTY, maxouter::Int=AL_MAXOUTER,
205-
maxiter::Int=AL_MAXITER, linsolve::Symbol=:auto,
206-
fillbudget::Real=LSQR_FILL_BUDGET, flopbudget::Real=LSQR_FLOP_BUDGET,
207-
tol::Union{Real,Nothing}=nothing)
208-
a, nt = _symcover_min_abslog2(A; θ, maxouter, maxiter, linsolve, fillbudget, flopbudget, tol)
239+
# The solver keywords of the `AbsLog{2}` methods pass through to
240+
# `_auglag_options`, which holds their defaults.
241+
function symcover_min(::AbsLog{2}, A::AbstractMatrix; kwargs...)
242+
a, nt = _symcover_min_abslog2(A; _auglag_options(; kwargs...)...)
209243
return a, AugmentedLagrangianStats(A, nt)
210244
end
211245

212246
# Asymmetric counterpart on stacked log scales `(α; β)`. The solve pins the
213247
# global gauge; `_cover_min_abslog2` regularizes and balances the remaining
214248
# component gauges.
215-
function cover_min(::AbsLog{2}, A::AbstractMatrix; θ::Real=AL_PENALTY, maxouter::Int=AL_MAXOUTER,
216-
maxiter::Int=AL_MAXITER, linsolve::Symbol=:auto,
217-
fillbudget::Real=LSQR_FILL_BUDGET, flopbudget::Real=LSQR_FLOP_BUDGET,
218-
tol::Union{Real,Nothing}=nothing)
219-
a, b, nt = _cover_min_abslog2(A; θ, maxouter, maxiter, linsolve, fillbudget, flopbudget, tol)
249+
function cover_min(::AbsLog{2}, A::AbstractMatrix; kwargs...)
250+
a, b, nt = _cover_min_abslog2(A; _auglag_options(; kwargs...)...)
220251
return a, b, AugmentedLagrangianStats(A, nt)
221252
end
222253

223254
# The convex objective has the same result from every valid start.
224-
function symcover_min!(::AbsLog{2}, a::AbstractVector, A::AbstractMatrix; θ::Real=AL_PENALTY,
225-
maxouter::Int=AL_MAXOUTER, maxiter::Int=AL_MAXITER, linsolve::Symbol=:auto,
226-
fillbudget::Real=LSQR_FILL_BUDGET, flopbudget::Real=LSQR_FLOP_BUDGET,
227-
tol::Union{Real,Nothing}=nothing)
255+
function symcover_min!(::AbsLog{2}, a::AbstractVector, A::AbstractMatrix; kwargs...)
256+
opts = _auglag_options(; kwargs...)
228257
_prepare_symcover_start!(a, A)
229-
anew, nt = _symcover_min_abslog2(A; start=a, θ, maxouter, maxiter, linsolve, fillbudget, flopbudget, tol)
258+
anew, nt = _symcover_min_abslog2(A; start=a, opts...)
230259
a .= anew
231260
return a, AugmentedLagrangianStats(A, nt)
232261
end
233262

234-
function cover_min!(::AbsLog{2}, a::AbstractVector, b::AbstractVector, A::AbstractMatrix; θ::Real=AL_PENALTY,
235-
maxouter::Int=AL_MAXOUTER, maxiter::Int=AL_MAXITER, linsolve::Symbol=:auto,
236-
fillbudget::Real=LSQR_FILL_BUDGET, flopbudget::Real=LSQR_FLOP_BUDGET,
237-
tol::Union{Real,Nothing}=nothing)
263+
function cover_min!(::AbsLog{2}, a::AbstractVector, b::AbstractVector, A::AbstractMatrix; kwargs...)
264+
opts = _auglag_options(; kwargs...)
238265
_prepare_cover_start!(a, b, A)
239-
anew, bnew, nt = _cover_min_abslog2(A; start=(a, b), θ, maxouter, maxiter, linsolve, fillbudget, flopbudget, tol)
266+
anew, bnew, nt = _cover_min_abslog2(A; start=(a, b), opts...)
240267
a .= anew
241268
b .= bnew
242269
return a, b, AugmentedLagrangianStats(A, nt)
@@ -1218,10 +1245,6 @@ function _symmul!(y::AbstractVector{T}, Cu::SparseMatrixCSC{T}, x::AbstractVecto
12181245
return y
12191246
end
12201247

1221-
# Augmented-Lagrangian defaults.
1222-
const AL_PENALTY = 1e2
1223-
const AL_MAXOUTER = 32
1224-
const AL_MAXITER = 40
12251248
# At the penalty cap the multiplier iteration contracts at a fixed rate. When
12261249
# the active-set finish follows, two consecutive updates that contract the KKT
12271250
# residual by less than this factor end the iteration.
@@ -1291,17 +1314,6 @@ _precond_pattern(::Type{T}, ::Union{Grid,DiffGrid}, v0, N::Int, mult) where {T}
12911314
# Scale-relative ridge for a positive-definite preconditioner.
12921315
_precond_ridge(dmax::T) where {T} = (dmax > 0 ? dmax : oneunit(T)) * sqrt(eps(T))
12931316

1294-
# Default storage limit, in bytes, for the LSQR Cholesky preconditioner.
1295-
# Tripping this switches to spanning-forest preconditioning, which has no fill
1296-
# but needs more iterations for convergence.
1297-
const LSQR_FILL_BUDGET = 1 << 30
1298-
1299-
# Largest predicted numeric-factorization flop count per stored entry of the
1300-
# support for which the LSQR Cholesky preconditioner is used. Above it,
1301-
# repeated refactorization of the preconditioner costs more than the extra
1302-
# LSQR iterations of spanning-forest preconditioning.
1303-
const LSQR_FLOP_BUDGET = 8.0e3
1304-
13051317
# The Newton steps of the multiplier iteration need only inexact solves while
13061318
# the active pattern is still changing. A stage starts LSQR at tolerance
13071319
# `LSQR_FORCING` times the KKT residual of the previous update, capped at
@@ -1425,9 +1437,8 @@ _dense_factor_type(::Type{T}) where {T} = LinearAlgebra.LU{T,Matrix{T},Vector{In
14251437
# active-set method of `_polish_cover` or `_polish_symcover`.
14261438
function _abslog2_auglag(sys::SupportSystem{T}, x0;
14271439
θ::Real, maxouter::Int, maxiter::Int, linsolve::Symbol, boost::Bool,
1428-
fillbudget::Real=LSQR_FILL_BUDGET,
1429-
flopbudget::Real=LSQR_FLOP_BUDGET, zeroslack::Bool=true,
1430-
tol::Union{Real,Nothing}=nothing) where {T}
1440+
fillbudget::Real, flopbudget::Real, zeroslack::Bool,
1441+
tol::Union{Real,Nothing}) where {T}
14311442
θ > 1 || throw(ArgumentError("θ must exceed 1; got $θ"))
14321443
tol === nothing || (isfinite(tol) && tol >= 0) ||
14331444
throw(ArgumentError("tol must be finite and nonnegative; got $tol"))
@@ -1840,7 +1851,7 @@ function _abslog2_auglag(sys::SupportSystem{T}, x0;
18401851
x = x0 === nothing ? solve_weighted(zeros(T, N), nothing) : x0
18411852
# Absolute thresholds preserve covariance under rescaling because the
18421853
# residuals are logarithmic.
1843-
vtol = tol === nothing ? 1000 * eps(T) : T(tol)
1854+
vtol = tol === nothing ? AL_TOL_EPS * eps(T) : T(tol)
18441855
# Precision bounds the usable penalty: the cap is `1e6` (LSQR) or `1e8`
18451856
# (exact solves) in `Float64` and grows as `1/sqrt(eps(T))`.
18461857
θcap = T(use_lsqr ? 1e6 : 1e8) * sqrt(T(eps(Float64)) / eps(T))
@@ -1980,12 +1991,9 @@ end
19801991
# `_polish_difference_qp` (`primal`, `ptol`, `dual`, `dtol`), and
19811992
# `stats.converged == stats.polish.certified`. A certified finish replaces the
19821993
# multiplier iterate, so `stats.kkt` then describes the iterate, not the result.
1983-
function _symcover_min_abslog2(A::AbstractMatrix; θ::Real=AL_PENALTY, maxouter::Int=AL_MAXOUTER,
1984-
maxiter::Int=AL_MAXITER, linsolve::Symbol=:auto, start=nothing,
1985-
boost::Bool=true, fillbudget::Real=LSQR_FILL_BUDGET,
1986-
flopbudget::Real=LSQR_FLOP_BUDGET, polish::Bool=true,
1987-
zeroslack::Bool=!polish, tol::Union{Real,Nothing}=nothing,
1988-
fname=:symcover_min)
1994+
function _symcover_min_abslog2(A::AbstractMatrix; start=nothing, boost::Bool=true, polish::Bool=true,
1995+
zeroslack::Bool=!polish, fname=:symcover_min, kwargs...)
1996+
(; θ, maxouter, maxiter, linsolve, fillbudget, flopbudget, tol) = _auglag_options(; kwargs...)
19891997
linsolve in (:auto, :dense, :lsqr, :woodbury) ||
19901998
throw(ArgumentError("linsolve must be :auto, :dense, :lsqr, or :woodbury; got :$linsolve"))
19911999
# Shared symmetry check for native symmetric minimal covers.
@@ -2112,13 +2120,11 @@ end
21122120
# stops at its first stall at the penalty cap rather than zeroing slack
21132121
# multipliers (`zeroslack`, which defaults to `!polish`). `stats` is as for
21142122
# `_symcover_min_abslog2`.
2115-
function _cover_min_abslog2(A::AbstractMatrix; θ::Real=AL_PENALTY, maxouter::Int=AL_MAXOUTER,
2116-
maxiter::Int=AL_MAXITER, linsolve::Symbol=:auto, start=nothing,
2117-
boost::Bool=true, fillbudget::Real=LSQR_FILL_BUDGET,
2118-
flopbudget::Real=LSQR_FLOP_BUDGET,
2123+
function _cover_min_abslog2(A::AbstractMatrix; start=nothing, boost::Bool=true,
21192124
transversal::Union{Nothing,Bool,AbstractVector{<:Integer}}=nothing,
2120-
polish::Bool=true, zeroslack::Bool=!polish, tol::Union{Real,Nothing}=nothing,
2121-
fname::Symbol=:cover_min)
2125+
polish::Bool=true, zeroslack::Bool=!polish, fname::Symbol=:cover_min,
2126+
kwargs...)
2127+
(; θ, maxouter, maxiter, linsolve, fillbudget, flopbudget, tol) = _auglag_options(; kwargs...)
21222128
tt = transversal !== nothing && transversal !== false
21232129
if tt
21242130
linsolve in (:auto, :dense, :lsqr, :woodbury) ||
@@ -2417,10 +2423,11 @@ end
24172423

24182424
# Soft `AbsLog{2}` covers are the unweighted initial solve with no feasibility
24192425
# shift. Convexity makes the constrained iteration and multistart unnecessary.
2426+
# They accept only the keywords of `_auglag_linsolve_options`.
24202427
_soft_symcover_abslog2(A::AbstractMatrix; kwargs...) =
2421-
_symcover_min_abslog2(A; maxouter=0, boost=false, fname=:soft_symcover, kwargs...)
2428+
_symcover_min_abslog2(A; maxouter=0, boost=false, fname=:soft_symcover, _auglag_linsolve_options(; kwargs...)...)
24222429
_soft_cover_abslog2(A::AbstractMatrix; kwargs...) =
2423-
_cover_min_abslog2(A; maxouter=0, boost=false, kwargs...)
2430+
_cover_min_abslog2(A; maxouter=0, boost=false, _auglag_linsolve_options(; kwargs...)...)
24242431

24252432
# JuMP reference used to test the native symmetric solver.
24262433
function symcover_min_jump end

‎src/noise_cover.jl‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ julia> round.(sum(Y ./ (a .* b').^2; dims=2); digits=8)
5151
```
5252
"""
5353
function noise_cover(V::AbstractMatrix; shrink::Real=0, tol::Union{Real,Nothing}=nothing, kwargs...)
54+
# The solver keywords pass through to `_powermean_options`, which holds their defaults.
55+
opts = _powermean_options(; kwargs...)
5456
_check_noise_variance(V)
5557
(isfinite(shrink) && shrink >= 0) ||
5658
throw(ArgumentError("noise_cover: `shrink` must be finite and nonnegative, got $shrink"))
@@ -97,8 +99,7 @@ function noise_cover(V::AbstractMatrix; shrink::Real=0, tol::Union{Real,Nothing}
9799
for (j, cj) in zip(eachindex(β), c)
98100
β[j] = cj > 0 ? log(cj) - ls2 : zero(T)
99101
end
100-
# The solver keywords pass through to `_powermean_solve!`, which holds their defaults.
101-
stats = _powermean_solve!(α, β, R, C, one(T), rtol; kwargs..., terms)
102+
stats = _powermean_solve!(α, β, R, C, one(T), rtol; opts..., terms)
102103
_warn_powermean_unconverged(:noise_cover, stats, :sweeps)
103104
_warn_powermean_unsettled(:noise_cover, stats)
104105

0 commit comments

Comments
 (0)