Hooks

Hooks enable you to dynamically choose to inject functionality into algorithms. Some hooks are mandatory for certain algorithms, such as having a hook with the SemioticOpt.IsStoppingCondition trait when using SemioticOpt.GradientDescent. Others are purely there for you to use at your discretion. In this section, we'll take you through Traits, Using Predefined Hooks and how to Create Custom Hooks.

Traits

Hooks have traits. This is how our code knows when to execute which hook. For example, the code will execute a hook that has the SemioticOpt.IsStoppingCondition trait when evaluating whether it has finished optimising. A full list of traits follows.

StopTrait

This trait tells the code if it should execute a hook when checking stopping conditions. By default, the code automatically gives all hooks the SemioticOpt.IsNotStoppingCondition trait except in certain situations, which are explicitly documented with the hooks in question. We decide to stop code execution on an OR basis. This means that if any hook with SemioticOpt.IsStoppingCondition returns true, the code breaks out of the optimisation loop. Hooks with this trait must implement SemioticOpt.stophook.

SemioticOpt.IsStoppingConditionType

Exhibited by hooks that are stopping conditions.

Stopping condition hooks must emit a boolean value when SemioticOpt.stophook is called. If multiple hooks meet IsStoppingCondition are instantiated at the same time, we assume that they are meant to be OR'ed, so if any of them is true, optimisation is finished. For more complex behaviour, consider defining a more complex function using a single StopWhen.

To set this trait for a hook, run

julia> StopTrait(::Type{MyHook}) = IsStoppingCondition()
source
SemioticOpt.stophookFunction
stophook(::IsStoppingCondition, h::Hook, a::OptAlgorithm; locals...)

Raise an error if the hook is a stopping condition but has not implemented SemioticOpt.stophook.

source
stophook(h::IsNotStoppingCondition, a::OptAlgorithm; locals...)

If the hook isn't a stopping condition, it shouldn't be considered in the OR, so return false.

source
stophook(::IsStoppingCondition, h::StopWhen, a::OptAlgorithm; locals...)

Call the stop-function on a and ;locals.

source

PostIterationTrait

This trait tells the code if it should execute the hook after it calls SemioticOpt.iteration. All hooks default to the SemioticOpt.DontRunAfterIteration trait unless otherwise documented. Hooks with this positive variant of this trait SemioticOpt.RunAfterIteration must return z, the output of iteration. Hooks with this trait must also implement SemioticOpt.postiterationhook.

SemioticOpt.RunAfterIterationType

Exhibited by hooks that run after an iteration finishes.

Such hooks must take the output of SemioticOpt.iteration z as input and return z back, potentially modified.

To set this trait for a hook, run

julia> PostIterationTrait(::Type{MyHook}) = RunAfterIteration()
source

Using Predefined Hooks

Hooks always descend from the same abstract type.

Generally speaking, pre-defined hooks won't exhibit any positive traits unless explicitly documented otherwise. This serves two purposes. Firstly, it prevents any unexpected behaviour when the code executes. Secondly, it gives you more flexibility when choosing where you want a hook to be executed.

Warning

This section is incomplete! We will fill this out more once we have more traits and hooks to work with.

Note

We recommend you read the below StopWhen section as it explains details that we won't cover in the later sections since it'd be too repetitive.

StopWhen

This hook has the SemioticOpt.IsStoppingCondition trait. To use it, you would specify a function that returns a boolean value. If said value is true, then the code breaks out of the optimisation loop.

Let's take an example from our tests to demonstrate how to specify this hook. We'll also implement a dummy optimisation function that just implements a counter for illustration purposes.

julia> using SemioticOpt
julia> struct FakeOptAlg <: SemioticOpt.OptAlgorithm end
juila> a = FakeOptAlg()
julia> function counter(h, a)
            i = 0
            while !shouldstop(h, a; Base.@locals()...)
                i += 1
            end
            return i
        end
julia> h = StopWhen((a; locals...) -> locals[:i] ≥ 5, Dict())  # Stop when i ≥ 5
julia> i = counter((h,), a)
5

One thing you may not have seen is Base.@locals. This takes variables from the local scope (in this case, from the counter scope), and tracks them as a dictionary of symbols. Thus, since i is a local variable inside of counter, :i becomes a key in the Base.@locals dictionary. We pass this dictionary to the anonymous function stored by StopWhen. Then, we can use locals[:i] to get the value of i from the counter scope and check it against some condition. This is a powerful trick you may find yourself using a lot when dealing with hooks.

Logging

There are a few different types of Loggers. All exhibit the SemioticOpt.RunAfterIteration trait. They're used to log values from the optimisation loop. To use them, specify a function that gets a value from the SemioticOpt.maybeminimize! scope.

SemioticOpt.VectorLoggerType
VectorLogger{I<:Integer,T,V<:AbstractVector{T},F<:Function} <: Logger

A hook name for logging a value specified by f into a vector data at some frequency. This hook exhibits the RunAfterIteration trait.

The value we want to log is returned by f. Note that f gets access to variables in the SemioticOpt.minimize scope. This means, for example, that it can use locals[:i] to store the iteration number.

You must also be careful to correctly set the type of the data vector.

julia> using SemioticOpt
julia> struct FakeOptAlg <: SemioticOpt.OptAlgorithm end
julia> a = FakeOptAlg()
julia> function counter(h, a)
           i = 0
           while !shouldstop(h, a; Base.@locals()...)
               z = [1, 2]
               i += 1
               z = postiteration(h, a, z; Base.@locals()...)
           end
           return i
       end
julia> stop = StopWhen((a; kws...) -> kws[:i] ≥ 5)  # Stop when i ≥ 5
julia> h = VectorLogger(name="i", frequency=1, data=Int32[], f=(a; kws...) -> kws[:i])
julia> i = counter((h, stop), a)
julia> SemioticOpt.data(h)
5-element Vector{Int32}:
 1
 2
 3
 4
 5
source
SemioticOpt.ConsoleLoggerType
ConsoleLogger{I<:Integer,F<:Function} <: Logger

A hook name for logging a value specified by f to the console at some frequency. This hook exhibits the RunAfterIteration trait.

The value we want to log is returned by f. Note that f gets access to variables in the SemioticOpt.minimize scope. This means, for example, that it can use locals[:i] to store the iteration number.

julia> using SemioticOpt
julia> struct FakeOptAlg <: SemioticOpt.OptAlgorithm end
julia> a = FakeOptAlg()
julia> function counter(h, a)
           i = 0
           while !shouldstop(h, a; Base.@locals()...)
               z = [1, 2]
               i += 1
               z = postiteration(h, a, z; Base.@locals()...)
           end
           return i
       end
julia> stop = StopWhen((a; kws...) -> kws[:i] ≥ 5)  # Stop when i ≥ 5
julia> h = ConsoleLogger(name="i", frequency=1, f=(a; kws...) -> kws[:i])
julia> _ = counter((h, stop), a);
i: 1
i: 2
i: 3
i: 4
i: 5
source

Create Custom Hooks

When you create a custom hook, you need to follow three steps. The first is that you need to descend from SemioticOpt.Hook.

julia> using SemioticOpt
julia> struct MyHook <: Hook end

The second is that you need to ensure that you specify which traits you want that hook to exhibit. For example, let's say MyHook is a stopping condition. You'd want to implement.

julia> StopTrait(::Type{MyHook}) = IsStoppingCondition()

Finally, if you need non-default behaviour for when the hook executes, you'll need to implement whatever function(s) the code calls for that trait-type. For IsStoppingCondition, that's stophook. Say we want MyHook to immediately cause optimisation to finish. We'd implement

julia> stophook(h::MyHook, a::SemioticOpt.OptAlgorithm; locals...) = true

That's it! As long as your follow those three steps, you should be able to implement whatever hook you want!