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.