New logger and iteration interface draft

Tue, 07 Dec 2021 11:41:07 +0200

author
Tuomo Valkonen <tuomov@iki.fi>
date
Tue, 07 Dec 2021 11:41:07 +0200
changeset 34
22a64e826ee7
parent 33
a60d2f12ef93
child 35
d881275c6564

New logger and iteration interface

src/AlgTools.jl file | annotate | diff | comparison | revisions
src/Iterate.jl file | annotate | diff | comparison | revisions
src/Logger.jl file | annotate | diff | comparison | revisions
--- a/src/AlgTools.jl	Mon Dec 06 11:52:10 2021 +0200
+++ b/src/AlgTools.jl	Tue Dec 07 11:41:07 2021 +0200
@@ -20,8 +20,10 @@
 """
 module AlgTools
 
+include("FunctionalProgramming.jl")
 include("StructTools.jl")
 include("LinkedLists.jl")
+include("Logger.jl")
 include("Iterate.jl")
 include("VectorMath.jl")
 include("Util.jl")
--- a/src/Iterate.jl	Mon Dec 06 11:52:10 2021 +0200
+++ b/src/Iterate.jl	Tue Dec 07 11:41:07 2021 +0200
@@ -4,56 +4,213 @@
 
 __precompile__()
 
+"""
+`module AlgTools.iterate`
+
+This module implements looping and status display utilities for iterate algorithms. It includes:
+- `make_iterate` and `make_iterate_log` for constructing functions for iterating
+- `simple_iterate` and `simple_iterage_log` for iteration
+- `default_iterate = make_iterate()` default implementation
+- `logrepr`
+"""
 module Iterate
 
 using Printf
+using ..Logger
+using ..FunctionalProgramming
 
 ##############
 # Our exports
 ##############
 
-export simple_iterate
+export make_iterate,
+       make_iterate_log,
+       simple_iterate,
+       simple_iterate_log,
+       default_iterate,
+       logrepr
+
+###########################
+# Internal helper routines
+###########################
+
+"""
+
+`logrepr(v)`
 
-########################################################################
-# Simple itertion function, calling `step()` `params.maxiter` times and 
+Create the displayed presentation for log items. Override for nice (condensed)
+presentation of rich log items, or define `show` if it is not defined for your type.
+"""
+function logrepr(v :: Any)
+    return "«", v, "»"
+end
+
+function logrepr(v :: Number)
+    return "J=", v
+end
+
+@inline function calculate_and_report(calc_objective, iter, maxiter)
+    v = calc_objective()
+    s = @sprintf("%d/%d J=%f%s\n", iter, params.maxiter, v, extra)
+    print_styled(iter, "/", maxiter, logrepr(v)...; color=:light_black)
+
+    return v
+end
+
+#########################################################################
+# Simple iteration function, calling `step()` `params.maxiter` times and
 # reporting objective value every `params.verbose_iter` iterations.
 # The function `step` should take as its argument a function that itself
 # takes as its argument a function that calculates the objective value
 # on demand.
-########################################################################
+#########################################################################
+
+"""
+`simple_iterate(step :: Function; maxiter = 1000, verbose_iter=100)`
+
+Iterate the function `step` for at most `maxiter` iterations, displaying
+status every `verbose_iter` iterations. The function `step` should accept one parameter,
+called `verbose` below, and return `true` if the iterations are to continue, `false`
+otherwise. The `verbose` parameter of `step` is itself a function that should be
+called with a function that will on demand, as determined by `verbose`, calculate
+the function value or other reported data. For example:
 
-function simple_verbosity(iter, params, calc_objective)
-    if params.verbose_iter!=0 && mod(iter, params.verbose_iter) == 0
-        v, extra₀ = calc_objective()
-        if isa(extra₀, AbstractString)
-            extra = " [$extra₀]"
-        else
-            extra = ""
+```julia
+simple_iterate() do verbose
+    # perform iterations
+    verbose() do
+        # calculate function value or other displayed data v
+        return
+    end
+    return true
+end
+```
+"""
+function simple_iterate(step :: Function ; maxiter=1000, verbose_iter=100)
+    for iter=1:maxiter
+        res = step() do calc_objective
+            if verbose_iter!=0 && mod(iter, verbose_iter) == 0
+                calculate_and_report(calc_objective, iter, params.maxiter)
+            end
+            true
         end
-        @printf("%d/%d J=%f%s\n", iter, params.maxiter, v, extra)
-        return true
-    end
-end
-
-function simple_iterate(step :: Function,
-                        params :: NamedTuple)
-    for iter=1:params.maxiter
-        step() do calc_objective
-            simple_verbosity(iter, params, calc_objective)
+        if !res
+            break
         end
     end
 end
 
-function simple_iterate(step :: Function,
-                        datachannel :: Channel{T},
-                        params :: NamedTuple) where T
-    for iter=1:params.maxiter
-        d = take!(datachannel)
-        step(d) do calc_objective
-            simple_verbosity(iter, params, calc_objective)
+"""
+`simple_iterate(step :: Function, ch :: Channel{D}; ...)`
+
+Version of `simple_iterate` that on each step reads a new value of type `D` from the channel `ch`.
+These are passed as the second parameter of `step`. The behaviour is otherwise as with
+the basic version of `simple_iterate`.
+"""
+function simple_iterate(step :: Function, ch :: Channel{D}; kwargs...) where D
+    simple_iterate(kwargs...) do calc_objective
+        step(calc_objective, take!(ch))
+    end
+end
+
+# constructor interface
+"""
+`make_iterate(; ...)` and `make_iterate(ch :: Channel{D}; ...)`
+
+These are constructor versions of `simple_iterate`, each returning a function
+that takes as its sole parameter the `step` parameter of `simple_iterate`.
+For example,
+
+```julia
+iterate = make_iterate(verbose_iter=10, maxiter=10000)
+
+iterate() do verbose
+    # use as the example in the documentation of `simple_iterate`
+end
+```
+"""
+make_iterate = curryflip(simple_iterate)
+
+# backwards-compatibility
+simple_iterate(step :: Function, params :: NamedTuple) =
+    simple_iterate(step; params...)
+simple_iterate(step :: Function, ch :: Channel{D}, params :: NamedTuple) where D =
+    simple_iterate(step, ch; params...)
+
+# Default parameterisation for use as default keyword argument in algorithm
+default_iterate = make_iterate()
+
+#########################################################################
+# Logging iteration function.
+#########################################################################
+
+"""
+`simple_iterate_log(step :: Function, log :: Log{T}; maxiter = 1000, verbose_iter=100)`
+
+Iterate the function `step` for at most `maxiter` iterations, logging and displaying
+results as determined by `verbose_iter`. The function `step` should accept one parameter,
+called `verbose` below, and return `true` if the iterations are to continue, `false`
+otherwise. The `verbose` parameter of `step` is itself a function that should be
+called with a function that will on demand, as determined by the `log`, calculate
+the function value or other logged and displayed data. For example:
+
+```julia
+log = Log{Float64}
+
+simple_iterate_log(log) do verbose
+    # perform iterations
+    verbose() do
+        # calculate function value or other logged data v
+        return v
+    end
+    return true
+end
+```
+"""
+function simple_iterate_log(step :: Function, log :: Log{T}; maxiter=1000) where T
+    for iter=1:maxiter
+        res = step() do calc_objective
+            if_log!(log, iter) do
+                calculate_and_report(calc_objective, iter, params.maxiter)
+            end
+            true
+        end
+        if !res
+            break
         end
     end
 end
 
+"""
+`simple_iterate_log(step :: Function, log :: Log{T}, ch :: Channel{D}; ...)`
+
+Version of `simple_iterate_log` that on each step reads a new value of type `D` from the channel `ch`.
+These are passed as the second parameter of `step`. The behaviour is otherwise as with
+the basic version of `simple_iterate_log`.
+"""
+function simple_iterate_log(step :: Function, log :: Log{T}, ch :: Channel{D}; kwargs...) where {T, D}
+    simple_iterate_log(log; kwargs...) do calc_objective
+        step(calc_objective, take!(ch))
+    end
 end
 
+"""
+`make_iterate_log(log :: log{T}; ...)` and `make_iterate_log(log :: Log{T}, ch :: Channel{D}; ...)`
+
+These are constructor versions of `simple_iterate_log`, each returning a function
+that takes as its sole parameter the `step` parameter of `simple_iterate_log`.
+For example,
+
+```julia
+log = Log{Float64}
+iterate = make_iterate_log(log, verbose_iter=10, maxiter=10000)
+
+iterate() do verbose
+    # proceed as in the example in the documentation of `simple_iterate`
+end
+```
+"""
+make_iterate_log = curryflip(simple_iterate_log)
+
+end
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/Logger.jl	Tue Dec 07 11:41:07 2021 +0200
@@ -0,0 +1,96 @@
+#
+# This module implements logging of intermediate computational results for
+# generating convergence graphs, etc.
+#
+
+"""
+Logging routines for intermediate computational results.
+Includes `Log`, `log!`, `if_log!`, and `write_log`.
+"""
+module Logger
+
+import DelimitedFiles: writedlm
+
+##############
+# Our exports
+##############
+
+export Log,
+       log!,
+       if_log!,
+       write_log
+
+##########
+# Logging
+##########
+
+"""
+`struct Log{T}`
+
+A log of items of type `T` along with log configuration.
+The constructor takes no arguments;  create the log with `Log{T}()` for `T` your
+data type, e.g., `Float64`.
+"""
+struct Log{T}
+    log :: Dict{Int, T}
+
+    Log{T}() where T = new{T}(Dict{Int, T}())
+end
+
+"""
+`if_log!(f :: Functdion, log :: Log{T}, i :: Int)`
+
+If based on the log settings, `i` is a verbose iteration, store the value of `f()`
+in the log at index `i`. Typically to be used with `do`-notation:
+
+```julia
+if_log!(log, iteration) do
+    # calculate value to be logged
+end
+```
+"""
+function if_log!(f :: Function, log :: Log{T}, i :: Int) where T
+    if mod(i, log.verbose_iterations)==0
+        log!(f, log, i)
+        #printstyled("$(i): $(val)\n", color=:light_black)
+    end
+end
+
+"""
+`log!(f :: Function, log :: Log{T}, i :: Int) `
+
+Store the value `f()` in the log at index `i`.
+"""
+function log!(f :: Function, log :: Log{T}, i :: Int) where T
+    val = f()
+    push!(log.log, i => val)
+end
+
+##############
+# Data export
+##############
+
+"""
+`write_log(filename :: AbstractString, log :: Log{T})`
+
+Write a `Log{T}` as a CSV file using `DelimitedFiles`.
+If T is a structural type, the field names are used as header fields.
+Otherwise a single `value` field is attempted to be written.
+"""
+function write_log(filename :: AbstractString, log :: Log{T}) where T
+    k = fieldnames(T)
+    @assert(:iter ∉ k)
+    
+    open(filename, "w") do io
+        # Write header
+        writedlm(io, isempty(k) ? [:iter :value] : ((:iter, k...),))
+        # Write values
+        iters = sort(collect(keys(log.log)))
+        for i ∈ iters
+            v = log.log[i]
+            writedlm(io, isempty(k) ? [i v] : ((i, (getfield(v, j) for j ∈ k)...),))
+        end
+    end
+end
+
+end # module

mercurial