Skip to content

Commit

Permalink
Add inital tests based on OurRadonReco example
Browse files Browse the repository at this point in the history
  • Loading branch information
nHackel committed Jan 28, 2025
1 parent 14c4574 commit 0ee8069
Show file tree
Hide file tree
Showing 10 changed files with 292 additions and 4 deletions.
11 changes: 9 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "AbstractImageReconstruction"
uuid = "a4b4fdbf-6459-4ec9-990d-77e1fa24a91b"
authors = ["nHackel <[email protected]> and contributors"]
version = "0.3.6"
version = "0.4.0"

[deps]
LRUCache = "8ac3fa9e-de4c-5943-b1dc-09c6b5f20637"
Expand All @@ -20,6 +20,13 @@ julia = "1.9"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
ImageGeoms = "9ee76f2b-840d-4475-b6d6-e485c9297852"
ImagePhantoms = "71a99df6-f52c-4da1-bd2a-69d6f37f3252"
RadonKA = "86de8297-835b-47df-b249-c04e8db91db5"
RegularizedLeastSquares = "1e9c538a-f78c-5de5-8ffb-0b6dbe892d23"
LinearOperatorCollection = "a4a2c56f-fead-462a-a3ab-85921a5f2575"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"

[targets]
test = ["Test"]
test = ["Test", "RadonKA", "ImagePhantoms", "ImageGeoms", "CairoMakie", "LinearOperatorCollection", "RegularizedLeastSquares", "Statistics"]
2 changes: 1 addition & 1 deletion src/RecoPlans/Listeners.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ Observables.on(f, plan::RecoPlan, property::Symbol; kwargs...) = on(f, plan[prop
Remove `f` from the listeners of `property` of `plan`.
"""
Observables.off(plan::RecoPlan, property::Symbol, f) = off(f, plan[property])
Observables.off(plan::RecoPlan, property::Symbol, f) = off(plan[property], f)

include("LinkedPropertyListener.jl")
10 changes: 10 additions & 0 deletions src/RecoPlans/RecoPlans.jl
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,21 @@ function setAll!(plan::RecoPlan{T}, name::Symbol, x) where {T<:AbstractImageReco
end
setAll!(plans::AbstractArray{<:AbstractRecoPlan}, name::Symbol, x) = foreach(p -> setAll!(p, name, x), plans)
setAll!(plan::RecoPlan{<:AbstractImageReconstructionAlgorithm}, name::Symbol, x) = setAll!(plan.parameter, name, x)
"""
setAll!(plan::AbstractRecoPlan; kwargs...)
Call `setAll!` with each given keyword argument.
"""
function setAll!(plan::AbstractRecoPlan; kwargs...)
for key in keys(kwargs)
setAll!(plan, key, kwargs[key])
end
end
"""
setAll!(plan::AbstractRecoPlan, dict::Union{Dict{Symbol, Any}, Dict{String, Any}})
Call `setAll!` with each entries of the dict.
"""
setAll!(plan::AbstractRecoPlan, dict::Dict{Symbol, Any}) = setAll!(plan; dict...)
setAll!(plan::AbstractRecoPlan, dict::Dict{String, Any}) = setAll!(plan, Dict{Symbol, Any}(Symbol(k) => v for (k,v) in dict))

Expand Down
20 changes: 20 additions & 0 deletions test/algorithm_api.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@testset "API" begin
# Partially testing if radon example is constructed correctly
# These things are mostly covered by Literate.jl examples
pre = RadonPreprocessingParameters(frames = collect(1:3))
back_reco = RadonBackprojectionParameters(;angles)
algo = DirectRadonAlgorithm(DirectRadonParameters(pre, back_reco))

@test parameter(algo) isa DirectRadonParameters

# High-level reco
@test isready(algo) == false
reco_1 = reconstruct(algo, sinograms)
@test isready(algo) == false

# Put!/take!
put!(algo, sinograms)
@test isready(algo)
reco_2 = take!(algo)
@test isapprox(reco_1, reco_2)
end
Empty file added test/caching.jl
Empty file.
57 changes: 57 additions & 0 deletions test/linkedproperty.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
export foo
foo(val) = length(val) % 2 == 0 ? 15 : 10

@testset "LinkedProperty" begin

@testset "Observable" begin
plan = RecoPlan(IterativeRadonAlgorithm; parameter = RecoPlan(IterativeRadonParameters;
pre = RecoPlan(RadonPreprocessingParameters),
reco = RecoPlan(IterativeRadonReconstructionParameters)))


avg_obs = plan.parameter.pre[:numAverages]
frame_obs = plan.parameter.pre[:frames]
@test isempty(avg_obs.listeners)
@test isempty(frame_obs.listeners)

# Connect frames -> averages
list = LinkedPropertyListener((val) -> length(val), plan.parameter.pre, :numAverages, plan.parameter.pre, :frames)
@test !isempty(avg_obs.listeners)
@test !isempty(frame_obs.listeners)

# Call function
@test ismissing(plan.parameter.pre.numAverages)
plan.parameter.pre.frames = collect(1:5)
@test plan.parameter.pre.numAverages == length(plan.parameter.pre.frames)

# Deactivate when user supplies parameter
plan.parameter.pre.numAverages = 50
plan.parameter.pre.frames = collect(1:1)
@test plan.parameter.pre.numAverages == 50
end

@testset "Serialization" begin
plan = RecoPlan(IterativeRadonAlgorithm; parameter = RecoPlan(IterativeRadonParameters;
pre = RecoPlan(RadonPreprocessingParameters),
reco = RecoPlan(IterativeRadonReconstructionParameters)))

# Connect across parameters
list = LinkedPropertyListener(foo, plan.parameter.reco, :iterations, plan.parameter.pre, :frames)
io = IOBuffer()
toTOML(io, plan)

seekstart(io)
plan_copy = loadPlan(io, [Main, AbstractImageReconstruction, OurRadonReco])

# Call function
@test ismissing(plan.parameter.pre.numAverages)

plan_copy.parameter.pre.frames = collect(1:5)
@test plan_copy.parameter.reco.iterations == foo(plan_copy.parameter.pre.frames)

# Deactivate when user supplies parameter
plan_copy.parameter.reco.iterations = -1
plan_copy.parameter.pre.frames = collect(1:2)
@test plan_copy.parameter.reco.iterations == -1
end
end
172 changes: 172 additions & 0 deletions test/reco_plan.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
@testset "RecoPlan" begin
pre = RadonPreprocessingParameters(frames = collect(1:3))
reco = IterativeRadonReconstructionParameters(; shape = size(images)[1:3], angles = angles, iterations = 1, reg = [L2Regularization(0.001), PositiveRegularization()], solver = CGNR);
algo = IterativeRadonAlgorithm(IterativeRadonParameters(pre, reco))


@testset "Construction" begin
# From algorithm
plan_fromAlgo = toPlan(algo)

# With kwarg constructor
plan_fromKwargs = RecoPlan(IterativeRadonAlgorithm; parameter = RecoPlan(IterativeRadonParameters; pre = RecoPlan(RadonPreprocessingParameters; frames = collect(1:3)),
reco = RecoPlan(IterativeRadonReconstructionParameters; shape = size(images)[1:3], angles = angles, iterations = 1, reg = [L2Regularization(0.001), PositiveRegularization()], solver = CGNR)))

# Individually with setproperty!
plan_pre = RecoPlan(RadonPreprocessingParameters)
plan_pre.frames = collect(1:3)
@test build(plan_pre).frames == collect(1:3)
@test build(plan_pre) isa RadonPreprocessingParameters

plan_reco = RecoPlan(IterativeRadonReconstructionParameters)
plan_reco.shape = size(images)[1:3]
plan_reco.angles = angles
plan_reco.iterations = 1
plan_reco.reg = [L2Regularization(0.001), PositiveRegularization()]
plan_reco.solver = CGNR
@test build(plan_reco).solver == plan_reco.solver
@test build(plan_reco) isa IterativeRadonReconstructionParameters

plan_params = RecoPlan(IterativeRadonParameters)
plan_params.pre = plan_pre
plan_params.reco = plan_reco

plan_set = RecoPlan(IterativeRadonAlgorithm)
plan_set.parameter = plan_params

algo_1 = build(plan_fromAlgo)
algo_2 = build(plan_fromKwargs)
algo_3 = build(plan_set)
# Not the best, but the types dont define proper equals, so we use our default hash method
@test hash(algo_1.parameter.pre) == hash(algo_2.parameter.pre)
@test hash(algo_2.parameter.pre) == hash(algo_3.parameter.pre)
@test hash(algo_1.parameter.reco) == hash(algo_2.parameter.reco)
@test hash(algo_2.parameter.reco) == hash(algo_3.parameter.reco)
end

@testset "Properties" begin
# Test parameter with union property type
plan = RecoPlan(RadonFilteredBackprojectionParameters)
instance = nothing

@testset "Setter/Getter" begin
# Init missing
@test ismissing(plan.angles)
@test ismissing(plan.filter)

# Set/get
plan.angles = angles
@test plan.angles == angles
@test_throws Exception plan.doesntExist = 42

# Type checking
plan.filter = nothing
@test isnothing(plan.filter)
@test_throws Exception plan.filter = "Test"
@test isnothing(plan.filter)
plan.filter = missing
@test ismissing(plan.filter)
plan.filter = [0.2]
@test plan.filter == [0.2]

# Clearing
clear!(plan)
@test ismissing(plan.angles)
@test ismissing(plan.filter)

# Used during construction
plan.angles = angles
instance = build(plan)
@test instance.angles == angles
@test isnothing(instance.filter) # Default kwarg
end

outer = RecoPlan(DirectRadonParameters)
incorrect = RecoPlan(RadonPreprocessingParameters)
@testset "Nested Plans" begin
# Type checking
@test_throws Exception outer.reco = incorrect
outer.reco = instance
@test outer.reco == instance
outer.reco = plan
@test outer.reco == plan
# Clearing
plan.angles = angles
@test !ismissing(outer.reco.angles)
clear!(outer)
@test ismissing(outer.reco.angles)
clear!(outer, false)
@test ismissing(outer.reco)
end

@testset "SetAll!" begin
# setAll! variants
clear!(plan)
# Kwargs
setAll!(plan; angles = angles, filter = nothing, doesntExist = 42)
@test plan.angles == angles
@test isnothing(plan.filter)
clear!(plan)
# Dict{Symbol}
setAll!(plan, Dict{Symbol, Any}(:angles => angles, :filter => nothing, :doesntExist => 42))
@test plan.angles == angles
@test isnothing(plan.filter)
clear!(plan)
# Dict{String}
setAll!(plan, Dict{String, Any}("angles" => angles, "filter" => nothing, "doesntExist" => 42))
@test plan.angles == angles
@test isnothing(plan.filter)
clear!(plan)
# Nested plan
outer.reco = plan
setAll!(plan; angles = angles, filter = nothing)
@test plan.angles == angles
@test isnothing(plan.filter)
clear!(plan)
end

@testset "Property names" begin
# Property names and filtering
struct TestParameters <: AbstractImageReconstructionParameters
a::Int64
b::Float64
_c::String
end
test = RecoPlan(TestParameters)
@test in(:a, collect(propertynames(test)))
@test in(:b, collect(propertynames(test)))
@test !in(:c, collect(propertynames(test)))
end
end

@testset "Observables" begin
plan = RecoPlan(RadonFilteredBackprojectionParameters)
observed = Ref{Bool}()
fun = (val) -> observed[] = true
on(fun, plan, :angles)
plan.angles = angles
@test observed[]
observed[] = false
try
plan.angles = "Test"
catch e
end
@test !(observed[])

off(plan, :angles, fun)
plan.angles = angles
@test !(observed[])

obsv = plan[:angles]
@test obsv isa Observable

on(fun, plan, :angles)
clear!(plan)
plan.angles = angles
@test !(observed[])
end

@testset "Traversal" begin
end

end
11 changes: 10 additions & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
using AbstractImageReconstruction
using AbstractImageReconstruction.Observables
using Test
using RegularizedLeastSquares

include(joinpath(@__DIR__(), "..", "docs", "src", "literate", "example", "example_include_all.jl"))

@testset "AbstractImageReconstruction.jl" begin
# Write your tests here.
include("algorithm_api.jl")
include("reco_plan.jl")
include("struct_transforms.jl")
include("serialization.jl")
include("linkedproperty.jl")
include("caching.jl")
end
13 changes: 13 additions & 0 deletions test/serialization.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@testset "Serialization" begin
pre = RadonPreprocessingParameters(frames = collect(1:3))
filter_reco = RadonFilteredBackprojectionParameters(;angles)
algo = DirectRadonAlgorithm(DirectRadonParameters(pre, filter_reco))

plan = toPlan(algo)

io = IOBuffer()
savePlan(io, plan)
seekstart(io)
plan_copy = loadPlan(io, [Main, AbstractImageReconstruction, RegularizedLeastSquares, OurRadonReco])
@test hash(parameter(build(plan))) == hash(parameter(build(plan_copy)))
end
Empty file added test/struct_transforms.jl
Empty file.

0 comments on commit 0ee8069

Please sign in to comment.