diff --git a/Project.toml b/Project.toml index 441a9dd..63f13ed 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "DaggerImageReconstruction" uuid = "b99085f1-2f43-45a7-bd38-d511f4bff3b1" authors = ["nHackel and contributors"] -version = "0.1.2" +version = "0.2.0" [deps] AbstractImageReconstruction = "a4b4fdbf-6459-4ec9-990d-77e1fa24a91b" @@ -9,7 +9,7 @@ Dagger = "d58978e5-989f-55fb-8d15-ea34adc7bf54" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" [compat] -AbstractImageReconstruction = "0.4, 0.5" +AbstractImageReconstruction = "0.6" Dagger = "0.18, 0.19" Distributed = "1" julia = "1.10" diff --git a/docs/make.jl b/docs/make.jl index e5254f9..f1618c4 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -31,6 +31,7 @@ makedocs( "Introduction" => "example_intro.md", "Algoritm Interface" => "generated/example/algorithm.md", "RecoPlan Interface" => "generated/example/daggerplan.md", + "Parameter Interface" => "generated/example/utility.md" ], "API Reference" => "API/api.md", diff --git a/docs/src/API/api.md b/docs/src/API/api.md index 9c39f0f..320d082 100644 --- a/docs/src/API/api.md +++ b/docs/src/API/api.md @@ -6,6 +6,7 @@ REPL one can access this documentation by entering the help mode with `?` ```@docs DaggerImageReconstruction.DaggerReconstructionAlgorithm DaggerImageReconstruction.DaggerReconstructionParameter +DaggerImageReconstruction.DaggerReconstructionUtility ``` ## DaggerRecoPlan diff --git a/docs/src/literate/example/0_radon_data.jl b/docs/src/literate/example/0_radon_data.jl index d267c7f..0c7d979 100644 --- a/docs/src/literate/example/0_radon_data.jl +++ b/docs/src/literate/example/0_radon_data.jl @@ -3,7 +3,7 @@ # In this example we will set up our radon data using RadonKA.jl, ImagePhantoms.jl and ImageGeoms.jl. We will start with simple 2D images and their sinograms and continue with a time series of 3D images and sinograms. # ## Background -# The Radon transform is an integral transform that projects the values of a function(or a phantom) along straight lines onto a detector. +# The Radon transform is an integral transform that projects the values of a function (or a phantom) along straight lines onto a detector. # These projections are recorded for a number of different angles and form the so-called sinogram. The Radon transform and its adjoint form the mathematical basis # for computed tomography (CT) and other imaging modalities such as single photon emission computed tomography (SPECT) and positron emission tomography (PET). @@ -18,7 +18,7 @@ size(image) # This produces a 256x256 image of a Shepp-Logan phantom. Next, we will generate the Radon data using `radon` from RadonKA.jl. # The arguments of this function are the image or phantom to be transformed, the angles at which the projections are taken, and the used geometry of the system. For this example we will use the default parallel circle geometry. -# For more details, we refer to the RadonKA.jl documentation. We will use 256 angles for the projections, between 0 and π. +# For more details, we refer to the RadonKA.jl documentation. We will use 256 angles for the projections between 0 and π. using RadonKA angles = collect(range(0, π, 256)) sinogram = Array(RadonKA.radon(image, angles)) @@ -39,8 +39,8 @@ plot_image(fig[1,2], sinogram, title = "Sinogram") resize_to_layout!(fig) fig -# ## 3D Pnantom -# RadonKA.jl also supports 3D Radon transforms. The first two dimensions are interpreted as the XY plane where the transform applied and the last dimensions is the rotational axis z of the projections. +# ## 3D Phantom +# RadonKA.jl also supports 3D Radon transforms. The first two dimensions are interpreted as the XY plane where the transform is applied and the last dimension is the rotational axis z of the projections. # For that we need to create a 3D Shepp-Logan phantom. First we retrieve the parameters of the ellipsoids of the Shepp-Logan phantom: shape = (64, 64, 64) params = map(collect, ellipsoid_parameters(; fovs = shape)); @@ -74,7 +74,7 @@ fig # ## Time Series of 3D Phantoms -# Lastly, we want to add a time dimension to our 3D phantom. For our example we will increase the intensity of the third ellipsoid every time step or frame. +# Lastly, we want to add a time dimension to our 3D phantom. For our example, we will increase the intensity of the third ellipsoid every time step or frame. images = similar(image, size(image)..., 5) sinograms = similar(sinogram, size(sinogram)..., 5) for (i, intensity) in enumerate(range(params[3][end], 0.3, 5)) diff --git a/docs/src/literate/example/1_interface.jl b/docs/src/literate/example/1_interface.jl index da0c298..ac9cd7c 100644 --- a/docs/src/literate/example/1_interface.jl +++ b/docs/src/literate/example/1_interface.jl @@ -26,24 +26,38 @@ abstract type AbstractDirectRadonAlgorithm <: AbstractRadonAlgorithm end abstract type AbstractIterativeRadonAlgorithm <: AbstractRadonAlgorithm end # ## Internal Interface -# Reconstruction algorithms in AbstractImageReconstruction.jl are expected to be implemented in the form of distinct processing steps, implemented in their own `process` methods. -# The `process` function takes an algorithm, parameters, and inputs and returns the result of the processing step. -# If no function is defined for an instance of an algorithm, the default implementation is called. This method tries to call the function `process` with the type of the algorithm: +# In AbstractImageReconstruction.jl, reconstruction algorithms are driven by *parameters*. Parameters are callable objects that implement individual processing steps. +# A parameter type `MyParams` is expected to implement one of: # ```julia -# process(algo::AbstractImageReconstructionAlgorithm, param::AbstractImageReconstructionParameters, inputs...) = process(typeof(algo), param, inputs...) +# (param::MyParams)(::Type{<:MyAlgorithm}, inputs...) +# (param::MyParams)(algo::MyAlgorithm, inputs...) # ``` -# The implementation of reconstruction algorithms is therefore expected to either implement the `process` function for the algorithm type or for the instance. Dispatch on instances allow an instance to change its state, while dispatch on types allows for pure helper functions. +# The type-based variant is preferred for pure functions; the instance-based variant allows mutation of the algorithm state. The default implementation of +# ```julia +# (param::AbstractImageReconstructionParameters)(algo::AbstractImageReconstructionAlgorithm, inputs...) = param(algo, inputs...) → param(typeof(algo), inputs...) +# ``` +# simply forwards to the type-based method. + +# A reconstruction algorithm typically stores a *main* parameter. Multiple processing steps can be encoded by +# composing parameter calls; there is no requirement to implement a strict linear pipeline. -# A `process` itself can invoke other `process` functions to enable multiple processing steps and generally have arbitarry control flow. It is not required to implement a straight-foward pipeline. We will see this later when we implementd our algorithms. +# To extend an existing algorithm with new behavior, it's enough to implement new parameters or potentially add an algorithm. +# Later on, we will see more infrastructure of the package which focuses on parameters and their Configuration. -# Let's define a preprocessing step that we can share between our algorithms. We want to allow the user to select certain frames from a time series and average them. -# We will use the `@kwdef` macro to provide constructor with keyword arguments and default values +# Let's define a preprocessing step that we can share between our algorithms. We want to +# allow the user to select certain frames from a time series and average them. We will use +# the `@parameter` macro. This is similar to `Base.@kwdef` and allows us to provide a constructor with keyword arguments and default values. +# It also allows us to validate the values of our parameters: using Statistics -Base.@kwdef struct RadonPreprocessingParameters <: AbstractRadonPreprocessingParameters +@parameter struct RadonPreprocessingParameters <: AbstractRadonPreprocessingParameters frames::Vector{Int64} = [] numAverages::Int64 = 1 + + @validate begin + @assert numAverages >= 0 "Averages must be a positive integer" + end end -function AbstractImageReconstruction.process(::Type{<:AbstractRadonAlgorithm}, params::RadonPreprocessingParameters, data::AbstractArray{T, 4}) where {T} +function (params::RadonPreprocessingParameters)(::Type{<:AbstractRadonAlgorithm}, data::AbstractArray{T, 4}) where {T} frames = isempty(params.frames) ? (1:size(data, 4)) : params.frames data = data[:, :, :, frames] @@ -57,4 +71,5 @@ end # ## User Interface # A user of our package should be able to reconstruct images by calling the `reconstruct` function. This function takes an algorithm and an input and returns the reconstructed image. -# Internally, the `reconstruct` function calls the `put!` and `take!` functions of the algorithm to pass the input and retrieve the output. Algorithms must implement these functions and are expected to have FIFO behavior. \ No newline at end of file +# Internally, the `reconstruct` function calls the `put!` and `take!` functions of the algorithm to pass the input and retrieve the output. Algorithms must implement these functions and are expected to have FIFO behavior. +# However, much of this boilerplate can be created via macros, as we will see in this example. \ No newline at end of file diff --git a/docs/src/literate/example/2_direct.jl b/docs/src/literate/example/2_direct.jl index a3fb458..1e94918 100644 --- a/docs/src/literate/example/2_direct.jl +++ b/docs/src/literate/example/2_direct.jl @@ -6,67 +6,51 @@ export AbstractDirectRadonReconstructionParameters, RadonFilteredBackprojectionP # To implement our direct reconstruction algorithms we need to define a few more methods and types. We will start by defining the parameters for the backprojection and for the filtered backprojection. Afterwards we can implement the algorithm itself. # ## Parameters and Processing -# For convenience we first introduce a new abstract type for the direct reconstruction paramters: +# For convenience we first introduce a new abstract type for the direct reconstruction parameters: abstract type AbstractDirectRadonReconstructionParameters <: AbstractRadonReconstructionParameters end # The backprojection parameters are simple and only contain the number of angles: -Base.@kwdef struct RadonBackprojectionParameters <: AbstractDirectRadonReconstructionParameters +@parameter struct RadonBackprojectionParameters <: AbstractDirectRadonReconstructionParameters angles::Vector{Float64} end # The filtered backprojection parameters are more complex and contain the number of angles and optionally the filter which should be used: -Base.@kwdef struct RadonFilteredBackprojectionParameters <: AbstractDirectRadonReconstructionParameters +@parameter struct RadonFilteredBackprojectionParameters <: AbstractDirectRadonReconstructionParameters angles::Vector{Float64} filter::Union{Nothing, Vector{Float64}} = nothing end # Since we have defined no default values for the angles, they are required to be set by the user. A more advanced implementation would also allow for the geometry to be set. # Next we will implement the process steps for both of our backprojection variants. Since RadonKA.jl expects 2D or 3D arrays we have to transform our time series accordingly. -function AbstractImageReconstruction.process(algoT::Type{<:AbstractDirectRadonAlgorithm}, params::AbstractDirectRadonReconstructionParameters, data::AbstractArray{T, 4}) where {T} +function (params::AbstractDirectRadonReconstructionParameters)(algoT::Type{<:AbstractDirectRadonAlgorithm}, data::AbstractArray{T, 4}) where {T} result = [] for i = 1:size(data, 4) - push!(result, process(algoT, params, view(data, :, :, :, i))) + push!(result, params(algoT, view(data, :, :, :, i))) end return cat(result..., dims = 4) end -AbstractImageReconstruction.process(::Type{<:AbstractDirectRadonAlgorithm}, params::RadonBackprojectionParameters, data::AbstractArray{T, 3}) where {T} = RadonKA.backproject(data, params.angles) -AbstractImageReconstruction.process(::Type{<:AbstractDirectRadonAlgorithm}, params::RadonFilteredBackprojectionParameters, data::AbstractArray{T, 3}) where {T} = RadonKA.backproject_filtered(data, params.angles; filter = params.filter) +(params::RadonBackprojectionParameters)(::Type{<:AbstractDirectRadonAlgorithm}, data::AbstractArray{T, 3}) where {T} = RadonKA.backproject(data, params.angles) +(params::RadonFilteredBackprojectionParameters)(::Type{<:AbstractDirectRadonAlgorithm}, data::AbstractArray{T, 3}) where {T} = RadonKA.backproject_filtered(data, params.angles; filter = params.filter) # ## Algorithm # The direct reconstruction algorithm has essentially no state to store between reconstructions and thus only needs its parameters as fields. We want our algorithm to accept any combination of our preprocessing and direct reconstruction parameters. # This we encode in a new type: -Base.@kwdef struct DirectRadonParameters{P <: AbstractRadonPreprocessingParameters, R <: AbstractDirectRadonReconstructionParameters} <: AbstractRadonParameters +@chain struct DirectRadonParameters{P <: AbstractRadonPreprocessingParameters, R <: AbstractDirectRadonReconstructionParameters} <: AbstractRadonParameters pre::P reco::R end -# And the according processing step: -function AbstractImageReconstruction.process(algoT::Type{<:AbstractDirectRadonAlgorithm}, params::DirectRadonParameters{P, R}, data::AbstractArray{T, 4}) where {T, P<:AbstractRadonPreprocessingParameters, R<:AbstractDirectRadonReconstructionParameters} - data = process(algoT, params.pre, data) - return process(algoT, params.reco, data) -end +# This composite type simply chains the result of our preprocessing step into our reco step. See the API for @chain for more information. -# Now we can define the algorithm type itself. Algorithms are usually constructed with one argument passing in the user parameters: -mutable struct DirectRadonAlgorithm{D <: DirectRadonParameters} <: AbstractDirectRadonAlgorithm - parameter::D - output::Channel{Any} - DirectRadonAlgorithm(parameter::D) where D = new{D}(parameter, Channel{Any}(Inf)) -end -# And they implement a method to retrieve the used parameters: -AbstractImageReconstruction.parameter(algo::DirectRadonAlgorithm) = algo.parameter +# Now we can define the algorithm type itself using the `@reconstruction` macro. The macro automatically generates: -# Algorithms are assumed to be stateful. To ensure thread safety, we need to implement the `lock` and `unlock` functions. We will use the `output` channel as a lock: -Base.lock(algo::DirectRadonAlgorithm) = lock(algo.output) -Base.unlock(algo::DirectRadonAlgorithm) = unlock(algo.output) +# - A mutable struct with the parameter field and internal infrastructure +# - A constructor accepting the parameter +# - Interface methods: `Base.put!`, `Base.take!`, `Base.wait`, `Base.isready`, `Base.lock`, `Base.unlock` +# - A parameter accessor method -# And implement the `put!` and `take!` functions, mimicking the behavior of a FIFO channel for reconstructions: -Base.take!(algo::DirectRadonAlgorithm) = Base.take!(algo.output) -function Base.put!(algo::DirectRadonAlgorithm, data::AbstractArray{T, 4}) where {T} - lock(algo) do - put!(algo.output, process(algo, algo.parameter, data)) - end -end +# It's also possible to implement these functions manually, see the corresponding docstrings for information -# The way the behaviour is implemented here, the algorithm does not buffer any inputs and instead blocks until the currenct reconstruction is done. Outputs are stored until they are retrieved. +@reconstruction struct DirectRadonAlgorithm{D <: DirectRadonParameters} <: AbstractDirectRadonAlgorithm + @parameter parameter::D +end -# With `wait` and `isready` we can check if the algorithm is currently processing data or if it is ready to accept new inputs: -Base.wait(algo::DirectRadonAlgorithm) = wait(algo.output) -Base.isready(algo::DirectRadonAlgorithm) = isready(algo.output) \ No newline at end of file +# The way the behaviour is implemented here, the algorithm does not buffer any inputs and instead blocks until the current reconstruction is done. Outputs are stored until they are retrieved. \ No newline at end of file diff --git a/docs/src/literate/example/4_iterative.jl b/docs/src/literate/example/4_iterative.jl index d52df96..ef975cf 100644 --- a/docs/src/literate/example/4_iterative.jl +++ b/docs/src/literate/example/4_iterative.jl @@ -13,17 +13,18 @@ export AbstractIterativeRadonReconstructionParameters, IterativeRadonReconstruct # We will start by defining the parameters for the algorithm and the processing steps. Afterwards we can implement the algorithm itself. Since we will use the same preprocessing as for the direct reconstruction, we can reuse the parameters and processing steps and jump directly to the iterative parameters: using RegularizedLeastSquares, LinearOperatorCollection abstract type AbstractIterativeRadonReconstructionParameters <: AbstractRadonReconstructionParameters end -Base.@kwdef struct IterativeRadonReconstructionParameters{S <: AbstractLinearSolver, R <: AbstractRegularization, N} <: AbstractIterativeRadonReconstructionParameters +@parameter struct IterativeRadonReconstructionParameters{T, S <: AbstractLinearSolver, R <: AbstractRegularization, N} <: AbstractIterativeRadonReconstructionParameters + eltype::Type{T} = Float64 solver::Type{S} - iterations::Int64 + iterations::Int64 reg::Vector{R} - shape::NTuple{N, Int64} + shape::NTuple{N, Int64} angles::Vector{Float64} end -# The parameters of this struct can be grouped into three catergories. The solver type just specifies which solver to use. The number of iterations and the regularization term could be abstracted into a nested `AbstractRadonParameter` which describe the parameters for the solver. Lastly the shape and angles are required to construct the linear operator. +# The parameters of this struct can be grouped into three categories. The solver type just specifies which solver to use. The number of iterations and the regularization term could be abstracted into a nested `AbstractRadonParameter` which describe the parameters for the solver. Lastly the shape and angles are required to construct the linear operator. # Since we want to construct the linear operator only once, we will write the `process` method with the operator as a given argument: -function AbstractImageReconstruction.process(::Type{<:AbstractIterativeRadonAlgorithm}, params::IterativeRadonReconstructionParameters, op, data::AbstractArray{T, 4}) where {T} +function (params::IterativeRadonReconstructionParameters{T})(::Type{<:AbstractIterativeRadonAlgorithm}, op, data::AbstractArray{T, 4}) where {T} solver = createLinearSolver(params.solver, op; iterations = params.iterations, reg = params.reg) result = similar(data, params.shape..., size(data, 4)) @@ -39,49 +40,29 @@ end # ## Algorithm # Similar to the direct reconstruction algorithm, we want our iterative algorithm to accept both preprocessing and reconstruction parameters. We will encode this in a new type: -Base.@kwdef struct IterativeRadonParameters{P<:AbstractRadonPreprocessingParameters, R<:AbstractIterativeRadonReconstructionParameters} <: AbstractRadonParameters +@parameter struct IterativeRadonParameters{P<:AbstractRadonPreprocessingParameters, R<:AbstractIterativeRadonReconstructionParameters} <: AbstractRadonParameters pre::P reco::R end # Instead of defining essentially the same struct again, we could also define a more generic one and specify the supported reconstruction parameter as type constraints in the algorithm constructor. -# Unlike the direct reconstruction algorithm, the iterative algorithm has to store the linear operator. We will store it as a field in the algorithm type: -mutable struct IterativeRadonAlgorithm{D <: IterativeRadonParameters} <: AbstractIterativeRadonAlgorithm - parameter::D - op::Union{Nothing, AbstractLinearOperator} - output::Channel{Any} -end - -# We will set the operator to `nothing` in the constructor: -function IterativeRadonAlgorithm(parameter::D) where D - return IterativeRadonAlgorithm{D}(parameter, nothing, Channel{Any}(Inf)) -end +# Unlike the direct reconstruction algorithm, the iterative algorithm has to store the linear operator. We will store it as a field in the algorithm type in an initialization step: +@reconstruction mutable struct IterativeRadonAlgorithm{D <: IterativeRadonParameters} <: AbstractIterativeRadonAlgorithm + @parameter parameter::D + op::Union{Nothing, AbstractLinearOperator} = nothing -# Next we implement the `process` method for our reconstruction parameters and an algorithm instance. This allows us to access the operator and pass it to the processing step: -function AbstractImageReconstruction.process(algo::IterativeRadonAlgorithm, params::IterativeRadonParameters{P, R}, data::AbstractArray{T, 4}) where {T, P<:AbstractRadonPreprocessingParameters, R<:AbstractIterativeRadonReconstructionParameters} - data = process(algo, params.pre, data) - return process(algo, params.reco, algo.op, data) + @init function createOperator(algo) + params = algo.parameter.reco + algo.op = RadonOp(params.eltype; shape = params.shape, angles = params.angles) + end end +# If our algorithms require more complex initial state or parametric types, we can also create custom constructors. See the API for more information. -# Note that initially the operator is `nothing` and the processing step would fail as it stands. To "fix" this we define a `process` method for the algorithm instance which creates the operator and stores it in the algorithm: -function AbstractImageReconstruction.process(algo::IterativeRadonAlgorithm, params::AbstractIterativeRadonReconstructionParameters, ::Nothing, data::AbstractArray{T, 4}) where {T} - op = RadonOp(T; shape = params.shape, angles = params.angles) - algo.op = op - return process(AbstractIterativeRadonAlgorithm, params, op, data) +# Next we implement the our manual chain method for our reconstruction parameters and an algorithm instance. This allows us to access the operator and pass it to the reconstruction step: +function (params::IterativeRadonParameters{P, R})(algo::IterativeRadonAlgorithm, data::AbstractArray{T, 4}) where {T, P<:AbstractRadonPreprocessingParameters, R<:AbstractIterativeRadonReconstructionParameters} + data = params.pre(algo, data) + return params.reco(algo, algo.op, data) end # Our algorithm is not type stable. To fix this, we would need to know the element type of the sinograms during construction. Which is possible with a different parameterization of the algorithm. We will not do this here. -# Often times the performance impact of this is negligible as the critical sections are in the preprocessing or the iterative solver, especially since we still dispatch on the operator. - -# To finish up the implementation we need to implement the remaining runtime related functions: -Base.take!(algo::IterativeRadonAlgorithm) = Base.take!(algo.output) -function Base.put!(algo::IterativeRadonAlgorithm, data::AbstractArray{T, 4}) where {T} - lock(algo.output) do - put!(algo.output, process(algo, algo.parameter, data)) - end -end -Base.lock(algo::IterativeRadonAlgorithm) = lock(algo.output) -Base.unlock(algo::IterativeRadonAlgorithm) = unlock(algo.output) -Base.isready(algo::IterativeRadonAlgorithm) = isready(algo.output) -Base.wait(algo::IterativeRadonAlgorithm) = wait(algo.output) -AbstractImageReconstruction.parameter(algo::IterativeRadonAlgorithm) = algo.parameter \ No newline at end of file +# Often times the performance impact of this is negligible as the critical sections are in the preprocessing or the iterative solver, especially since we still dispatch on the operator. \ No newline at end of file diff --git a/docs/src/literate/example/daggerplan.jl b/docs/src/literate/example/daggerplan.jl index 613b6e2..641870c 100644 --- a/docs/src/literate/example/daggerplan.jl +++ b/docs/src/literate/example/daggerplan.jl @@ -56,12 +56,16 @@ fig # ## Serialization # The serialization process of `DaggerReconstructionAlgorithm` and `DaggerReconstructionParameter` ignores the worker parameter and retrieves the entire plan tree: -toTOML(stdout, plan_dagger) +setAll!(plan_dagger, :reg, missing) +setAll!(plan_dagger, :angles, missing) +savePlan(stdout, plan_dagger) -# It is also possible to directly load and distribute a serialized plan from a file using: -# ```julia -# loadDaggerPlan(filename, modules; worker = worker) -# ``` +# It is also possible to directly load and distribute a serialized plan from a file using `loadDaggerPlan`: +clear!(plan_iter) +io = IOBuffer() +savePlan(io, plan_iter) +seekstart(io) +loadDaggerPlan(io, [OurRadonReco]; worker = worker) # This automatically wraps everything in a `DaggerReconstructionAlgorithm`. # ## Observables @@ -70,7 +74,7 @@ toTOML(stdout, plan_dagger) # This functionality also applies to the `loadDaggerPlan` method mentioned earlier. # Additionally, listeners can be attached across workers using the Observable interface on a `DaggerRecoPlan`: -using Observables +using AbstractImageReconstruction.Observables localVariable = 3 plan_iter_remote = plan_dagger.parameter.algo fun = on(plan_iter_remote.parameter.pre, :frames) do newval diff --git a/docs/src/literate/example/utility.jl b/docs/src/literate/example/utility.jl new file mode 100644 index 0000000..29ede44 --- /dev/null +++ b/docs/src/literate/example/utility.jl @@ -0,0 +1,50 @@ +worker = 1 # hide +using DaggerImageReconstruction # hide +include("../../literate/example/example_include_all.jl") #hide + +# # Distributed Image Reconstruction using DaggerReconstructionUtility +# This example demonstrates how to use `DaggerReconstructionUtility` to wrap reconstruction parameters and execute them on a separate worker process. + +# ## Parameter Wrapping +# The `DaggerReconstructionUtility` wraps parameters (not the entire algorithm), allowing the wrapped parameters to execute on a specified worker. +# We begin with the preprocessing parameters we defined earlier: + +pre = RadonPreprocessingParameters(frames = collect(1:3)) + +# To distribute these parameters to a worker, we wrap them using `DaggerReconstructionUtility`: + +pre_dagger = DaggerReconstructionUtility(pre, worker) + +# The `DaggerReconstructionUtility` takes the parameters and a worker ID. Internally, it creates a `Dagger.Chunk` of the parameters and schedules execution on the specified worker. +# The `DirectRadonParameters` defined in the example don't accept utility parameters just yet. As a workaround we define a new preprocessing steps, however usually it would be better to do this directly in the type: +@parameter struct RadonPreprocessingWithUtilityParameters{P <: AbstractRadonPreprocessingParameters, PU <: AbstractUtilityReconstructionParameters{P}} <: AbstractRadonPreprocessingParameters + params::Union{P, PU} +end +(params::RadonPreprocessingWithUtilityParameters)(algoT::Type{<:AbstractRadonAlgorithm}, args...) = params.params(algoT, args...) +# This is a similar artifical case to the caching example from AbstractImageReconstruction. + +# ## Algorithm Construction with Distributed Parameters +# We can now construct our reconstruction algorithm using the distributed parameters. We will use the direct reconstruction algorithm for this example: +pre_wrapped = RadonPreprocessingWithUtilityParameters(pre_dagger) +algo_direct = DirectRadonAlgorithm(DirectRadonParameters(pre = pre_wrapped, reco = RadonBackprojectionParameters(angles))); + +# Note that we can mix local and distributed parameters. Only the parameters wrapped with `DaggerReconstructionUtility` will execute on the worker. + +# ## Reconstruction +# We can now reconstruct our sinograms. The reconstruction will be executed on the worker, but the result will be returned to the main process: + +imag_direct = reconstruct(algo_direct, sinograms) + +# ## RecoPlan Interface +# The `DaggerReconstructionUtility` also works with the `RecoPlan` interface, allowing for distributed configuration of parameters. +# We can create a `RecoPlan` for our distributed preprocessing parameters: +plan = toPlan(algo_direct) + +# We can now configure the parameters Distributedly using the `RecoPlan` interface: + +plan.parameter.pre.params.param.frames = collect(1:3) + +# The configuration changes will be executed on the worker, not locally. + +# The `DaggerReconstructionUtility` provides a simple and flexible way to distribute reconstruction parameters without the overhead of distributing entire algorithms. +# However, it requires parameters to accept utility parameters diff --git a/src/DaggerImageReconstruction.jl b/src/DaggerImageReconstruction.jl index bb9e9cb..903ca71 100644 --- a/src/DaggerImageReconstruction.jl +++ b/src/DaggerImageReconstruction.jl @@ -5,14 +5,15 @@ using Dagger using AbstractImageReconstruction using AbstractImageReconstruction.AbstractTrees using AbstractImageReconstruction.Observables +using AbstractImageReconstruction.StructUtils -import AbstractImageReconstruction: process, build, toDictValue!, showtree, showproperty, INDENT, PIPE, TEE, ELBOW +import AbstractImageReconstruction: @reconstruction, @parameter, RecoPlanStyle, build, showtree, showproperty, INDENT, PIPE, TEE, ELBOW abstract type AbstractDaggerReconstructionAlgorithm{A} <: AbstractImageReconstructionAlgorithm end include("Utils.jl") include("DaggerRecoPlan.jl") include("DaggerReconstructionAlgorithm.jl") -include("DaggerReconstructionProcess.jl") +include("DaggerReconstructionUtility.jl") end \ No newline at end of file diff --git a/src/DaggerReconstructionAlgorithm.jl b/src/DaggerReconstructionAlgorithm.jl index 1eeccfd..42f974f 100644 --- a/src/DaggerReconstructionAlgorithm.jl +++ b/src/DaggerReconstructionAlgorithm.jl @@ -27,24 +27,10 @@ end Struct representing a Dagger-based reconstruction algorithm, which encapsulates the distrubted reconstruction execution and manages the outputs. """ -mutable struct DaggerReconstructionAlgorithm{T} <: AbstractDaggerReconstructionAlgorithm{T} - parameter::DaggerReconstructionParameter{T} - output::Channel{Any} +@reconstruction struct DaggerReconstructionAlgorithm{T} <: AbstractDaggerReconstructionAlgorithm{T} + @parameter parameter::DaggerReconstructionParameter{T} end -DaggerReconstructionAlgorithm(param::DaggerReconstructionParameter) = DaggerReconstructionAlgorithm(param, Channel{Any}(Inf)) -AbstractImageReconstruction.parameter(algo::DaggerReconstructionAlgorithm) = algo.parameter -Base.lock(algo::DaggerReconstructionAlgorithm) = lock(algo.output) -Base.unlock(algo::DaggerReconstructionAlgorithm) = unlock(algo.output) -Base.take!(algo::DaggerReconstructionAlgorithm) = Base.take!(algo.output) -function Base.put!(algo::DaggerReconstructionAlgorithm, data) - lock(algo) do - put!(algo.output, process(algo, algo.parameter, data)) - end -end -Base.wait(algo::DaggerReconstructionAlgorithm) = wait(algo.output) -Base.isready(algo::DaggerReconstructionAlgorithm) = isready(algo.output) - -function AbstractImageReconstruction.process(algo::DaggerReconstructionAlgorithm, params::DaggerReconstructionParameter, data) +function (params::DaggerReconstructionParameter)(algo::DaggerReconstructionAlgorithm, data) result = fetch(Dagger.spawn(params.algo) do algo reconstruct(algo, data) end @@ -81,7 +67,14 @@ function AbstractImageReconstruction.build(plan::RecoPlan{DaggerReconstructionPa end # Do not serialize the the worker and collect the remote algo -AbstractImageReconstruction.toDictValue!(dict, plan::RecoPlan{DaggerReconstructionParameter}) = dict["algo"] = fetch(Dagger.@spawn toDict(getchunk(plan, :algo))) +function StructUtils.lower(style::RecoPlanStyle, plan::RecoPlan{T}) where T <: DaggerReconstructionParameter + dict = Dict{String, Any}( + MODULE_TAG => string(parentmodule(T)), + TYPE_TAG => "RecoPlan{$(nameof(T))}" + ) + dict["algo"] = fetch(Dagger.@spawn StructUtils.lower(style, getchunk(plan, :algo))) + return dict +end function AbstractImageReconstruction.showtree(io::IO, property::RecoPlan{DaggerReconstructionParameter}, indent::String, depth::Int) if !ismissing(property.algo) @@ -108,13 +101,13 @@ function AbstractImageReconstruction.clear!(plan::RecoPlan{DaggerReconstructionP end # First load the plan in the current worker, then make it chunk for the current worker. Afterwards with setproperty! one can move the chunk to another process -function AbstractImageReconstruction.loadPlan!(plan::RecoPlan{DaggerReconstructionParameter}, dict::Dict{String, Any}, modDict) +function StructUtils.make!(style::RecoPlanStyle, plan::RecoPlan{DaggerReconstructionParameter}, dict::Dict{String, Any}) algo = missing if haskey(dict, "algo") - algo = AbstractImageReconstruction.loadPlan!(dict["algo"], modDict) + algo, _ = StructUtils.make(style, RecoPlan, dict["algo"]) parent!(algo, plan) end - setchunk(plan, :algo, Dagger.@mutable worker = myid() algo) + setchunk!(plan, :algo, Dagger.@mutable worker = myid() algo) plan.worker = myid() return plan end @@ -161,7 +154,8 @@ function Base.setproperty!(plan::RecoPlan{DaggerReconstructionParameter}, name:: end getfield(plan, :values)[name][] = value elseif name == :algo - setchunk!(plan, :algo, Dagger.@mutable worker = plan.worker value) + worker = ismissing(plan.worker) ? myid() : plan.worker + setchunk!(plan, :algo, Dagger.@mutable worker = worker value) end return Base.getproperty(plan, name) diff --git a/src/DaggerReconstructionProcess.jl b/src/DaggerReconstructionProcess.jl deleted file mode 100644 index f9388df..0000000 --- a/src/DaggerReconstructionProcess.jl +++ /dev/null @@ -1,111 +0,0 @@ -export DaggerReconstructionProcess -struct DaggerReconstructionProcess{P, T <: Union{P, AbstractUtilityReconstructionParameters{P}}, C <: Dagger.Chunk{T}} <: AbstractUtilityReconstructionParameters{P} - param::C - worker::Int64 - function DaggerReconstructionProcess(params::Union{P, AbstractUtilityReconstructionParameters{P}}, worker::Int64) where P <: AbstractImageReconstructionParameters - chunk = Dagger.@mutable worker = worker params - return new{P, typeof(params), typeof(chunk)}(chunk, worker) - end - # TODO arbitrary scope -end -DaggerReconstructionProcess(; params, worker = myid()) = DaggerReconstructionProcess(params, worker) - -AbstractImageReconstruction.process(algo::A, param::DaggerReconstructionProcess, inputs...) where {A <: AbstractImageReconstructionAlgorithm} = dagger_process(algo, param, inputs...) -AbstractImageReconstruction.process(algoT::Type{<:A}, param::DaggerReconstructionProcess, inputs...) where {A <: AbstractImageReconstructionAlgorithm} = dagger_process(algoT, param, inputs...) -function dagger_process(algo, param::DaggerReconstructionProcess, inputs...) - return fetch(Dagger.spawn(param.param) do p - return AbstractImageReconstruction.process(algo, p, inputs...) - end) -end - -function getchunk(plan::RecoPlan{DaggerReconstructionProcess}, name::Symbol) - if name != :param - error("$name is not a chunk of DaggerReconstructionProcess") - end - return getfield(plan, :values)[name][] -end -function setchunk!(plan::RecoPlan{DaggerReconstructionProcess}, name::Symbol, chunk) - if name != :param - error("$name is not a chunk of DaggerReconstructionProcess") - end - getfield(plan, :values)[name][] = chunk -end - -# Make distr. process transparent for property getter/setter -function Base.setproperty!(plan::RecoPlan{<:DaggerReconstructionProcess}, name::Symbol, value) - if in(name, [:param, :worker]) - t = type(plan, name) - value = missing - if AbstractImageReconstruction.validvalue(plan, t, x) - value = x - else - value = convert(t, x) - end - - if value isa RecoPlan - parent!(value, plan) - end - - if name == :worker - if !ismissing(getchunk(plan, :param)) - setchunk!(plan, :param, Dagger.@mutable worker = value collect(getchunk(plan, :param))) - end - getfield(plan, :values)[name][] = value - elseif name == :param - setchunk!(plan, :param, Dagger.@mutable worker = plan.worker value) - end - else - setproperty!(plan.param, name, value) - end - return Base.getproperty(plan, name) -end -AbstractImageReconstruction.validvalue(plan::RecoPlan{DaggerReconstructionProcess}, ::Type{<:Dagger.Chunk}, value::Union{AbstractImageReconstructionParameters, RecoPlan{<:AbstractImageReconstructionParameters}}) = true -AbstractImageReconstruction.validvalue(plan::RecoPlan{DaggerReconstructionProcess}, ::Type{<:Dagger.Chunk}, value::Missing) = true -AbstractImageReconstruction.validvalue(plan::RecoPlan{DaggerReconstructionProcess}, ::Type{<:Dagger.Chunk}, value) = false - -function Base.getproperty(plan::RecoPlan{<:DaggerReconstructionProcess}, name::Symbol) - if name == :param - chunk = getfield(plan, :values)[name][] - return ismissing(chunk) ? chunk : DaggerRecoPlan(chunk) - elseif name == :worker - return getfield(plan, :values)[name][] - else - return getproperty(plan.param, name) - end -end - - -# Do not serialize the the worker and collect the remote algo -AbstractImageReconstruction.toDictValue!(dict, plan::RecoPlan{DaggerReconstructionProcess}) = dict["param"] = fetch(Dagger.@spawn toDict(getchunk(plan, :param))) - -function AbstractImageReconstruction.toPlan(param::DaggerReconstructionProcess) - plan = RecoPlan(DaggerReconstructionProcess) - plan.worker = param.worker - setchunk!(plan, :param, Dagger.@mutable worker = param.worker fetch(Dagger.spawn(param.param) do tmp - toPlan(tmp) - end)) - return plan -end -# First load the plan in the current worker, then make it chunk for the current worker. Afterwards with setproperty! one can move the chunk to another process -function AbstractImageReconstruction.loadPlan!(plan::RecoPlan{DaggerReconstructionProcess}, dict::Dict{String, Any}, modDict) - param = missing - if haskey(dict, "param") - param = AbstractImageReconstruction.loadPlan!(dict["param"], modDict) - parent!(param, plan) - end - setchunk!(plan, :param, Dagger.@mutable worker = myid() param) - plan.worker = myid() - return plan -end -function AbstractImageReconstruction.build(plan::RecoPlan{DaggerReconstructionProcess}) - worker = plan.worker - param = getchunk(plan, :param) - if param isa Dagger.Chunk - param = Dagger.@mutable worker = worker fetch(Dagger.spawn(param) do tmp - build(tmp) - end) - else - error("Expected a Dagger.Chunk, found $(typeof(param))") - end - return DaggerReconstructionProcess(param, worker) -end diff --git a/src/DaggerReconstructionUtility.jl b/src/DaggerReconstructionUtility.jl new file mode 100644 index 0000000..a1b7c65 --- /dev/null +++ b/src/DaggerReconstructionUtility.jl @@ -0,0 +1,148 @@ +export DaggerReconstructionUtility + +""" + DaggerReconstructionUtility(; param, worker) + +Wraps reconstruction parameters for distributed execution using Dagger. + +`DaggerReconstructionUtility` takes parameters (or parameters wrapped in +`AbstractUtilityReconstructionParameters`) and executes them on a specified +worker process via `Dagger.spawn`. This is useful when parameters need to +execute on a specific worker for resource access (e.g., GPU) without the +overhead of distributing entire algorithms. + +See also: [`DaggerReconstructionAlgorithm`](@ref) +""" +@parameter struct DaggerReconstructionUtility{P, T <: Union{P, AbstractUtilityReconstructionParameters{P}}, C <: Dagger.Chunk{T}} <: AbstractUtilityReconstructionParameters{P} + param::C + worker::Int64 = myid() + # TODO arbitrary scope +end +function DaggerReconstructionUtility(params::Union{P, AbstractUtilityReconstructionParameters{P}}, worker::Int64) where P <: AbstractImageReconstructionParameters + chunk = Dagger.@mutable worker = worker params + return DaggerReconstructionUtility{P, typeof(params), typeof(chunk)}(chunk, worker) +end + + +(param::DaggerReconstructionUtility)(algo::A, inputs...) where {A <: AbstractImageReconstructionAlgorithm} = dagger_process(algo, param, inputs...) +(param::DaggerReconstructionUtility)(algoT::Type{<:A}, inputs...) where {A <: AbstractImageReconstructionAlgorithm} = dagger_process(algoT, param, inputs...) +function dagger_process(algo, param::DaggerReconstructionUtility, inputs...) + return fetch(Dagger.spawn(param.param) do p + return p(algo, inputs...) + end) +end + +function getchunk(plan::RecoPlan{DaggerReconstructionUtility}, name::Symbol) + if name != :param + error("$name is not a chunk of DaggerReconstructionUtility") + end + return getfield(plan, :values)[name][] +end +function setchunk!(plan::RecoPlan{DaggerReconstructionUtility}, name::Symbol, chunk) + if name != :param + error("$name is not a chunk of DaggerReconstructionUtility") + end + getfield(plan, :values)[name][] = chunk +end + +# Make distr. process transparent for property getter/setter +function Base.setproperty!(plan::RecoPlan{<:DaggerReconstructionUtility}, name::Symbol, x) + if in(name, [:param, :worker]) + t = type(plan, name) + value = missing + if AbstractImageReconstruction.validvalue(plan, t, x) + value = x + else + value = convert(t, x) + end + + if value isa RecoPlan + parent!(value, plan) + end + + if name == :worker + if !ismissing(getchunk(plan, :param)) + setchunk!(plan, :param, Dagger.@mutable worker = value collect(getchunk(plan, :param))) + end + getfield(plan, :values)[name][] = value + elseif name == :param + setchunk!(plan, :param, Dagger.@mutable worker = plan.worker value) + end + else + setproperty!(plan.param, name, value) + end + return Base.getproperty(plan, name) +end +AbstractImageReconstruction.validvalue(plan::RecoPlan{DaggerReconstructionUtility}, ::Type{<:Dagger.Chunk}, value::Union{AbstractImageReconstructionParameters, RecoPlan{<:AbstractImageReconstructionParameters}}) = true +AbstractImageReconstruction.validvalue(plan::RecoPlan{DaggerReconstructionUtility}, ::Type{<:Dagger.Chunk}, value::Missing) = true +AbstractImageReconstruction.validvalue(plan::RecoPlan{DaggerReconstructionUtility}, ::Type{<:Dagger.Chunk}, value) = false + +# TODO: requires support of nested utility in AbstractImageReconstruction +function AbstractImageReconstruction.validvalue(plan, union::Type{Union{T, DaggerReconstructionUtility{<:T}}}, value::RecoPlan{DaggerReconstructionUtility}) where T + innertype = value.param isa DaggerRecoPlan ? typeof(value.param).parameters[1] : typeof(value.param) + return DaggerReconstructionUtility{<:innertype} <: union +end + +function AbstractImageReconstruction.validvalue(plan, union::UnionAll, value::RecoPlan{DaggerReconstructionUtility}) + innertype = value.param isa DaggerRecoPlan ? typeof(value.param).parameters[1] : typeof(value.param) + return DaggerReconstructionUtility{<:innertype} <: union +end + +function AbstractImageReconstruction.validvalue(plan, union::UnionAll, value::RecoPlan{<:DaggerReconstructionUtility}) + innertype = value.param isa DaggerRecoPlan ? typeof(value.param).parameters[1] : typeof(value.param) + return DaggerReconstructionUtility{<:innertype} <: union +end + +function Base.getproperty(plan::RecoPlan{<:DaggerReconstructionUtility}, name::Symbol) + if name == :param + chunk = getfield(plan, :values)[name][] + return ismissing(chunk) ? chunk : DaggerRecoPlan(chunk) + elseif name == :worker + return getfield(plan, :values)[name][] + else + return getproperty(plan.param, name) + end +end + + +# Do not serialize the the worker and collect the remote algo +function StructUtils.lower(style::RecoPlanStyle, plan::RecoPlan{T}) where T <: DaggerReconstructionUtility + dict = Dict{String, Any}( + MODULE_TAG => string(parentmodule(T)), + TYPE_TAG => "RecoPlan{$(nameof(T))}" + ) + dict["param"] = fetch(Dagger.@spawn StructUtils.lower(style, getchunk(plan, :param))) + return dict +end + +function AbstractImageReconstruction.toPlan(param::DaggerReconstructionUtility) + plan = RecoPlan(DaggerReconstructionUtility) + plan.worker = param.worker + setchunk!(plan, :param, Dagger.@mutable worker = param.worker fetch(Dagger.spawn(param.param) do tmp + toPlan(tmp) + end)) + return plan +end +# First load the plan in the current worker, then make it chunk for the current worker. Afterwards with setproperty! one can move the chunk to another process +function StructUtils.make!(style::RecoPlanStyle, plan::RecoPlan{DaggerReconstructionUtility}, dict::Dict{String, Any}) + param = missing + if haskey(dict, "param") + param, _ = StructUtils.make(style, RecoPlan, dict["param"]) + parent!(param, plan) + end + setchunk!(plan, :param, Dagger.@mutable worker = myid() param) + plan.worker = myid() + return plan +end +function AbstractImageReconstruction.build(plan::RecoPlan{DaggerReconstructionUtility}) + worker = plan.worker + param = getchunk(plan, :param) + if param isa Dagger.Chunk + param = Dagger.@mutable worker = worker fetch(Dagger.spawn(param) do tmp + build(tmp) + end) + else + error("Expected a Dagger.Chunk, found $(typeof(param))") + end + return DaggerReconstructionUtility(param, worker) +end \ No newline at end of file diff --git a/src/Utils.jl b/src/Utils.jl index 88f71a6..0175076 100644 --- a/src/Utils.jl +++ b/src/Utils.jl @@ -2,11 +2,12 @@ chunktype(::Dagger.Chunk{T}) where T = T export loadDaggerPlan """ - loadDaggerPlan(filename; worker) + loadDaggerPlan(filename, modules; worker) + loadDaggerPlan(io, modules; worker) Load a local `RecoPlan` from the specified `filename` and interpret it on the designated `worker`. The resulting `RecoPlan` is encapsulated within a `DaggerReconstructionAlgorithm`. """ -function loadDaggerPlan(filename, modules; worker) +function loadDaggerPlan(filename::String, modules; worker) buffer = IOBuffer() open(filename) do file for line in readlines(file; keep = true) @@ -14,7 +15,10 @@ function loadDaggerPlan(filename, modules; worker) end end seekstart(buffer) - plan = Dagger.@mutable worker = worker loadPlan(buffer, modules) + return loadDaggerPlan(buffer, modules; worker) +end +function loadDaggerPlan(io, modules; worker) + plan = Dagger.@mutable worker = worker loadPlan(io, modules) params = RecoPlan(DaggerReconstructionParameter; worker = worker, algo = plan) return RecoPlan(DaggerReconstructionAlgorithm; parameter = params) end \ No newline at end of file diff --git a/test/DaggerRecoAlgorithm.jl b/test/DaggerRecoAlgorithm.jl index ee1a2c1..ec59f9a 100644 --- a/test/DaggerRecoAlgorithm.jl +++ b/test/DaggerRecoAlgorithm.jl @@ -3,12 +3,11 @@ reco = IterativeRadonReconstructionParameters(; shape = size(images)[1:3], angles = angles, iterations = 1, reg = [L2Regularization(0.001), PositiveRegularization()], solver = CGNR); algo = IterativeRadonAlgorithm(IterativeRadonParameters(pre, reco)) plan = toPlan(algo) + reco = reconstruct(algo, sinograms) @testset "Algorithm Interface" begin parameter = DaggerReconstructionParameter(; algo = algo, worker = worker) algoD = DaggerReconstructionAlgorithm(parameter) - - reco = reconstruct(algo, sinograms) @test isready(algoD) == false recoD1 = reconstruct(algoD, sinograms) @@ -21,5 +20,80 @@ recoD2 = take!(algoD) @test isapprox(recoD1, recoD2) end + + @testset "RecoPlan Interface" begin + parameter = RecoPlan(DaggerReconstructionParameter; worker = 1, algo = plan) + planD = RecoPlan(DaggerReconstructionAlgorithm; parameter = parameter) + algoD = build(planD) + + @test algoD isa DaggerReconstructionAlgorithm + + @test isready(algoD) == false + recoD1 = reconstruct(algoD, sinograms) + @test isready(algoD) == false + @test isapprox(reco, recoD1) + + @async put!(algoD, sinograms) + wait(algoD) + @test isready(algoD) + recoD2 = take!(algoD) + @test isapprox(recoD1, recoD2) + + @testset "Serialization" begin + @everywhere begin + @parameter struct SerTestParams <: AbstractTestParameters + value::Float64 = 1.0 + iterations::Int64 = 10 + end + + @reconstruction struct SerTestAlgorithm <: AbstractTestBase + @parameter parameter::SerTestParams + end + + function (params::SerTestParams)(algo::SerTestAlgorithm, input) + return input + params.value + end + end + + @testset "Basic save and load" begin + params = SerTestParams(value=10.0) + algo = SerTestAlgorithm(params) + parameter = DaggerReconstructionParameter(; algo = algo, worker = worker) + algoD = DaggerReconstructionAlgorithm(parameter) + + reco_original = reconstruct(algoD, 5.0) + + plan = toPlan(algoD) + io = IOBuffer() + savePlan(io, plan) + seekstart(io) + + loaded = loadPlan(io, [Main, AbstractImageReconstruction, DaggerImageReconstruction]) + loaded_algo = build(loaded) + reco_loaded = reconstruct(loaded_algo, 5.0) + + @test typeof(plan) == typeof(loaded) + @test typeof(plan.parameter.algo) == typeof(loaded.parameter.algo) + @test reco_original == reco_loaded + end + + @testset "loadDaggerPlan integration" begin + params = SerTestParams(value=7.0) + algo = SerTestAlgorithm(params) + plan = toPlan(algo) + + io = IOBuffer() + savePlan(io, plan) + seekstart(io) + + loaded = loadDaggerPlan(io, [Main, AbstractImageReconstruction, DaggerImageReconstruction], worker = worker) + + @test loaded isa RecoPlan{DaggerReconstructionAlgorithm} + built = build(loaded) + @test built isa DaggerReconstructionAlgorithm + @test built.parameter isa DaggerReconstructionParameter{SerTestAlgorithm} + end + end + end end \ No newline at end of file diff --git a/test/DaggerReconstructionProcess.jl b/test/DaggerReconstructionProcess.jl deleted file mode 100644 index e69de29..0000000 diff --git a/test/DaggerReconstructionUtility.jl b/test/DaggerReconstructionUtility.jl new file mode 100644 index 0000000..b45693e --- /dev/null +++ b/test/DaggerReconstructionUtility.jl @@ -0,0 +1,45 @@ +@testset "DaggerReconstructionUtility" begin + @everywhere begin + @parameter struct SimpleTestParams <: AbstractTestParameters + value::Float64 = 1.0 + iterations::Int64 = 100 + end + @reconstruction mutable struct SimpleAlgorithm{P <: Union{SimpleTestParams, AbstractUtilityReconstructionParameters{SimpleTestParams}}} <: AbstractTestBase + @parameter parameter::P + end + function (param::SimpleTestParams)(algo::SimpleAlgorithm, input::Int64) + return param.value * input + param.iterations + end + end + + params = SimpleTestParams() + wrapped = DaggerReconstructionUtility(; param = params, worker = worker) + + # Direct + algo = SimpleAlgorithm(params) + algoD = SimpleAlgorithm(wrapped) + @test algoD.parameter.worker == worker + @test reconstruct(algo, 42) == reconstruct(algoD, 42) + + # To Plan + plan = toPlan(algo) + planD = toPlan(algoD) + @test planD.parameter isa RecoPlan{DaggerReconstructionUtility} + @test planD.parameter.worker == worker + @test plan.parameter.value == planD.parameter.param.value + + # From Plan + algoD = build(planD) + @test algoD.parameter.worker == worker + @test reconstruct(algo, 42) == reconstruct(algoD, 42) + + # From "file" + io = IOBuffer() + savePlan(io, planD) + seekstart(io) + loaded = loadPlan(io, [Main, AbstractImageReconstruction, DaggerImageReconstruction]) + algoD = build(planD) + @test algoD.parameter.worker == worker + @test reconstruct(algo, 42) == reconstruct(algoD, 42) + +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 7716020..7410efe 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -9,9 +9,12 @@ using Test @everywhere include(joinpath(@__DIR__(), "..", "docs", "src", "literate", "example", "example_include_all.jl")) +@everywhere abstract type AbstractTestBase <: AbstractImageReconstructionAlgorithm end +@everywhere abstract type AbstractTestParameters <: AbstractImageReconstructionParameters end + @testset "DaggerImageReconstruction.jl" begin include("DaggerRecoPlan.jl") include("DaggerRecoAlgorithm.jl") - include("DaggerReconstructionProcess.jl") + include("DaggerReconstructionUtility.jl") end