Skip to content

Commit e9e237c

Browse files
Merge #86
86: Add print_condition_number option r=dennisYatunin a=dennisYatunin Co-authored-by: Dennis Yatunin <[email protected]>
2 parents 93f16fa + cc464ad commit e9e237c

File tree

3 files changed

+97
-3
lines changed

3 files changed

+97
-3
lines changed

docs/src/newtons_method.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ KrylovMethod
1414
ForcingTerm
1515
ConstantForcing
1616
EisenstatWalkerForcing
17+
KrylovMethodDebugger
18+
PrintConditionNumber
1719
```
1820

1921
## Jacobian-free Newton-Krylov Method

src/solvers/newtons_method.jl

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,74 @@ function run!(alg::EisenstatWalkerForcing, cache, f, n)
255255
return rtol
256256
end
257257

258+
"""
259+
KrylovMethodDebugger
260+
261+
Prints information about the Jacobian matrix `j` and the preconditioner `M` (if
262+
it is available) that are passed to a Krylov method. This is done by calling
263+
`run!(::KrylovMethodDebugger, cache, j, M)`. The `cache` can be obtained with
264+
`allocate_cache(::KrylovMethodDebugger, x_prototype)`, where `x_prototype` is
265+
`similar` to `x`.
266+
"""
267+
abstract type KrylovMethodDebugger end
268+
269+
"""
270+
PrintConditionNumber()
271+
272+
Prints the condition number of the Jacobian matrix `j`, and, if a preconditioner
273+
`M` is available, also prints the condition number of `inv(M) * j` (i.e., the
274+
matrix that actually gets "inverted" by the Krylov method). This requires
275+
computing dense representations of `j` and `inv(M) * j`, which is likely to be
276+
significantly slower than the Krylov method itself.
277+
"""
278+
struct PrintConditionNumber <: KrylovMethodDebugger end
279+
280+
function allocate_cache(::PrintConditionNumber, x_prototype)
281+
l = length(x_prototype)
282+
FT = eltype(x_prototype)
283+
return (;
284+
dense_vector = Array{FT}(undef, l),
285+
dense_j = Array{FT}(undef, l, l),
286+
dense_inv_M = Array{FT}(undef, l, l),
287+
dense_inv_M_j = Array{FT}(undef, l, l),
288+
)
289+
end
290+
291+
function run!(::PrintConditionNumber, cache, j, M)
292+
(; dense_vector, dense_j, dense_inv_M, dense_inv_M_j) = cache
293+
dense_matrix_from_operator!(dense_j, dense_vector, j)
294+
if M === I
295+
@info "Condition number = $(cond(dense_j))"
296+
else
297+
dense_inverse_matrix_from_operator!(dense_inv_M, dense_vector, M)
298+
mul!(dense_inv_M_j, dense_inv_M, dense_j)
299+
@info "Condition number = $(cond(dense_inv_M_j)) ($(cond(dense_j)) \
300+
without the preconditioner)"
301+
end
302+
end
303+
304+
# Like Matrix(op::AbstractLinearOperator) from LinearOperators.jl, but in-place.
305+
function dense_matrix_from_operator!(dense_matrix, dense_vector, op)
306+
n_columns = size(dense_matrix)[2]
307+
dense_vector .= 0
308+
for column in 1:n_columns
309+
dense_vector[column] = 1
310+
mul!(view(dense_matrix, :, column), op, dense_vector)
311+
dense_vector[column] = 0
312+
end
313+
end
314+
315+
# Same as dense_matrix_from_operator!, but with ldiv! instead of mul!.
316+
function dense_inverse_matrix_from_operator!(dense_inv_matrix, dense_vector, op)
317+
n_columns = size(dense_inv_matrix)[2]
318+
dense_vector .= 0
319+
for column in 1:n_columns
320+
dense_vector[column] = 1
321+
ldiv!(view(dense_inv_matrix, :, column), op, dense_vector)
322+
dense_vector[column] = 0
323+
end
324+
end
325+
258326
"""
259327
KrylovMethod(;
260328
jacobian_free_jvp = nothing,
@@ -265,6 +333,7 @@ end
265333
solve_kwargs = (;),
266334
disable_preconditioner = false,
267335
verbose = false,
336+
debugger = nothing,
268337
)
269338
270339
Finds an approximation `Δx[n] ≈ j(x[n]) \\ f(x[n])` for Newton's method such
@@ -312,6 +381,10 @@ All of the arguments and keyword arguments used to construct and run the solver
312381
can be modified using `args`, `kwargs`, and `solve_kwargs`. So, the default
313382
behavior of this wrapper can be easily overwritten, and any features of
314383
`Krylov.jl` that are not explicitly covered by this wrapper can still be used.
384+
385+
If `verbose` is `true`, the residual `‖f(x[n]) - j(x[n]) * Δx[n]‖` is printed on
386+
each iteration of the Krylov method. If a debugger is specified, it is run
387+
before the call to `Kyrlov.solve!`.
315388
"""
316389
Base.@kwdef struct KrylovMethod{
317390
J <: Union{Nothing, JacobianFreeJVP},
@@ -320,6 +393,7 @@ Base.@kwdef struct KrylovMethod{
320393
A <: Tuple,
321394
K <: NamedTuple,
322395
S <: NamedTuple,
396+
D <: Union{Nothing, KrylovMethodDebugger},
323397
}
324398
jacobian_free_jvp::J = nothing
325399
forcing_term::F = ConstantForcing(0)
@@ -329,29 +403,34 @@ Base.@kwdef struct KrylovMethod{
329403
solve_kwargs::S = (;)
330404
disable_preconditioner::Bool = false
331405
verbose::Bool = false
406+
debugger::D = nothing
332407
end
333408

334409
function allocate_cache(alg::KrylovMethod, x_prototype)
335-
(; jacobian_free_jvp, forcing_term, type, args, kwargs) = alg
410+
(; jacobian_free_jvp, forcing_term, type, args, kwargs, debugger) = alg
336411
@assert alg.type isa Type{<:Krylov.KrylovSolver}
337412
l = length(x_prototype)
338413
return (;
339414
jacobian_free_jvp_cache = isnothing(jacobian_free_jvp) ? nothing :
340415
allocate_cache(jacobian_free_jvp, x_prototype),
341416
forcing_term_cache = allocate_cache(forcing_term, x_prototype),
342417
solver = type(l, l, args..., Krylov.ktypeof(x_prototype); kwargs...),
418+
debugger_cache = isnothing(debugger) ? nothing :
419+
allocate_cache(debugger, x_prototype),
343420
)
344421
end
345422

346423
function run!(alg::KrylovMethod, cache, Δx, x, f!, f, n, j = nothing)
347424
(; jacobian_free_jvp, forcing_term, type, solve_kwargs) = alg
348-
(; disable_preconditioner, verbose) = alg
349-
(; jacobian_free_jvp_cache, forcing_term_cache, solver) = cache
425+
(; disable_preconditioner, verbose, debugger) = alg
426+
(; jacobian_free_jvp_cache, forcing_term_cache, solver, debugger_cache) =
427+
cache
350428
jΔx!(jΔx, Δx) = isnothing(jacobian_free_jvp) ? mul!(jΔx, j, Δx) :
351429
run!(jacobian_free_jvp, jacobian_free_jvp_cache, jΔx, Δx, x, f!, f)
352430
opj = LinearOperator(eltype(x), length(x), length(x), false, false, jΔx!)
353431
M = disable_preconditioner || isnothing(j) || isnothing(jacobian_free_jvp) ?
354432
I : j
433+
run!(debugger, debugger_cache, opj, M)
355434
ldiv = true
356435
atol = zero(eltype(Δx))
357436
rtol = run!(forcing_term, forcing_term_cache, f, n)

test/test_newtons_method.jl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ function linear_equation(FT, n)
1010
x_init = zeros(FT, n)
1111
return (f!, j!, x_exact, x_init)
1212
end
13+
1314
function nonlinear_equation(FT, n)
1415
rng = MersenneTwister(1)
1516
A = rand(rng, FT, n, n)
@@ -66,3 +67,15 @@ end
6667
end
6768
end
6869
end
70+
71+
@testset "Dense Matrix From Operator" begin
72+
n = 10
73+
rng = MersenneTwister(1)
74+
A = rand(rng, n, n)
75+
vector = similar(A, n)
76+
matrix = similar(A, n, n)
77+
ClimaTimeSteppers.dense_matrix_from_operator!(matrix, vector, A)
78+
@test matrix == A
79+
ClimaTimeSteppers.dense_inverse_matrix_from_operator!(matrix, vector, lu(A))
80+
@test matrix inv(A)
81+
end

0 commit comments

Comments
 (0)