Observables

Observables from Observables.jl are containers which can invoke callbacks whenever their stored value is changed. Each property of a RecoPlan is an Òbservable to which functions can be attached. These function listen to changes of the Observables value. This can be used to store "logic" about the parameter within a plan, such as a function to update and visualize the current state of a plan or to calculate default values whenever a parameter changes.

In this documentation we will focus on the interaction between RecoPlans and Observables. For more details on the Observables API we refer to the package and Makie documentation.

using Observables
plan = RecoPlan(DirectRadonAlgorithm; parameter = RecoPlan(DirectRadonParameters;
        pre = RecoPlan(RadonPreprocessingParameters; frames = collect(1:3)),
        reco = RecoPlan(RadonBackprojectionParameters; angles = angles)))
RecoPlan{Main.OurRadonReco.DirectRadonAlgorithm}
└─ parameter::RecoPlan{Main.OurRadonReco.DirectRadonParameters}
   ├─ reco::RecoPlan{Main.OurRadonReco.RadonBackprojectionParameters}
   │  └─ angles = [0.0, 0.01232, 0.0246399, 0.0369599, 0.0492799, 0.0615999, 0.0739198, 0.0862398, 0.0985598, 0.11088  …  3.03071, 3.04303, 3.05535, 3.06767, 3.07999, 3.09231, 3.10463, 3.11695, 3.12927, 3.14159]
   └─ pre::RecoPlan{Main.OurRadonReco.RadonPreprocessingParameters}
      ├─ numAverages
      └─ frames = [1, 2, 3]

You can interact with parameters as if they are "normal" properties of a nested struct, which we shown in previous examples:

length(plan.parameter.pre.frames) == 3
true

Internally, these properties are stored as Observables to which we can attach functions:

on(plan.parameter.pre, :frames) do val
  @info "Number of frames: $(length(val))"
end
setAll!(plan, :frames, collect(1:42))
[ Info: Number of frames: 42

Clearing the plan also resets the Observables and removes all listeners:

clear!(plan)
setAll!(plan, :frames, collect(1:3))
plan.parameter.pre.frames
3-element Vector{Int64}:
 1
 2
 3

To directly access the Observable of a property you can use getindex on the plan with the property name:

plan.parameter.pre[:frames]
Observable{Any}([1, 2, 3])

Observables can also be used to connect two properties of a plan. For example, we can set the number of averages to the number of frames:

on(plan.parameter.pre, :frames) do val
  plan.parameter.pre.numAverages = length(val)
end
setAll!(plan, :frames, collect(1:42))
plan.parameter.pre.numAverages
42

It is important to avoid circular dependencies when connecting Observables, as this can lead to infinite loops of callbacks. Also note that the connection shown above will always overwrite the number of averages even if a user has set the value manually:

plan.parameter.pre.numAverages = 5
setAll!(plan, :frames, collect(1:42))
plan.parameter.pre.numAverages
42

To connect two properties without overwriting user-prvided values, we can use the LinkedPropertyListener provided by AbstractImageReconstruction:

clear!(plan)
listener = LinkedPropertyListener(plan.parameter.pre, :numAverages, plan.parameter.pre, :frames) do val
  @info "Setting default numAverages value to: $(length(val))"
  return length(val)
end
plan.parameter.pre.frames = collect(1:42)
plan.parameter.pre.numAverages = 1
plan.parameter.pre.frames = collect(1:3)
3-element Vector{Int64}:
 1
 2
 3

The LinkedPropertyListener can also be serialized and deserialized with the plan. However, for the function to be properly serialized, it should be a named function:

clear!(plan)
defaultAverages(val) = length(val)
LinkedPropertyListener(defaultAverages, plan.parameter.pre, :numAverages, plan.parameter.pre, :frames)
plan.parameter.pre.frames = collect(1:42)
@info plan.parameter.pre.numAverages == 42
toTOML(stdout, plan)
[ Info: true
_module = "Main.OurRadonReco"
_type = "RecoPlan{Main.OurRadonReco.DirectRadonAlgorithm}"

[parameter]
_module = "Main.OurRadonReco"
_type = "RecoPlan{Main.OurRadonReco.DirectRadonParameters}"

    [parameter.reco]
    _module = "Main.OurRadonReco"
    _type = "RecoPlan{Main.OurRadonReco.RadonBackprojectionParameters}"

    [parameter.pre]
    frames = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42]
    numAverages = 42
    _module = "Main.OurRadonReco"
    _type = "RecoPlan{Main.OurRadonReco.RadonPreprocessingParameters}"

        [[parameter.pre._listener.numAverages]]
        field = "frames"
        _module = "AbstractImageReconstruction"
        _type = "LinkedPropertyListener"
        plan = ["parameter", "pre"]

            [parameter.pre._listener.numAverages.fn]
            _module = "Main"
            _type = "defaultAverages"

To serialize custom listener one can inherit from AbstractPlanListener and follow the serialization How-To to implement the serialization. Listener are deserialized after the plan is built and the parameters are set. This means that the listener can access the parameters of the plan and the plan itself. For deserialization the listener has to implement loadListener!(::Type{<:AbstractPlanListener}, plan::RecoPlan, field::Symbol, dict::Dict{String, Any}, args...).


This page was generated using Literate.jl.