Parameter Sweeps

When developing or optimizing reconstruction algorithms, it's often useful to systematically explore how different parameter values affect the reconstruction result. Parameter sweeps allow you to iterate over a RecoPlan template, generating multiple configurations for systematic exploration. This is particularly useful for:

  • Hyperparameter tuning (e.g., iterations, regularization strength)
  • Algorithm comparison (e.g., different solvers)
  • Grid search optimization

This How-To builds on the reconstruction examples and shows how to use PlanSweep and related utilities.

Basic Single-Field Sweep

A PlanSweep iterates over values for a specific field of a RecoPlan. Let's create a template plan and sweep over the number of iterations:

First, create a base reconstruction plan:

pre = RecoPlan(RadonPreprocessingParameters; frames = collect(1:1))
reco = RecoPlan(IterativeRadonReconstructionParameters;
  eltype = eltype(sinograms),
  angles = angles,
  shape = size(images)[1:3],
  iterations = 10,
  reg = [L2Regularization(0.001)],
  solver = CGNR)
params = RecoPlan(IterativeRadonParameters; pre = pre, reco = reco)
plan_template = RecoPlan(IterativeRadonAlgorithm; parameter = params)
RecoPlan{Main.OurRadonReco.IterativeRadonAlgorithm}
└─ parameter::RecoPlan{Main.OurRadonReco.IterativeRadonParameters}
   ├─ reco::RecoPlan{Main.OurRadonReco.IterativeRadonReconstructionParameters}
   │  ├─ iterations = 10
   │  ├─ solver = CGNR
   │  ├─ reg = L2Regularization{Float64}[L2Regularization{Float64}(0.001)]
   │  ├─ eltype = Float64
   │  ├─ 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]
   │  └─ shape = (64, 64, 64)
   └─ pre::RecoPlan{Main.OurRadonReco.RadonPreprocessingParameters}
      ├─ numAverages
      └─ frames = [1]

Now create a sweep over the iterations field:

sweep = PlanSweep(plan_template.parameter.reco, :iterations, [1, 2, 3])
PlanSweep(RecoPlan{Main.OurRadonReco.IterativeRadonAlgorithm}, field=iterations, nvalues=3)
  values = [1, 2, 3]

The sweep has a length equal to the number of values:

length(sweep)
3

You can iterate over the sweep to get plans with different parameter values:

results_iterations = []
for plan in sweep
  algo = build(plan)
  img = reconstruct(algo, sinograms)
  push!(results_iterations, img)
end

Each iteration produces a complete RecoPlan with the field value set:

sweep[3].parameter.reco.iterations
3

Using the @plan_sweep Macro

The @plan_sweep macro provides a more convenient syntax using assignment notation:

sweep_macro = @plan_sweep(plan_template.parameter.reco.iterations = [1, 5, 10, 20])
PlanSweep(RecoPlan{Main.OurRadonReco.IterativeRadonAlgorithm}, field=iterations, nvalues=4)
  values = [1, 5, 10, 20]

Note: The macro requires the base plan to be a variable. It parses the left-hand side to determine the parent plan and field name.

Grid Search with Iterators.product

When you want to explore combinations of multiple parameters, use Iterators.product to create a Cartesian product of sweeps:

sweep_iterations = @plan_sweep(plan_template.parameter.reco.iterations = [5, 10, 20])
sweep_solver = @plan_sweep(plan_template.parameter.reco.solver = [CGNR, Kaczmarz])

grid = Iterators.product(sweep_iterations, sweep_solver)
ProdSweep(2 sweeps, total_combinations=6)
  [1] PlanSweep(RecoPlan{Main.OurRadonReco.IterativeRadonAlgorithm}, field=iterations, nvalues=3)
  [2] PlanSweep(RecoPlan{Main.OurRadonReco.IterativeRadonAlgorithm}, field=solver, nvalues=2)

The grid has length equal to the product of individual sweep lengths:

length(grid)
6

Iterate over all combinations:

parameters = []
for (i, plan) in enumerate(grid)
  push!(parameters, grid(i))
end
parameters
6-element Vector{Any}:
 (:iterations => 5, :solver => RegularizedLeastSquares.CGNR)
 (:iterations => 10, :solver => RegularizedLeastSquares.CGNR)
 (:iterations => 20, :solver => RegularizedLeastSquares.CGNR)
 (:iterations => 5, :solver => RegularizedLeastSquares.Kaczmarz)
 (:iterations => 10, :solver => RegularizedLeastSquares.Kaczmarz)
 (:iterations => 20, :solver => RegularizedLeastSquares.Kaczmarz)

This gives us 3 × 2 = 6 combinations: (5, CGNR), (10, CGNR), (20, CGNR), (5, Kaczmarz), (10, Kaczmarz), (20, Kaczmarz)

Zipped Sweeps with Iterators.zip

For sweeps that should iterate in parallel (same index), use Iterators.zip:

iterations_list = [5, 10, 20]
reg_values = [L2Regularization(0.0001), L2Regularization(0.001), L2Regularization(0.01)]

sweep_iter = @plan_sweep(plan_template.parameter.reco.iterations = iterations_list)
sweep_reg = @plan_sweep(plan_template.parameter.reco.reg = [[r] for r in reg_values])

zipped = Iterators.zip(sweep_iter, sweep_reg)
ZipSweep(2 sweeps, length=3)
  [1] PlanSweep(RecoPlan{Main.OurRadonReco.IterativeRadonAlgorithm}, field=iterations, nvalues=3)
  [2] PlanSweep(RecoPlan{Main.OurRadonReco.IterativeRadonAlgorithm}, field=reg, nvalues=3)

The zipped sweep has length equal to the length of the shortest input:

length(zipped)
3

Each iteration yields a plan with both parameters set:

parameters = []
for (i, plan) in enumerate(grid)
  push!(parameters, grid(i))
end
parameters
6-element Vector{Any}:
 (:iterations => 5, :solver => RegularizedLeastSquares.CGNR)
 (:iterations => 10, :solver => RegularizedLeastSquares.CGNR)
 (:iterations => 20, :solver => RegularizedLeastSquares.CGNR)
 (:iterations => 5, :solver => RegularizedLeastSquares.Kaczmarz)
 (:iterations => 10, :solver => RegularizedLeastSquares.Kaczmarz)
 (:iterations => 20, :solver => RegularizedLeastSquares.Kaczmarz)

Multi-Threading

Sweeps can also be combined with multi-threading and caching:

results = [similar(images, 0, 0, 0, 0) for i = 1:length(zipped)]
Threads.@threads for i = 1:length(zipped)
  reco = reconstruct(build(zipped[i]), sinograms)
  results[i] = reco
end

Note that we did not directly loop over the sweep, since @threads expects an abstract vector and not an iterator. With the getindex we can generate the appropriate sweep plan variant.

fig = Figure()
for i = 1:length(zipped)
  plot_image(fig[i,1], reverse(images[:, :, 24, 1]))
  plot_image(fig[i,2], sinograms[:, :, 24, 1])
  plot_image(fig[i,3], reverse(results[i][:, :, 24, 1]))
end
resize_to_layout!(fig)
fig
Example block output

Limitations

  • All sweeps in a product or zip must share the same root plan
  • Duplicate sweeps targeting the same field will throw an error
  • Zipped sweeps must have the same length

This page was generated using Literate.jl.