diff --git a/examples/0_example_radon_backproject.jl b/examples/0_example_radon_backproject.jl index a984041..340e99f 100644 --- a/examples/0_example_radon_backproject.jl +++ b/examples/0_example_radon_backproject.jl @@ -29,23 +29,35 @@ using RadonKA using IndexFunArrays, ImageShow, Plots, ImageIO, PlutoUI, PlutoTest, TestImages # ╔═╡ 03bccb92-b47f-477a-9bdb-74cc404da690 -using KernelAbstractions, CUDA, CUDA.CUDAKernels +using CUDA # ╔═╡ 6f6c8d28-5a54-440e-9b7a-52e1fce959ab -md"# Activate example environment" - -# ╔═╡ f5e2024b-deaf-4344-b610-a4b956abbfaa -md"# Load CUDA -In case you have no CUDA card available, it will run on CPU :) +md"# Load packages +On the first run, Julia is going to install some packages automatically. So start this notebook and give it some minutes to install all packages. +No worries, any future runs will be much faster to start! " # ╔═╡ ef92457a-87c0-43bf-a046-9fe82afbbe13 begin use_CUDA = Ref(true && CUDA.functional()) var"@mytime" = use_CUDA[] ? CUDA.var"@time" : var"@time" + """ + togoc(x) + + Stands for "to GPU or CPU". In case you have a GPU, it moves the array to the GPU. + In case CUDA is runable, it will just leave it on the CPU + """ togoc(x) = use_CUDA[] ? CuArray(x) : x end +# ╔═╡ f5e2024b-deaf-4344-b610-a4b956abbfaa +md""" ## CUDA +Thanks to Julia multiple dispatch and [CUDA.jl](https://github.com/JuliaGPU/CUDA.jl) our code can run on CUDA GPUs. +Big reconstructions can run 5-20 times faster on a CUDA GPU! + +Your CUDA is functional: **$(use_CUDA[])** +""" + # ╔═╡ 810aebe4-2c6e-4ba6-916b-9e4306df33c9 TableOfContents() @@ -135,25 +147,28 @@ simshow(Array(filtered_bproj[:, :, i_z4])) # ╔═╡ a29be556-174a-4ec5-962d-9fdf203d51aa backproject ≈ Array(backproject_cu) +# ╔═╡ fd2bab7a-7ef6-42aa-bcb5-2a52bffc61db + + # ╔═╡ Cell order: -# ╠═6f6c8d28-5a54-440e-9b7a-52e1fce959ab +# ╟─6f6c8d28-5a54-440e-9b7a-52e1fce959ab # ╠═4eb3148e-8f8b-11ee-3cfe-854d3bd5cc80 # ╠═b336e55e-0be4-422f-b48a-0a2242cb0915 # ╠═1311e853-c4cd-42bb-8bf3-5e0d564bf9c5 # ╟─f5e2024b-deaf-4344-b610-a4b956abbfaa # ╠═03bccb92-b47f-477a-9bdb-74cc404da690 -# ╠═ef92457a-87c0-43bf-a046-9fe82afbbe13 +# ╟─ef92457a-87c0-43bf-a046-9fe82afbbe13 # ╟─810aebe4-2c6e-4ba6-916b-9e4306df33c9 # ╟─d25c1381-baf1-429b-8150-622b8f731d83 # ╠═54208d78-cf55-41d7-b4bf-6d1ab4927bbb # ╠═1393d029-66be-40aa-a2f9-f31317222575 -# ╠═01b4b8f8-37d5-425f-975e-ebb3890d8624 +# ╟─01b4b8f8-37d5-425f-975e-ebb3890d8624 # ╟─8be220a4-293d-411d-bbce-e39b64780814 # ╠═b8618268-0892-4abc-ae26-e25e41d07968 # ╠═135e728b-efd8-44bc-81d9-6a2244ce4c31 # ╠═d2cc6fc6-135b-4c4a-8453-9c5bf9e4a24f # ╠═dc14103d-993c-402f-a8b5-a35843f3f4ac -# ╠═783f05e0-2640-4ecd-8c19-1c15a99ee294 +# ╟─783f05e0-2640-4ecd-8c19-1c15a99ee294 # ╠═db2676fd-3305-408f-93b4-08a3d04fdd02 # ╠═1a931e03-6a29-4c3e-b66f-bc1b5936a6f4 # ╠═3d584d94-b88f-4738-a470-7db1fb3fb996 @@ -169,3 +184,4 @@ backproject ≈ Array(backproject_cu) # ╟─bc6e2d40-fcd1-4d7b-8f96-3d4d9e4336de # ╠═eee184e3-8d5d-42fb-81fb-a5d7e59a083a # ╠═a29be556-174a-4ec5-962d-9fdf203d51aa +# ╠═fd2bab7a-7ef6-42aa-bcb5-2a52bffc61db diff --git a/examples/2_CT_with_optimizer.jl b/examples/2_CT_with_optimizer.jl index 3870b92..d7af95f 100644 --- a/examples/2_CT_with_optimizer.jl +++ b/examples/2_CT_with_optimizer.jl @@ -13,23 +13,20 @@ begin end # ╔═╡ 1a255b6f-e83a-481b-8017-ecc996bc4929 -using Zygote, Optim, RadonKA, TestImages, ImageShow, Noise, Plots - -# ╔═╡ 8d73ca31-2b63-4543-a5e5-82fd08917140 -using PlutoUI +using Zygote, Optim, RadonKA, TestImages, ImageShow, Noise, Plots,PlutoUI,Statistics # ╔═╡ a0057ab8-5c84-4f63-882f-1ac6f84fbc5e using CUDA -# ╔═╡ 9310a824-ee3b-4e51-b870-57401c411809 -using Statistics - -# ╔═╡ 8a66c5e3-fa8c-42ec-82c3-7342dc9ca330 -using Tullio - # ╔═╡ 2f0ca285-fd1f-42bf-bb07-7fafb7ddd7f6 using Optimization, OptimizationOptimisers, OptimizationOptimJL +# ╔═╡ d2d039dd-b584-4f72-b16b-1d54841ee767 +md"# Load packages +On the first run, Julia is going to install some packages automatically. So start this notebook and give it some minutes to install all packages. +No worries, any future runs will be much faster to start! +" + # ╔═╡ f4ca400d-2405-40bf-95d8-35580b9871c3 begin use_CUDA = Ref(true && CUDA.functional()) @@ -38,7 +35,12 @@ begin end # ╔═╡ 063d95c7-d150-48f6-b04a-b2026ac4ba69 -md"# CUDA is enabled: $(use_CUDA[])" +md""" ## CUDA +Thanks to Julia multiple dispatch and [CUDA.jl](https://github.com/JuliaGPU/CUDA.jl) our code can run on CUDA GPUs. +Big reconstructions can run 5-20 times faster on a CUDA GPU! + +Your CUDA is functional: **$(use_CUDA[])** +""" # ╔═╡ a351afa1-33e6-4f03-ae06-2215646b70db TableOfContents() @@ -67,7 +69,10 @@ measurement = poisson(radon(img, angles), 2000); simshow(measurement) # ╔═╡ 144d3c73-8b7e-4049-924e-a2e20423f3f7 -md"""# Simple Backprojection""" +md"""# Simple Backprojection + +Typical filtered backprojection which does not perform super well with noise +""" # ╔═╡ 464f022d-0773-43dd-81a7-ca2f6fc91634 img_bp = backproject_filtered(measurement, angles); @@ -81,83 +86,82 @@ img_backproject = backproject(measurement, angles); # ╔═╡ 1feccdec-cc35-4cc8-9a76-0e0e99bc7be3 md"# Optimization with gradient descent -We construct the loss function `f` and its gradient `g!`. +We construct the loss function `f` and its gradient. This format is typically used with [Optim.jl](https://julianlsolvers.github.io/Optim.jl/stable/). The gradient is calculated by automatic differentiation and the reverse rules of RadonKA.jl " -# ╔═╡ e501ede0-62d7-42a1-b258-518be737bd9c +# ╔═╡ cdf0f1fb-c95f-43ab-a41f-159f4963f3af function make_fg!(fwd_operator, measurement; λ=0.01f0, regularizer=x -> zero(eltype(x))) - f = let measurement=measurement - f(x) = mean(abs2, fwd_operator(x) .- measurement) + λ * regularizer(x) - end - - g! = let f=f - g!(G, x) = begin - if !isnothing(G) - G .= Zygote.gradient(f, x)[1] - end - end - end - - return f, g! + f(x) = mean(abs2, fwd_operator(x) .- measurement) + λ * regularizer(x) + + # some Optim boilerplate to get gradient and loss as fast as possible + fg! = let f=f + function fg!(F, G, x) + # Zygote calculates both derivative and loss + if G !== nothing + y, back = Zygote.withgradient(f, x) + # calculate gradient + G .= back[1] + if F !== nothing + return y + end + end + if F !== nothing + return f(x) + end + end + end + return fg! end # ╔═╡ 6a6b53c8-b5f2-46e6-ada1-ee53d99db4d2 -f, g! = make_fg!(x -> radon(x, angles), measurement) +fg! = make_fg!(x -> radon(x, angles), measurement) # ╔═╡ 8f7688e7-208e-466b-b58d-b3e92aae87b3 -init0 = zeros(Float32, size(img)); - -# ╔═╡ ae7a0720-78b4-472a-97d7-301c27c2b877 -@time f(init0); +init0 = ones(Float32, size(img)); -# ╔═╡ 482ae232-b188-4f34-90ea-797406b518ed -@time g!(copy(init0), init0); +# ╔═╡ d65efdb0-4c80-4119-9fcd-185d13c37b2a +@time fg!(copy(init0), copy(init0), init0) # ╔═╡ fc5aae11-793a-47f0-9759-2cc5d856d6a4 -res = Optim.optimize(f, g!, init0, LBFGS(), +@time res = Optim.optimize(Optim.only_fg!(fg!), init0, LBFGS(), Optim.Options(iterations = 20, store_trace=true)) -# ╔═╡ 3149e668-7479-42e4-969a-f2901a2be9d7 -a = res.trace[1] - # ╔═╡ 27488594-0dfd-419d-be5c-0047b2ebdb59 plot([a.value for a in res.trace], xlabel="Iterations", ylabel="loss value", yscale=:log10) # ╔═╡ 9f3d751e-6576-420d-8e5f-a4245be1bb0f [simshow(res.minimizer) simshow(img_bp) simshow(img)] -# ╔═╡ 8b9c5804-877c-4679-a732-8042031892de -f(img_bp ./ maximum(img_bp) .* maximum(img)) - -# ╔═╡ 13695e7c-b91e-4443-a811-d705de1478e2 -f(res.minimizer) - # ╔═╡ 538fec79-94c8-4374-9176-c4dc2c149a74 md"# Add a TV regularizer -[Tullio.jl](https://github.com/mcabbott/Tullio.jl) is a very elegant and performant way to add a regularizer to the reconstruction! -In principle all code except the regularizer runs fast with CUDA! +We add a [Total Variation](https://en.wikipedia.org/wiki/Total_variation_denoising#2D_signal_images) regularizer. " -# ╔═╡ 7224f60a-c29a-48e3-8c99-689aa9587506 -reg(x) = @tullio r := sqrt(1f-8 + (x[i,j] - x[i-1, j])^2 + (x[i,j] - x[i, j-1])^2) +# ╔═╡ 4b6a0cfe-a0a1-486f-ad56-4cf2bf5db430 +reg(x) = sum(sqrt.((circshift(x, (1,0)) .- x).^2 .+ (circshift(x, (0,1)) .- x).^2 .+ 1f-8)) # ╔═╡ 70aa0bff-41f7-409b-a191-10f196dd9233 -reg(init0) +@time reg(init0) # ╔═╡ a69281cf-f024-4cd6-9ae8-a6f52644956a -f2, g2! = make_fg!(x -> radon(x, angles), measurement, regularizer=reg, λ=0.002f0) +fg2! = make_fg!(x -> radon(x, angles), measurement, regularizer=reg, λ=0.002f0) # ╔═╡ a9d74730-2096-4e79-85b6-323ef8a2f54c -res2 = Optim.optimize(f2, g2!, init0, LBFGS(), +@time res2 = Optim.optimize(Optim.only_fg!(fg2!), init0, LBFGS(), Optim.Options(iterations = 20, store_trace=true)) # ╔═╡ 77d2fdf2-e0b2-45f4-ab80-bd03c871baf0 -plot([a.value for a in res2.trace], yscale=:log10) +plot([a.value for a in res2.trace], yscale=:log10, xlabel="iterations", ylabel="loss") + +# ╔═╡ ef15aef6-06c2-4a57-8ac0-86e1253ce895 +md" +----------------with TV ---------------------- without TV ---------------- filtered backprojection ----------- ground truth +" # ╔═╡ 3355590b-00d3-4f37-ae4c-8c69dd5dac5f [simshow(res2.minimizer) simshow(res.minimizer) simshow(img_bp) simshow(img) ] @@ -168,81 +172,77 @@ The [Anscombe transform](https://en.wikipedia.org/wiki/Anscombe_transform) helps Visually the Anscombe transform results in the best reconstructions. " -# ╔═╡ bc38105d-ce12-400f-97a4-7059fa0ca72e -function make_fg_anscombe!(fwd_operator, measurement; λ=0.01f0, regularizer=x -> zero(eltype(x))) - - f = let measurement=measurement - # apply sqrt for anscombe - f(x) = sum(abs2, sqrt.(max.(0, fwd_operator(x)) .+ 3f0/8f0) .- sqrt.(3f0 / 8f0 .+ measurement)) + λ * regularizer(x) - end - - g! = let f=f - g!(G, x) = begin - if !isnothing(G) - G .= Zygote.gradient(f, x)[1] - end - end - end - - return f, g! +# ╔═╡ aa13314c-aa98-4952-a5be-02fb02c50709 +function make_fg_anscombe!(fwd_operator, measurement) + + f(x) = sum(abs2, sqrt.(max.(0, fwd_operator(x)) .+ 3f0/8f0) .- sqrt.(3f0 / 8f0 .+ measurement)) + + # some Optim boilerplate to get gradient and loss as fast as possible + fg! = let f=f + function fg!(F, G, x) + # Zygote calculates both derivative and loss + if G !== nothing + y, back = Zygote.withgradient(f, x) + # calculate gradient + G .= back[1] + if F !== nothing + return y + end + end + if F !== nothing + return f(x) + end + end + end + return fg! end # ╔═╡ 0f73f624-3fd9-42a9-b623-d1be11bbe5af -f3, g3! = make_fg_anscombe!(x -> radon(x, angles), 500 .* measurement, regularizer=reg, λ=0.0002f0 * 500) +fg_ans! = make_fg_anscombe!(x -> radon(x, angles), 2000 .* measurement) # ╔═╡ 2428fb0d-c911-4ac4-9dec-0fb19d9ad466 -@time res3 = Optim.optimize(f3, g3!, init0, LBFGS(), +@time res_ans = Optim.optimize(Optim.only_fg!(fg_ans!), init0, LBFGS(), Optim.Options(iterations = 20, store_trace=true)) -# ╔═╡ 40bff2bd-fbbb-4840-92f5-7319202afeb5 -[simshow(res3.minimizer) simshow(res2.minimizer) simshow(img_bp) simshow(img) ] - -# ╔═╡ bdd283a2-2ecd-4e66-927b-d961842af434 -compare(a, b) = sum(abs2, a ./ mean(a) .- b ./ mean(b)) - -# ╔═╡ 3ccb13a4-d2d3-41f1-bf3a-24fe657c8b07 -compare(res.minimizer, img) - -# ╔═╡ 9e100052-07b2-4bce-b4e7-e85e2c2f3c7c -compare(res2.minimizer, img) +# ╔═╡ b66fcc8f-3f01-4163-9e49-d46b6370390d +md" +-------- Anscombe transform ------------ with TV ---------------- filtered backprojection ----------- ground truth +" -# ╔═╡ 35fb48f6-5a80-4eee-bbb8-c2baa98043fe -compare(res3.minimizer, img) +# ╔═╡ 40bff2bd-fbbb-4840-92f5-7319202afeb5 +[simshow(res_ans.minimizer) simshow(res2.minimizer) simshow(img_bp) simshow(img) ] # ╔═╡ 2692dda1-fcd7-408e-bf33-0511597513fc -md"# Try with CUDA" +md"# Try with CUDA +On my multithreaded CPU it takes around 4 seconds. +With CUDA it takes 0.2 seconds! +" # ╔═╡ 4f432699-3b0a-4275-8594-57904cd5d7ba angles_c = togoc(angles); # ╔═╡ 443f9400-3cbd-4e41-b761-1c15c5bb537d -f_cuda, g_cuda! = make_fg!(x -> radon(x, angles_c), togoc(measurement)) +fg_cuda! = make_fg!(x -> radon(x, angles_c), togoc(measurement)) # ╔═╡ 7c12b32f-1098-4050-b3f0-a89a53bb569e -g_cuda!(togoc(init0), togoc(init0)); +fg_cuda!(togoc(init0), togoc(init0), togoc(init0)); # ╔═╡ 9060c58c-95fb-4616-b474-2193b41aab4f -@mytime res4 = Optim.optimize(f_cuda, g_cuda!, togoc(init0), LBFGS(), +@mytime res_cuda = Optim.optimize(Optim.only_fg!(fg_cuda!), togoc(init0), LBFGS(), Optim.Options(iterations = 20, store_trace=true)) -# ╔═╡ 559a8866-5094-4e59-ac09-222a002e052b -f_cuda2, g_cuda2! = make_fg_anscombe!(x -> radon(x, angles_c), togoc(measurement)) - -# ╔═╡ 3b2599ea-a8db-4e7d-a51f-d9b0db6389c4 -@mytime res7 = Optim.optimize(f_cuda2, g_cuda2!, togoc(init0), LBFGS(), - Optim.Options(iterations = 20, - store_trace=true)) - -# ╔═╡ 34ec68ba-6130-448c-9bb0-04a4c3ed734c -[simshow(Array(res4.minimizer)) simshow(Array(res7.minimizer))] +# ╔═╡ 7107f76e-7033-4e2f-ad44-d8486072c3a1 +md" +-------------------- CUDA ------------------- without TV ---------------- filtered backprojection ----------- ground truth +" # ╔═╡ e72103d4-2940-4a60-addd-6d8990d8f0cf -[simshow(Array(res4.minimizer)) simshow(res2.minimizer) simshow(res.minimizer) simshow(img_bp) simshow(img) ] +[simshow(Array(res_cuda.minimizer)) simshow(res.minimizer) simshow(img_bp) simshow(img) ] # ╔═╡ 2b71a257-b3d1-4df1-83d2-4e0c7eeb4af8 -md"# Try with Optimization" +md"# Try with Optimization.jl" # ╔═╡ e2628d52-641d-4dcc-bfb0-6c3731d6a1c5 measurement_c = togoc(measurement); @@ -263,72 +263,62 @@ opt_f(init0_c, angles_c) problem = OptimizationProblem(opt_fun, init0_c, angles_c); # ╔═╡ fb3b8d74-341b-4627-b361-a28f7ccaf75b -@mytime res5 = solve(problem, OptimizationOptimisers.Adam(0.01), maxiters=500) +@mytime res5 = solve(problem, OptimizationOptimisers.Adam(0.01), maxiters=500); # ╔═╡ 68df9047-a898-4445-992a-cdfd9b4dc910 @mytime res6 = solve(problem, OptimizationOptimJL.LBFGS(), maxiters=20) # ╔═╡ b054e438-d3f2-4df9-b504-5281e48287d4 -[simshow(Array(res5.u)) simshow(Array(res6.u)) simshow(Array(res4.minimizer))] +[simshow(Array(res5.u)) simshow(Array(res6.u)) simshow(Array(res_cuda.minimizer))] # ╔═╡ Cell order: +# ╟─d2d039dd-b584-4f72-b16b-1d54841ee767 # ╠═72f79d9e-a0dc-11ee-3eee-979a9f94c302 # ╠═1a255b6f-e83a-481b-8017-ecc996bc4929 -# ╠═8d73ca31-2b63-4543-a5e5-82fd08917140 +# ╠═063d95c7-d150-48f6-b04a-b2026ac4ba69 # ╠═a0057ab8-5c84-4f63-882f-1ac6f84fbc5e -# ╟─063d95c7-d150-48f6-b04a-b2026ac4ba69 # ╠═f4ca400d-2405-40bf-95d8-35580b9871c3 -# ╠═9310a824-ee3b-4e51-b870-57401c411809 # ╠═a351afa1-33e6-4f03-ae06-2215646b70db # ╟─0b49cba6-d506-422e-8bba-35a6e3476f37 # ╠═5fc5b324-90ca-4e4a-8b5b-35b9f276c47d -# ╠═274e71b3-d4b5-406b-848b-6c8847179125 +# ╟─274e71b3-d4b5-406b-848b-6c8847179125 # ╟─b36b1679-d4bd-4a7d-b55e-c0ad930be9d5 # ╠═e0043bf0-c55e-4160-a97b-41e2acceb19f # ╠═8b8a4aa2-e583-4f50-9082-06b3511b853e -# ╠═d2f035c7-8bc4-4c2b-aeb5-56ce955b8f4c +# ╟─d2f035c7-8bc4-4c2b-aeb5-56ce955b8f4c # ╟─144d3c73-8b7e-4049-924e-a2e20423f3f7 # ╠═464f022d-0773-43dd-81a7-ca2f6fc91634 # ╠═49e59001-0e8d-4872-ae50-47c38486b3fd -# ╠═6fac5606-2350-4f22-9f35-120936114d5a +# ╟─6fac5606-2350-4f22-9f35-120936114d5a # ╟─1feccdec-cc35-4cc8-9a76-0e0e99bc7be3 -# ╠═e501ede0-62d7-42a1-b258-518be737bd9c +# ╠═cdf0f1fb-c95f-43ab-a41f-159f4963f3af # ╠═6a6b53c8-b5f2-46e6-ada1-ee53d99db4d2 +# ╠═d65efdb0-4c80-4119-9fcd-185d13c37b2a # ╠═8f7688e7-208e-466b-b58d-b3e92aae87b3 -# ╠═ae7a0720-78b4-472a-97d7-301c27c2b877 -# ╠═482ae232-b188-4f34-90ea-797406b518ed # ╠═fc5aae11-793a-47f0-9759-2cc5d856d6a4 -# ╠═3149e668-7479-42e4-969a-f2901a2be9d7 # ╠═27488594-0dfd-419d-be5c-0047b2ebdb59 # ╠═9f3d751e-6576-420d-8e5f-a4245be1bb0f -# ╠═8b9c5804-877c-4679-a732-8042031892de -# ╠═13695e7c-b91e-4443-a811-d705de1478e2 # ╟─538fec79-94c8-4374-9176-c4dc2c149a74 -# ╠═8a66c5e3-fa8c-42ec-82c3-7342dc9ca330 -# ╠═7224f60a-c29a-48e3-8c99-689aa9587506 +# ╠═4b6a0cfe-a0a1-486f-ad56-4cf2bf5db430 # ╠═70aa0bff-41f7-409b-a191-10f196dd9233 # ╠═a69281cf-f024-4cd6-9ae8-a6f52644956a # ╠═a9d74730-2096-4e79-85b6-323ef8a2f54c -# ╠═77d2fdf2-e0b2-45f4-ab80-bd03c871baf0 -# ╠═3355590b-00d3-4f37-ae4c-8c69dd5dac5f +# ╟─77d2fdf2-e0b2-45f4-ab80-bd03c871baf0 +# ╟─ef15aef6-06c2-4a57-8ac0-86e1253ce895 +# ╟─3355590b-00d3-4f37-ae4c-8c69dd5dac5f # ╟─d91046bd-f5cb-4924-b518-1446a0029b80 -# ╠═bc38105d-ce12-400f-97a4-7059fa0ca72e +# ╠═aa13314c-aa98-4952-a5be-02fb02c50709 # ╠═0f73f624-3fd9-42a9-b623-d1be11bbe5af # ╠═2428fb0d-c911-4ac4-9dec-0fb19d9ad466 +# ╠═b66fcc8f-3f01-4163-9e49-d46b6370390d # ╠═40bff2bd-fbbb-4840-92f5-7319202afeb5 -# ╠═bdd283a2-2ecd-4e66-927b-d961842af434 -# ╠═3ccb13a4-d2d3-41f1-bf3a-24fe657c8b07 -# ╠═9e100052-07b2-4bce-b4e7-e85e2c2f3c7c -# ╠═35fb48f6-5a80-4eee-bbb8-c2baa98043fe # ╟─2692dda1-fcd7-408e-bf33-0511597513fc # ╠═4f432699-3b0a-4275-8594-57904cd5d7ba # ╠═443f9400-3cbd-4e41-b761-1c15c5bb537d # ╠═7c12b32f-1098-4050-b3f0-a89a53bb569e # ╠═9060c58c-95fb-4616-b474-2193b41aab4f -# ╠═559a8866-5094-4e59-ac09-222a002e052b -# ╠═3b2599ea-a8db-4e7d-a51f-d9b0db6389c4 -# ╠═34ec68ba-6130-448c-9bb0-04a4c3ed734c -# ╠═e72103d4-2940-4a60-addd-6d8990d8f0cf +# ╟─7107f76e-7033-4e2f-ad44-d8486072c3a1 +# ╟─e72103d4-2940-4a60-addd-6d8990d8f0cf # ╟─2b71a257-b3d1-4df1-83d2-4e0c7eeb4af8 # ╠═2f0ca285-fd1f-42bf-bb07-7fafb7ddd7f6 # ╠═e2628d52-641d-4dcc-bfb0-6c3731d6a1c5