From e4aea40f43699fc752f15bab21785f0fac683bec Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Tue, 30 May 2023 17:06:48 +0200 Subject: [PATCH 01/44] added the optim code --- src/DataToFunctions.jl | 49 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/DataToFunctions.jl b/src/DataToFunctions.jl index fd58afb..07ae703 100644 --- a/src/DataToFunctions.jl +++ b/src/DataToFunctions.jl @@ -52,4 +52,51 @@ function get_function(data::AbstractArray; super_sampling=2, extrapolation_bc=ze end -end # module DataToFunctions + + +""" + perform_fit(loss_function, fitting_data::AbstractArray) + +Performs a fit to the fitting data using a loss function defined by the user + +# Arguments +`loss_function`: User-defined loss function which is minimized +`fitting_data`: The data which is being fitted + +# Returns +a vector of 4 parameters: first 2 for the shift and other 2 for the scaling factors + +# Example +there is an example of this function in the `examples/star_fitting.jl` + +""" +function perform_fit(loss_function, fitting_data::AbstractArray) + # guess the shift parameters by taking the maximum values of the array and + # centering the positions + a, b = Tuple(argmax(fitting_data)) .- size(fitting_data) ./2.0 .- 1.0 + + # assigning the initial parameter estimates + init_x = [a, b, 1.0, 1.0] + + # setting the lower and upper boundary of the parameter values based on the limits of the shift and scaling + lower = [-1*size(fitting_data)[1], -1*size(fitting_data)[2], 0.0, 0.0] + upper = [size(fitting_data)[1], size(fitting_data)[2], size(fitting_data)[1], size(fitting_data)[2]] + + # initializing the LBFGS optimizer + inner_optimizer = LBFGS(; m=1, linesearch=LineSearches.BackTracking(order=2)) + + # Computer, Optimize! :D + res = optimize( + loss_function, + lower, upper, + init_x, + Fminbox(inner_optimizer), + Optim.Options(store_trace = true, extended_trace = true, iterations=500), + autodiff = :forward + ) + + # return the estimated parameters + return Optim.minimizer(res) +end + +end # module DataToFunctions \ No newline at end of file From 7ecba4b315f790d38529a9fb7cea04328384c69f Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Wed, 31 May 2023 17:47:38 +0200 Subject: [PATCH 02/44] added the general 7 parameters fitting --- Project.toml | 14 +++ examples/Zygote_adding.jl | 47 +++++++++ examples/anim_general_1.mp4 | Bin 0 -> 163373 bytes examples/deformed_fitting.jl | 42 ++++++++ examples/perform_fitting.jl | 117 ++++++++++++++++++++ examples/perform_random_optim.jl | 176 +++++++++++++++++++++++++++++++ examples/star_fitting.jl | 85 +++++++++++++++ examples/star_fitting_loop.jl | 107 +++++++++++++++++++ examples/star_rotation.jl | 90 ++++++++++++++++ src/DataToFunctions.jl | 101 +++++++++++++++++- 10 files changed, 777 insertions(+), 2 deletions(-) create mode 100644 examples/Zygote_adding.jl create mode 100644 examples/anim_general_1.mp4 create mode 100644 examples/deformed_fitting.jl create mode 100644 examples/perform_fitting.jl create mode 100644 examples/perform_random_optim.jl create mode 100644 examples/star_fitting.jl create mode 100644 examples/star_fitting_loop.jl create mode 100644 examples/star_rotation.jl diff --git a/Project.toml b/Project.toml index 7c59800..d072ef9 100644 --- a/Project.toml +++ b/Project.toml @@ -4,5 +4,19 @@ authors = ["RainerHeintzmann "] version = "0.1.0" [deps] +Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +CoordinateTransformations = "150eb455-5306-5404-9cee-2592286d6298" +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" FourierTools = "b18b359b-aebc-45ac-a139-9c0ccbb2871e" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" +JLArrays = "27aeb0d3-9eb9-45fb-866b-73c2ecf80fcb" +LineSearches = "d3d80556-e9d4-5f37-9878-2ab0fcc64255" +Optim = "429524aa-4258-5aef-a3af-852621145aeb" +Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" +Rotations = "6038ab10-8711-5258-84ad-4b1120ba62dc" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +View5D = "90d841e0-6953-4e90-9f3a-43681da8e949" +Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" diff --git a/examples/Zygote_adding.jl b/examples/Zygote_adding.jl new file mode 100644 index 0000000..d23ef1d --- /dev/null +++ b/examples/Zygote_adding.jl @@ -0,0 +1,47 @@ +using Zygote +using DataToFunctions + +data = rand(11,10) +f = get_function(data; super_sampling=2); + +loss(p, z) = sum(abs2.(f(p, z) .- data)) + +# Zygote.forwarddiff needs only one input +loss(p2::Vector{Tuple{Float64, Float64}}) = loss(p2[1], p2[2]) + + +for i in 1:20 + pr = [(0.0, -0.1+i/100.0), (1.0, 1.0)] + + # to take derivative of a interpolation process, it is better to use the Zygote.forwarddiff + loss_grad = Zygote.forwarddiff(loss, pr) + + # it can be seen that increasing the scale variable (y direction) leads to increase in the loss function gradient value + println(loss_grad) + +end + +array_scale = Array{Tuple{Float64, Float64}, 2}(undef, 200, 2) +list_loss_grad_scale = Array{Float64, 2}(undef, 200, 200) + +x_1 = 0.9:0.01:1.1 +y_1 = 0.9:0.01:1.1 + + +for i in 1:200 + for j in 1:200 + array_scale[i, :] = [(0.0, 0.0), (0.9 + j/1000.0, 0.9 + i/1000.0)] + + #pr = [(0.0, 0.0), loss_scale[1, :]] + + # to take derivative of a interpolation process, it is better to use the Zygote.forwarddiff + list_loss_grad_scale[i, j] = Zygote.forwarddiff(loss, array_scale[i, :]) + + # it can be seen that increasing the scale variable (y direction) leads to increase in the loss function gradient value + #println(loss_grad) + end + +end + +surface(1:200, 1:200, list_loss_grad_scale) + diff --git a/examples/anim_general_1.mp4 b/examples/anim_general_1.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..50991bd283e3483b969f659b0a928317b93b0016 GIT binary patch literal 163373 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∈)(qNK|ZT9 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

W7lq8Gnf4n@yyPv_Q{EGPrVH*Pf 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 (yWUgbRB0Ho;^<+P)Zs@~P1o5fI9od=) z3}5faZHa9qVD7M?qzc$IPFY&!`8QLwrmQ328(}apzWgp2d4Fhj^kXHpbzffEbtz)V z(Z|hp?r;ShZd+3XwrJEVzeXmTMu-6>FI7g^Hv=Dc`9Y7zq^&K_D}i-boq{u`e_`ru z>=*-;`k<1Qy*uvjDEU8|eD80#DIyr+^lhb>DwBB|JRzi6n)DWdR-jagbt6EycLx)&)_d#vD4I87z^(xy1cX; zw{fDSRv^^oWsL3;mwh?uYto|8wmn#wHMq!PuF5uz+itDG<;t+YcDxb(FCgnWD-T%}_A zewc!uX&8G+Mn_wS-L~tw5Ssl3hNdiMYd*{KgdO+oFr4os%BKT!kxh6UEavO9$h+*Z zd^;%=BiMDXZ>n~3OGjYnI%u3o?a9Y;fQ!f*&z-_=d0#T&EWug?-Fp9c%>U)qE5&+- zrM#e4SCC1(F#62kYKQ0kdr{j74aM^egk~D71H$9yqfNys^{o*^R(!{u)A8x@hP0Al zW*Q$FPB0A7MlHkTHLreigtbneE4d`Sk`K1`@H@ zma1rb6MT!lU)YP{0hR&Wyz(zMd2ozbY}>JQ4LYAskwX#Qo+3bS$;}ox)J-vXIoTud z=fo9tO@qje%|4RsSgSau4FsQzGFcg{>>4*x7qwkKTGLoecO}$8p9uCUSe7Ix4a5o5 z))uZ7_86KxlVfzEJF7wtep!mOK?6_Mr{?YwoIJx9-?C0d9#w6MX5-r(Ed$XY-}=}d zw2%IDSmp88vaD2>SJD9e5~wA(Jn1hd`M){zlH{uATn}9@S}f>xw;B~YZ;W3@Q~lsk z*c^q1mUR2!tC0<7vgJb{oRL^M6JZHpD0xoqg0SF8s? z+WLqTQBZ^sje5CWL$Vx}hdhCmKCkJqP|TnzVxZE~e}Hs{o9lu=loCE6U@qdbg;pH- zRbLgSlmQm?W%fnj%kQ3kmTiLyc}Jz{_^h=QO**708?m~wM9A;7uXL@_1t=|?8)!Zd zT)yDJyK15C8!v80Hbve>;v4U9wbq*{c$%Y{o$o0*;yAr;sE&-}?9_tmS@VUj!xbku zuYdMS^CrAkB+X^jqjtEscdiB66xlM1*22;+<#nbP1IOp5fefCtLtTfM#~A+^$K1LiQ54EnU+;F>cU77kj%9I2WSen0+}H zb1}hkR>t=V*Ww8&Grd^@_d?m8GI zC8evnx?n^|_kM=|1+wh&P!k;%nF9RDI7Vwpju5@lm{72?tMRGuj|XCoulXLrd7%n~ zWeLaFQSZ<=wtA;!i)1lxjNwXhlfX6AJHGWuq^5g)P$>GQfRa|gkyhaf4tqUKL2IWn zF=Fh;t^WlGTKwhEuKG4O1Bzek@Fg!$r0<>arE-uCPa*0+ejr+#z0uAb`Plf-Hbcm+ zL^=3?irIU(S1qeRG=A9e16cJKxPWw_CH6hE1jQYZ4Eb{M< zArYQlJ`IeDVm<9?3`h-!0u~=%$v3R0a}LVV$f^$G-dj1})j;>BA|KG1X^5KODk=oj zFKYZI2G-!RvXJE5*~-Y@Ap@_VpgraP!L$E95!%&m$r!sQc^0*-`rHGb-5piOacZ<2}+)(bt$y&mqbB_lzXe~UOC zIA8c6dDhr!d%j%wSc%GVwTP%}guM3=p~YExTu{t->;j#SZ@k)5x%6!~0w^lgWKKZX z_0YuTH@XY2F)iMEG74!wff$&ket)2Ct($8;F;nSLaNa9g)|yrRl)553+-cRaO;>uR zVBe)^*jF5bjwv`ih1p~9^Xo|mWxZ;wQWOfpeM&Aq1l|vE?T9E6!5<^50>ke-7IDxx z*~dCqxol=%Myz=JW#1=6W&GAaK`GG``~yk}GmXOJD_irAZyE7KyV6s6A5ST%glOe1j*Ax;ssHc)z0;U+hu zNE`vcYA_;S`_u+*gHlr7>P!v?a9&(}=XQ{Ij`cOvkd)=q7|Kw{#fOqkq~XN3JhjS* zO$iDQas{&^hJy3@7zUA63D|*`0Z76E`WQoAXscsldjhGWS2p`!q^nH4?8|^^*s%N4 zANO%qd6xdubCW~~z-J@)OE+otBckt8nB>gXMK|XXSw{sGPq@$oZyMfUi9rt+MrmJyM zf9!ByRq@D7-VL3-dPN3q?pxBhz@4dLuU=5k8YSggMk%W68Zx!$Wp{zftMcGc-`!(K z!NmlU!s-Zur6e0qqYuWF7rM?;WP~Dwhi^a2w?;B^Q1mSjJy%@33f&c>zy-@x@dxmx zTnNx_McDqKapjeq;G}|oye9sdk>S;&^QWjBjt-v@tLffk*23vU0T7P5`=3Nwxo}67 z;1UnQb#Kz^GTL`!T5|@5J|7edVdMMmH)wf7hH;XAyls9XH_W0N#s!b!+etHCzM-#s zpFDOgH+Hv0} zg5?Zh`s=4g@M8dgZfWP>;0mg4Yv*cZ2J*ih@M{1-_ZEPFFyi0N{xbq5{38(n#QvW5 zzlPxe04>_t(fApdr04wX_MV_o|L6EE4ZQEakAL0gzjt4RN>H7{+Y9N<&77UU5QVv| zlk;y;pahs)(3ARB78}tvZa@g^AnYSkpD#2od`*+{ce>cZ%w!i<7+}wasUQntycrEzB zZh+Jhlk9RzhLT4fsQV{Dx!z0w7s!e(RKxxH*OS00RUj&W^t-6^_>ufDr(Y zI62=|I4m8eUlq9c7ZxS}%K>{3umg4;B*YNYKY1&DodX}rZ5rBd{#P6pg70QzFhHpz`WJqxl5490e})DkY$i^!1y7^cR>!81p?}efeMECL81oZpw0-aAb$c9 zJxFYztbLHjfayjcrv50(A%SLD~Q*9i(iKKpi24ATI}L7^HU~MgNY2_XdhV z0_zW!7r_+F>-Os$Oh0(A4j@`!d=4Z~KEe#hS3!CYQUiEjPzPWHBv3vC)ER)KgJlOw zL8=AMVS@Z4NV&hM9^^1RI)3rn^jDN9bO)E&CjX=af-u*icKD*mG0QI=dUk7s7ySPSUtc9c0cewKp#k8 zJwf1qhhZ{cbqOmkc-?JYn0_H3!Q{cNgVo7ANHBS@Jg|BN?Fd#60B8%h^$zw&OdY^5 z?0|p-+6G|aV&-fN@~3v@w;T)tw14mPfgMFh interp_linear((center .+ pos)...) + # fitp(t) = interp_linear(t...) + # @time res1 = fitp.(tcoords); # 1 sec + # function my_zoom + +end + + +f_d = get_function(sample_data_d; super_sampling=2, extrapolation_bc=0.0) + + +f_d(true_vals); \ No newline at end of file diff --git a/examples/perform_fitting.jl b/examples/perform_fitting.jl new file mode 100644 index 0000000..c12925f --- /dev/null +++ b/examples/perform_fitting.jl @@ -0,0 +1,117 @@ +using DataToFunctions +using Optim, StaticArrays, LinearAlgebra +using Zygote +using ForwardDiff, LineSearches, Plots, Printf + + +Base.show(io::IO, f::Float64) = @printf(io, "%.2f", f) + +true_vals = [1.0, 2.0, 1.0, 1.0] +init_x = [0.5, 1.5, 1.0, 1.0] + +sample_data = rand(11,12) + +f = get_function(sample_data; super_sampling=2, extrapolation_bc=0.0); +#f(p0::Vector{Float64}) = f([p0[1], p0[2], p0[3], p0[4]]) + +fitting_data = f(true_vals) .+ rand(11, 12)./10.0 +#f(p2[1], p2[2]) = f(p2::Vector{Tuple{Float64, Float64}}) +loss(p, z) = sum(abs2.(f(p, z) .- fitting_data)) +loss(p2::Vector{Tuple{Float64, Float64}}) = loss(p2[1], p2[2]) +loss(p3) = loss([p3[1], p3[2]], [p3[3], p3[4]]) + + + + + +#Zygote.forwarddiff(loss, init_x) + +ForwardDiff.gradient(loss, init_x)#true_vals .+ [0.0001, 0.0, 0.0, 0.0]) +#conf = ForwardDiff.GradientConfig(f, init_x, chunk::Chunk = Chunk(init_x)) + + +""" +BFGS(; alphaguess = Optim.LineSearches.InitialStatic(), + linesearch = Optim.LineSearches.HagerZhang(), + initial_invH = nothing, + initial_stepnorm = 0.001, + manifold = Optim.Flat() + ) + +GradientDescent(; alphaguess = 0.01, + linesearch = Optim.LineSearches.HagerZhang(), + P = nothing, + precondprep = (P, x) -> nothing +) +""" +lower = [-1*size(fitting_data)[1], -1*size(fitting_data)[2], 0.0, 0.0] +upper = [size(fitting_data)[1], size(fitting_data)[2], size(fitting_data)[1], size(fitting_data)[2]] +#initial_x = [2.0, 2.0] +# requires using LineSearches +inner_optimizer = LBFGS(; m=1, linesearch=LineSearches.BackTracking(order=2)) +res = optimize( + loss, + lower, upper, + init_x, + Fminbox(inner_optimizer), + Optim.Options(store_trace = true, extended_trace = true, iterations=500), + autodiff = :forward +) + + +p00 = heatmap(sample_data, aspect_ratio=1.0, clim=(0.0, 1.0), title="Sample data", legend = :none); +p01 = heatmap(fitting_data, aspect_ratio=1.0, clim=(0.0,1.0), title="Fitting data", legend = :none); +p02 = heatmap(f(Optim.minimizer(res)), aspect_ratio=1.0, clim=(0.0,1.0), title="estimated fit", legend = :none); +p03 = heatmap(fitting_data .- f(Optim.minimizer(res)), aspect_ratio=1.0, clim=(0.0,1.0), title="discrepancy", legend = :none); + +plot(p00, p01, p02, p03, layout=@layout([A B C D]), + framestyle=nothing, showaxis=false, + xticks=false, yticks=false, + size=(700, 300), + plot_title="True vals: $(true_vals)", plot_titlevspan=0.2 +) + +heatmap(fitting_data .- f(Optim.x_trace(res)[end]), aspect_ratio=1, clim=(0.0, 1.0)) + +""" + +res = optimize( + loss, init_x, + LBFGS(), + Optim.Options(store_trace=true, extended_trace=true, iterations=500), + autodiff = :forward + ) +""" + +trace = Optim.trace(res); +trace + + + + +Optim.minimizer(res) +Optim.f_trace(res) +Optim.x_trace(res) +Optim.converged(res) +Optim.g_norm_trace(res) +Optim.g_calls(res) + +loss(Optim.x_trace(res)[end]) +ForwardDiff.gradient(loss, Optim.x_trace(res)[end]) + + + +anim = @animate for i1 in 1:length(Optim.x_trace(res)) + + heatmap(fitting_data .- f(Optim.x_trace(res)[i1]), + aspect_ratio=1, + clim=(0.0, 1.0), + dpi=300 + ) + title!("iteration: $(Int(i1))/$(length(Optim.x_trace(res))), + estimation: $(Optim.x_trace(res)[i1]) + true vals : $(true_vals)") + +end; + +gif(anim, "anim1.mp4", fps=5) diff --git a/examples/perform_random_optim.jl b/examples/perform_random_optim.jl new file mode 100644 index 0000000..b7a1b69 --- /dev/null +++ b/examples/perform_random_optim.jl @@ -0,0 +1,176 @@ +using DataToFunctions +using Optim, StaticArrays, LinearAlgebra +using Zygote +using ForwardDiff, LineSearches, Plots, Printf +using ProfileView, Profile +using Random, Distributions + +Random.seed!(123) + +# to show all the numbers in 2 decimals format +Base.show(io::IO, f::Float64) = @printf(io, "%.2f", f) + +### +# creating the random matrix in size = (11, 12) +sample_data = rand(11, 12) + +# creating the lower and upper bounds of the fitting variables (shift and scaling) +# shift can not be higher than the size of the array, +# scale can not be lower than zero or higher than size of the data, the latter causes the resulting array to be just one pixel +lower = [-1*size(sample_data)[1], -1*size(sample_data)[2], 0.0, 0.0] +upper = [size(sample_data)[1], size(sample_data)[2], size(sample_data)[1], size(sample_data)[2]] + +# preparing the function to fit +f = get_function(sample_data; super_sampling=2, extrapolation_bc=0.0); + +# assigning a scale (multiplier) to the range of true values, wedo not want that the parameters of +# function to be near the limits and cause strange behavior of the optimization +scale_range = 4.0 + +# true values of the fitting are random for the repeatibility +true_vals = (rand(4) .* (upper .- lower)/scale_range ) .+ (lower/scale_range) #[3.0, 5.5, 1.85, 0.6] + +# create the fitting data and adding random noise with scale of 1/10 +d = Normal() +noise = rand(d, 11, 12) + + +fitting_data = f(true_vals) #.+ noise / 10.0 +heatmap(fitting_data, aspect_ratio=1.0) +savefig("fitting_data_without_noise.png") + +# create the loss function and using the Julia's multiple dispatch +# because the gradients function is required just one input (can be vector) +loss(p, z) = sum(abs2.(f(p, z) .- fitting_data)) +loss(p2::Vector{Tuple{Float64, Float64}}) = loss(p2[1], p2[2]) +loss(p3) = loss([p3[1], p3[2]], [p3[3], p3[4]]) + + + + +# initialization of the LBFGS optimizer +inner_optimizer = LBFGS(; m=1, linesearch=LineSearches.BackTracking(order=2)) + + + +function perform_optim_mthr(loss, n_walkers) + """ + defining a function to survey the parameter space to neglect the local + minima and find the global maxima + + loss: the loss function + n_walkers: number of random initial parameter estimation + """ + # allocating the estimation array: + # consists of four parameters [1:4] and the minimum loss function of them [5] + est_m = zeros(n_walkers, 5) + + #x_tr = Array{Any} + #res = Array{Optim.MultivariateOptimizationResults{}}(undef, n_walkers, 1) + #res = Optim.MultivariateOptimizationResults{} + + # defining the random initial parameter values + walkers = (rand(4, n_walkers) .* (upper .- lower)/scale_range ) .+ (lower/scale_range) + + # main loop to do the optimization for each of the initial parameter values + # it uses the Threads to distribute the for loop to each thread + # note that in the settings.json the Julia is started with 16 threads + Threads.@threads for i in 1:size(walkers)[2] + res = optimize( + loss, + lower, upper, # the limits (simple box constraints) + walkers[:, i], + Fminbox(inner_optimizer), # assigning the limits of fitting (simple constraints) along with the LBFGS + Optim.Options(store_trace = true, extended_trace = true, iterations=500), + autodiff = :forward + ); + #x_tr = Optim.x_trace(res) + + # saving each 4 parameters of the fit and the minimum loss function + est_m[i, 1:4] .= Optim.minimizer(res)#x_tr[end] + est_m[i, 5] = minimum(res)#loss(x_tr[end]) + end + # saving the parameters of the minimum of the loss function + ans_m = est_m[argmin(est_m[:, 5]), :] + return est_m, ans_m +end + +#@profile est_m, ans_m = perform_optim_mthr(loss, 10000) + + +# first time: 4.749537 seconds (31.85 M allocations: 3.468 GiB, 8.50% gc time, 88.55% compilation time) +# 2nd time: 0.645307 seconds (12.68 M allocations: 2.525 GiB, 56.30% gc time) +# changing the fitting_data (noise values) : 0.448227 seconds (9.90 M allocations: 1.984 GiB, 27.55% compilation time: 38% of which was recompilation) +@time est_m, ans_m = perform_optim_mthr(loss, 500); + + +println(string(true_vals) * "\n" * string(ans_m)) + +# [2.23, -1.16, 1.26, 2.59] +# [2.26, -1.15, 1.27, 2.59, 0.41] + + +p00 = heatmap(sample_data, aspect_ratio=1.0, clim=(0.0, 1.0), title="Sample data", legend = :none); +p01 = heatmap(fitting_data, aspect_ratio=1.0, clim=(0.0,1.0), title="Fitting data", legend = :none); +p02 = heatmap(f(ans_m[1:4]), aspect_ratio=1.0, clim=(0.0,1.0), title="estimated fit", legend = :none); +p03 = heatmap(fitting_data .- f(ans_m[1:4]), aspect_ratio=1.0, clim=(0.0,1.0), title="discrepancy", legend = :none); + +plot(p00, p01, p02, p03, layout=@layout([A B C D]), + framestyle=nothing, showaxis=false, + xticks=false, yticks=false, + size=(700, 300), + plot_title="True vals: $(true_vals)", plot_titlevspan=0.2 +) +savefig("Output_mth.png") + + +plot( + heatmap(fitting_data .- f(ans_m[1:4]), aspect_ratio=1.0, clim=(0.0, 0.3), title="discrepancy", legend = :none), + heatmap(noise / 10.0 , aspect_ratio=1.0, clim=(0.0, 0.3), title="noise", legend = :none), + layout=@layout([A B]), + framestyle=nothing, showaxis=false, + xticks=false, yticks=false, + size=(700, 300), + plot_title="Noise comparison", plot_titlevspan=0.2 +) +savefig("Discrepancy.png") + +p12 = plot(fitting_data[1, :], color="black", legend=:none); +t = fitting_data[1, :] +for i in 2:size(fitting_data)[1] + t .+= fitting_data[i, :] + plot!(p12, fitting_data[i, :], color="black", legend=:none) +end +display(p12) + +plot(t) + + +function perform_optim_sthr(loss, n_walkers=1000) + est = Array{Float64, 2}(undef, n_walkers, 5) + + walkers = (rand(4, n_walkers) .* (upper .- lower)/scale_range ) .+ (lower/scale_range) + for i in 1:size(walkers)[2] + res = optimize( + loss, + lower, upper, + walkers[:, i], + Fminbox(inner_optimizer), + Optim.Options(store_trace = true, extended_trace = true, iterations=500), + autodiff = :forward + ); + x_tr = Optim.x_trace(res) + est[i, 1:4] .= x_tr[end] + est[i, 5] = loss(x_tr[end]) + end + ans1 = est[argmin(est[:, 5]), :] + return est, ans1 +end + + +@time est_s, ans_s = perform_optim_sthr(loss, 20000); + +println(string(true_vals) * "\n" * string(ans_s)) +# [1.07, -2.63, 0.78, 1.65] +# [1.02, -2.60, 0.77, 1.66, 0.46] + diff --git a/examples/star_fitting.jl b/examples/star_fitting.jl new file mode 100644 index 0000000..0a0fd2c --- /dev/null +++ b/examples/star_fitting.jl @@ -0,0 +1,85 @@ +using DataToFunctions +using Optim, StaticArrays, LinearAlgebra +using Zygote +using ForwardDiff, LineSearches, Plots, Printf +using View5D +using Distributions +using Plots + +using Rotations +using CoordinateTransformations + +Base.show(io::IO, f::Float64) = @printf(io, "%.3f", f) + +# defining the mean and the varixance of the test normal (Gaussian) distribution +μ = [0, 0] +Σ = [2 0.0; + 0.0 2] + +Σ_d = [2 1.5; + 1.5 2] + +# initializing the multivariate normal distribution +p = MvNormal(μ, Σ) +p_d = MvNormal(μ, Σ_d) + +# size of the test array to fit +size_arr = 12.0 + +# this part of the code is to define the sample array based on a 2D normal distribution +X = -1*size_arr/2.0:1*size_arr/2.0 +Y = -1*size_arr/2.0:1*size_arr/2.0 + +z = [pdf(p, [x,y]) for y in Y, x in X] +z_d = [pdf(p_d, [x,y]) for y in Y, x in X] + +@vv z + +heatmap(z, aspect_ratio=1) +heatmap(z_d, aspect_ratio=1) + +# setting a typical values for the shift (1:2) and scale (3:4) +true_vals = [1.1, -1.5, 0.75, 1.5] + +# normalizing the sample data +sample_data = z./maximum(z) +sample_data_d = z_d./maximum(z_d) + +# converting the data to function (DataToFunctions.get_function) +f = get_function(sample_data; super_sampling=2, extrapolation_bc=0.0); +f_d = get_function(sample_data_d; super_sampling=2, extrapolation_bc=0.0); + +# adding some scaled random noise to the fitting data +fitting_data = f(true_vals) .+ rand(13, 13)./10.0 + +# @vv fitting_data + +# defining the loss function based on the gaussian noise +loss(p) = sum(abs2.(f(p) .- fitting_data)) + +#loss(p3) = loss([p3[1], p3[2]], [p3[3], p3[4]]) + +heatmap(fitting_data, aspect_ratio=1) + +# perform the main fit to the fitting data by minimizing the loss function +output = perform_fit(loss, fitting_data) + +# plotting the output of the fitting pocedure for further illustration +begin + p00 = heatmap(sample_data, aspect_ratio=1.0, clim=(0.0, 1.0), title="Sample data", legend = :none); + p01 = heatmap(fitting_data, aspect_ratio=1.0, clim=(0.0,1.0), title="Fitting data", legend = :none); + p02 = heatmap(f(output), aspect_ratio=1.0, clim=(0.0,1.0), title="estimated fit", legend = :none); + p03 = heatmap(fitting_data .- f(output), aspect_ratio=1.0, clim=(0.0, 0.3), title="discrepancy", legend = :none); + + plot(p00, p01, p02, p03, layout=@layout([A B C D]), + framestyle=nothing, showaxis=false, + xticks=false, yticks=false, + size=(700, 300), + plot_title="True vals: $(true_vals) + est vals: $(output)", + plot_titlevspan=0.25 + ) +end + +# comparing the true values to the best fitting parameters +println(string(true_vals) * "\n" * string(output)) diff --git a/examples/star_fitting_loop.jl b/examples/star_fitting_loop.jl new file mode 100644 index 0000000..7d2a215 --- /dev/null +++ b/examples/star_fitting_loop.jl @@ -0,0 +1,107 @@ +using DataToFunctions +using Optim, StaticArrays, LinearAlgebra +using Zygote +using ForwardDiff, LineSearches, Plots, Printf +using View5D +using Distributions +using Plots + + +Base.show(io::IO, f::Float64) = @printf(io, "%.3f", f) + +# defining the mean and the varixance of the test normal (Gaussian) distribution +μ = [0, 0] +Σ = [2 0.0; + 0.0 2] + +Σ_d = [2 1.5; + 1.5 2] + +# initializing the multivariate normal distribution +p = MvNormal(μ, Σ) + +# size of the test array to fit +size_arr = 12.0 + +# this part of the code is to define the sample array based on a 2D normal distribution +X = -1*size_arr/2.0:1*size_arr/2.0 +Y = -1*size_arr/2.0:1*size_arr/2.0 + +z = [pdf(p, [x,y]) for y in Y, x in X] + +@vv z + +heatmap(z, aspect_ratio=1) + +# setting a typical values for the shift (1:2) and scale (3:4) +# true_vals = [1.15, -0.73, 2.1, 0.9, 0.0, 0.0, pi/6] +true_vals = [0.0, 0.0, 1.0, 2.0, 0.0, 0.0, pi/6] + +# normalizing the sample data +sample_data = z./maximum(z) + +# converting the data to function (DataToFunctions.get_function) +f_general = get_function_general(sample_data; super_sampling=1);#, extrapolation_bc=0.0); +# f_d = get_function_loop(sample_data_d; super_sampling=1);#, extrapolation_bc=0.0); + +# adding some scaled random noise to the fitting data +fitting_data = f_general(true_vals) .+ rand(13, 13)./10.0 + +# @vv fitting_data + +# defining the loss function based on the gaussian noise +loss(p) = sum(abs2.(f_general(p) .- fitting_data)) +# loss(x) = loss(x::AbstractVector{T} where T) + +# loss(p3) = loss([p3[1], p3[2], p3[3], p3[4], p3[5], p3[6], p3[7]]) + +heatmap(fitting_data, aspect_ratio=1) + +# perform the main fit to the fitting data by minimizing the loss function +output, res = perform_fit_general(loss, fitting_data) + +# plotting the output of the fitting pocedure for further illustration +begin + p00 = heatmap(sample_data, aspect_ratio=1.0, clim=(0.0, 1.0), title="Sample data", legend = :none); + p01 = heatmap(fitting_data, aspect_ratio=1.0, clim=(0.0,1.0), title="Fitting data", legend = :none); + p02 = heatmap(f_general(output), aspect_ratio=1.0, clim=(0.0,1.0), title="estimated fit", legend = :none); + p03 = heatmap(fitting_data .- f_general(output), aspect_ratio=1.0, clim=(0.0, 0.3), title="discrepancy", legend = :none); + + plot(p00, p01, p02, p03, layout=@layout([A B C D]), + framestyle=nothing, showaxis=false, + xticks=false, yticks=false, + size=(1200, 500), + plot_title="True vals: $(true_vals) + est vals: $(output)", + plot_titlevspan=0.25 + ) +end + +# comparing the true values to the best fitting parameters +println(string(true_vals) * "\n" * string(output)) + + + +anim = @animate for i1 in 1:length(Optim.x_trace(res)) + + begin + p00 = heatmap(sample_data, aspect_ratio=1.0, clim=(0.0, 1.0), title="Sample data", legend = :none); + p01 = heatmap(fitting_data, aspect_ratio=1.0, clim=(0.0,1.0), title="Fitting data", legend = :none); + p02 = heatmap(f_general(Optim.x_trace(res)[i1]), aspect_ratio=1.0, clim=(0.0,1.0), title="estimated fit", legend = :none); + p03 = heatmap(fitting_data .- f_general(Optim.x_trace(res)[i1]), aspect_ratio=1.0, clim=(0.0, 0.3), title="discrepancy", legend = :none); + + plot(p00, p01, p02, p03, layout=@layout([A B C D]), + framestyle=nothing, showaxis=false, + xticks=false, yticks=false, + size=(1200, 500), + plot_title="iteration: $(Int(i1))/$(length(Optim.x_trace(res))), + estimation: $(Optim.x_trace(res)[i1]) + true vals : $(true_vals)", + plot_titlevspan=0.25 + ) + end + + +end; + +gif(anim, "examples/anim_general_1.mp4", fps=20) \ No newline at end of file diff --git a/examples/star_rotation.jl b/examples/star_rotation.jl new file mode 100644 index 0000000..5704ec0 --- /dev/null +++ b/examples/star_rotation.jl @@ -0,0 +1,90 @@ +using DataToFunctions +using Optim, StaticArrays, LinearAlgebra, FourierTools +using Zygote +using ForwardDiff, LineSearches, Plots, Printf +using View5D +using Distributions +using Plots + +using Rotations +using CoordinateTransformations + +using Interpolations + +function get_function2(data::AbstractArray; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) + new_size = super_sampling.*size(data) + upsampled = fftshift(resample(ifftshift(data), new_size)) + + # return upsampled + # itp = LinearInterpolation(axes(upsampled), upsampled, extrapolation_bc=extrapolation_bc); + interpolation = Interpolations.interpolate(upsampled, interp_type) + interpolation = extrapolate(interpolation, extrapolation_bc) + # center of the original data (too keep the axis and number of datapointsi dentical to the original) + center_orig = (size(data) .÷2 .+1) + # create zero-centered original ranges (== axes) + zero_axes = Tuple(ax .- c for (ax, c) in zip(axes(data), center_orig)) + # center of the upsampled data. This is where to access the upsampled data + cen_vec = SVector(size(data)./ 2.0) + + function transform_axes(t::SVector, mat::SMatrix, cen_vec::SVector, shift_vec::SVector) + + return (mat * (t - cen_vec)) + cen_vec - shift_vec + + end + + function zoomed(shift_vec, zoom, theta) + zoom = zoom .* super_sampling + # # careful: The center of the original data is not at the expected position! But rather at: + # center_upsamp = new_size .÷2 .+1 # ((center_orig .-1) .*super_sampling .+1) # new_size .÷2 .+1 + # scaled_axes = ((ax.-myc) .* z .+ cen for (ax, myc, cen, z) in zip(zero_axes, shift, center_upsamp, zoom)) + + # #sh_x, sh_y = 0.0, 0.0 + s_x, s_y = zoom + theta = theta[1] + + rot_mat = @SMatrix [cos(theta) -1.0*sin(theta); sin(theta) cos(theta)]; + scale_mat = @SMatrix [s_x 0.0; 0.0 s_y]; + mat = rot_mat * scale_mat + + return interpolation.(transform_axes.(SVector.(Tuple.(CartesianIndices(data))), mat, cen_vec, SVector(shift_vec[1], shift_vec[2]))...) + # return extrapolate(scale(interpolation, scaled_axes...), extrapolation_bc) + end + zoomed(p) = zoomed([p[1], p[2]], [p[3], p[4]], p[5]); + # zoomed([p[1], p[2]], [p[3], p[4]], p[5]) = zoomed[p] + return zoomed + +end + +# defining the mean and the varixance of the test normal (Gaussian) distribution +μ = [0, 0] +Σ = [2 0.0; + 0.0 2] + +Σ_d = [2 1.5; + 1.5 2] + +# initializing the multivariate normal distribution +p = MvNormal(μ, Σ) +p_d = MvNormal(μ, Σ_d) + +# size of the test array to fit +size_arr = 12.0 + +# this part of the code is to define the sample array based on a 2D normal distribution +X = -1*size_arr/2.0:1*size_arr/2.0 +Y = -1*size_arr/2.0:1*size_arr/2.0 + +z = [pdf(p, [x,y]) for y in Y, x in X] +heatmap(z, aspect_ratio=1) + + + + +true_vals = [1.1, -1.5, 0.75, 1.5, pi/2] + + +sample_data = z./maximum(z) +f = get_function2(sample_data; super_sampling=2, extrapolation_bc=0.0); +f(true_vals) + +heatmap(f(true_vals), aspect_ratio=1.0, clim=(0.0,1.0), legend = :none); diff --git a/src/DataToFunctions.jl b/src/DataToFunctions.jl index 07ae703..5531e43 100644 --- a/src/DataToFunctions.jl +++ b/src/DataToFunctions.jl @@ -1,7 +1,12 @@ module DataToFunctions using Interpolations using FourierTools -export get_function +using Optim, LineSearches +using Revise +using StaticArrays +using Zygote + +export get_function, perform_fit, get_function_general, perform_fit_general """ get_function(data::AbstractArray; super_sampling=2, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) @@ -23,7 +28,7 @@ This is useful for fitting with a function which is itself defined by measured d function get_function(data::AbstractArray; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) new_size = super_sampling.*size(data) upsampled = fftshift(resample(ifftshift(data), new_size)) - @show upsampled + # @show upsampled # return upsampled # itp = LinearInterpolation(axes(upsampled), upsampled, extrapolation_bc=extrapolation_bc); interpolation = Interpolations.interpolate(upsampled, interp_type) @@ -45,6 +50,7 @@ function get_function(data::AbstractArray; super_sampling=2, extrapolation_bc=ze return zoomed + zoomed(p) = zoomed([p[1], p[2]], [p[3], p[4]]) # return (pos) -> interp_linear((center .+ pos)...) # fitp(t) = interp_linear(t...) # @time res1 = fitp.(tcoords); # 1 sec @@ -99,4 +105,95 @@ function perform_fit(loss_function, fitting_data::AbstractArray) return Optim.minimizer(res) end + + +function get_function_general(data::AbstractArray; super_sampling=1) + # new_size = super_sampling.*size(data) + # upsampled = fftshift(resample(ifftshift(data), new_size)) + + itp = extrapolate(interpolate(data, BSpline(Linear())), 0.0); + + function f(t::SVector, matrix_c::SMatrix) + return matrix_c * t + end + + function interpolated(p) + + out = similar(data) + x_tmp = Zygote.Buffer(out) + + x_cen, y_cen = (size(data) .÷ 2.0 .+1) + # x_cen_up, y_cen_up = (size(upsampled) .÷ 2.0 .+ 1.0) + + # scale_x *= super_sampling + # scale_y *= super_sampling + # @show p[1] + rot_mat = @SMatrix [cos(p[7]) -1.0*sin(p[7]) 0.0; sin(p[7]) cos(p[7]) 0.0; 0.0 0.0 1.0]; + shear_mat = @SMatrix [1.0 p[5] 0.0; p[6] 1.0 0.0; 0.0 0.0 1.0]; + scale_mat = @SMatrix [1/p[3] 0.0 0.0; 0.0 1/p[4] 0.0; 0.0 0.0 1.0]; + shift_mat = @SMatrix [1.0 0.0 -1*p[1]; 0.0 1.0 -1*p[2]; 0.0 0.0 1.0]; + t_to_origin = @SMatrix [1.0 0.0 1*x_cen; 0.0 1.0 y_cen; 0.0 0.0 1.0]; + t_to_center = @SMatrix [1.0 0.0 -1.0*x_cen; 0.0 1.0 -1.0*y_cen; 0.0 0.0 1.0]; + # t_orig_upsampled = @SMatrix [1.0 0.0 -1.0*x_cen_up; 0.0 1.0 -1.0*y_cen_up; 0.0 0.0 1.0] + + matrix_c = t_to_origin * scale_mat * shear_mat * rot_mat *shift_mat * t_to_center + + + + for I in CartesianIndices(data) + x_tmp[I] = itp(f(SVector(Tuple(I)..., 1), matrix_c)[1:2]...) + end + + return copy(x_tmp) + end + + return interpolated +end + + +""" + perform_fit(loss_function, fitting_data::AbstractArray) + +Performs a fit to the fitting data using a loss function defined by the user + +# Arguments +`loss_function`: User-defined loss function which is minimized +`fitting_data`: The data which is being fitted + +# Returns +a vector of 4 parameters: first 2 for the shift and other 2 for the scaling factors + +# Example +there is an example of this function in the `examples/star_fitting.jl` + +""" +function perform_fit_general(loss_function, fitting_data::AbstractArray) + # guess the shift parameters by taking the maximum values of the array and + # centering the positions + a, b = Tuple(argmax(fitting_data)) .- size(fitting_data) ./2.0 .- 1.0 + + # assigning the initial parameter estimates + init_x = [a, b, 1.0, 1.0, 0.0, 0.0, 0.0] + + # setting the lower and upper boundary of the parameter values based on the limits of the shift and scaling + lower = [-1*size(fitting_data)[1], -1*size(fitting_data)[2], 0.0, 0.0, 0.0, 0.0, 0.0] + upper = [size(fitting_data)[1], size(fitting_data)[2], size(fitting_data)[1], size(fitting_data)[2], 5.0, 5.0, pi] + + # initializing the LBFGS optimizer + inner_optimizer = LBFGS(; m=1, linesearch=LineSearches.BackTracking(order=2)) + + # Computer, Optimize! :D + res = optimize( + loss_function, + lower, upper, + init_x, + Fminbox(inner_optimizer), + Optim.Options(store_trace = true, extended_trace = true, iterations=500), + autodiff = :finite + ) + + # return the estimated parameters + return Optim.minimizer(res), res +end + end # module DataToFunctions \ No newline at end of file From 0c828e5dfef9d55333f9d8529687572b26680679 Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Fri, 2 Jun 2023 13:10:33 +0200 Subject: [PATCH 03/44] Updated the helpa and examples --- examples/star_fitting_general.jl | 107 +++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 examples/star_fitting_general.jl diff --git a/examples/star_fitting_general.jl b/examples/star_fitting_general.jl new file mode 100644 index 0000000..47bb6f8 --- /dev/null +++ b/examples/star_fitting_general.jl @@ -0,0 +1,107 @@ +using DataToFunctions +using Optim, StaticArrays, LinearAlgebra +using Zygote +using ForwardDiff, LineSearches, Plots, Printf +using View5D +using Distributions +using Plots + + +Base.show(io::IO, f::Float64) = @printf(io, "%.3f", f) + +# defining the mean and the varixance of the test normal (Gaussian) distribution +μ = [0, 0] +Σ = [2 0.0; + 0.0 2] + +Σ_d = [2 1.5; + 1.5 2] + +# initializing the multivariate normal distribution +p = MvNormal(μ, Σ) + +# size of the test array to fit +size_arr = 12.0 + +# this part of the code is to define the sample array based on a 2D normal distribution +X = -1*size_arr/2.0:1*size_arr/2.0 +Y = -1*size_arr/2.0:1*size_arr/2.0 + +z = [pdf(p, [x,y]) for y in Y, x in X] + +# @vv z + +heatmap(z, aspect_ratio=1) + +# setting a typical values for the shift (1:2) and scale (3:4) +# true_vals = [1.15, -0.73, 2.1, 0.9, 0.0, 0.0, pi/6] +true_vals = [2.3, -1.2, 0.9, 2.1, 0.1, 0.05, pi/6] + +# normalizing the sample data +sample_data = z./maximum(z) + +# converting the data to function (DataToFunctions.get_function) +f_general = get_function_general(sample_data; super_sampling=1);#, extrapolation_bc=0.0); +# f_d = get_function_loop(sample_data_d; super_sampling=1);#, extrapolation_bc=0.0); + +# adding some scaled random noise to the fitting data +fitting_data = f_general(true_vals) .+ rand(13, 13)./10.0 +heatmap(fitting_data, aspect_ratio=1) + +# @vv fitting_data + +# defining the loss function based on the gaussian noise +loss(p) = sum(abs2.(f_general(p) .- fitting_data)) +# loss(x) = loss(x::AbstractVector{T} where T) + +# loss(p3) = loss([p3[1], p3[2], p3[3], p3[4], p3[5], p3[6], p3[7]]) + + +# perform the main fit to the fitting data by minimizing the loss function +output, res = perform_fit_general(loss, fitting_data) + +# plotting the output of the fitting pocedure for further illustration +begin + p00 = heatmap(sample_data, aspect_ratio=1.0, clim=(0.0, 1.0), title="Sample data", legend = :none); + p01 = heatmap(fitting_data, aspect_ratio=1.0, clim=(0.0,1.0), title="Fitting data", legend = :none); + p02 = heatmap(f_general(output), aspect_ratio=1.0, clim=(0.0,1.0), title="estimated fit", legend = :none); + p03 = heatmap(fitting_data .- f_general(output), aspect_ratio=1.0, clim=(0.0, 0.3), title="discrepancy", legend = :none); + + plot(p00, p01, p02, p03, layout=@layout([A B C D]), + framestyle=nothing, showaxis=false, + xticks=false, yticks=false, + size=(1200, 500), + plot_title="True vals: $(true_vals) + est vals: $(output)", + plot_titlevspan=0.25 + ) +end + +# comparing the true values to the best fitting parameters +println(string(true_vals) * "\n" * string(output)) + + + +anim = @animate for i1 in 1:length(Optim.x_trace(res)) + + begin + p00 = heatmap(sample_data, aspect_ratio=1.0, clim=(0.0, 1.0), title="Sample data", legend = :none); + p01 = heatmap(fitting_data, aspect_ratio=1.0, clim=(0.0,1.0), title="Fitting data", legend = :none); + p02 = heatmap(f_general(Optim.x_trace(res)[i1]), aspect_ratio=1.0, clim=(0.0,1.0), title="estimated fit", legend = :none); + p03 = heatmap(fitting_data .- f_general(Optim.x_trace(res)[i1]), aspect_ratio=1.0, clim=(0.0, 0.3), title="discrepancy", legend = :none); + + plot(p00, p01, p02, p03, layout=@layout([A B C D]), + framestyle=nothing, showaxis=false, + xticks=false, yticks=false, + size=(1200, 500), + plot_title="iteration: $(Int(i1))/$(length(Optim.x_trace(res))), + estimation: $(Optim.x_trace(res)[i1]) + true vals : $(true_vals)", + plot_titlevspan=0.25 + ) + end + + +end; + +gif(anim, "examples/anim_general_3.mp4", fps=4) \ No newline at end of file From e09916ba56b4123e3a33e1c674a778bc3f1eb068 Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Fri, 2 Jun 2023 13:15:45 +0200 Subject: [PATCH 04/44] Updated the Project and removed the additional dep --- .gitignore | 4 ++ Project.toml | 5 -- examples/anim_general_1.mp4 | Bin 163373 -> 0 bytes examples/star_fitting_loop.jl | 107 ---------------------------------- src/DataToFunctions.jl | 45 +++++++++----- 5 files changed, 34 insertions(+), 127 deletions(-) delete mode 100644 examples/anim_general_1.mp4 delete mode 100644 examples/star_fitting_loop.jl diff --git a/.gitignore b/.gitignore index 29126e4..e688be6 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,7 @@ docs/site/ # committed for packages, but should be committed for applications that require a static # environment. Manifest.toml + + +*.mp4 +*.png diff --git a/Project.toml b/Project.toml index d072ef9..0768e27 100644 --- a/Project.toml +++ b/Project.toml @@ -5,10 +5,7 @@ version = "0.1.0" [deps] Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" -BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" -CoordinateTransformations = "150eb455-5306-5404-9cee-2592286d6298" -Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" FourierTools = "b18b359b-aebc-45ac-a139-9c0ccbb2871e" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" @@ -16,7 +13,5 @@ JLArrays = "27aeb0d3-9eb9-45fb-866b-73c2ecf80fcb" LineSearches = "d3d80556-e9d4-5f37-9878-2ab0fcc64255" Optim = "429524aa-4258-5aef-a3af-852621145aeb" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" -Rotations = "6038ab10-8711-5258-84ad-4b1120ba62dc" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" -View5D = "90d841e0-6953-4e90-9f3a-43681da8e949" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" diff --git a/examples/anim_general_1.mp4 b/examples/anim_general_1.mp4 deleted file mode 100644 index 50991bd283e3483b969f659b0a928317b93b0016..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 163373 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∈)(qNK|ZT9 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

W7lq8Gnf4n@yyPv_Q{EGPrVH*Pf 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 (yWUgbRB0Ho;^<+P)Zs@~P1o5fI9od=) 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 Date: Thu, 25 Jan 2024 16:55:09 +0100 Subject: [PATCH 05/44] N-dimensional generalization --- .DS_Store | Bin 0 -> 6148 bytes Project.toml | 4 +- examples/Project.toml | 15 ++++++ examples/star_fitting.jl | 23 +++++---- examples/star_fitting_general.jl | 61 ++++++++++++++++------- src/DataToFunctions.jl | 80 ++++++++++++++++++++++++++----- 6 files changed, 142 insertions(+), 41 deletions(-) create mode 100644 .DS_Store create mode 100644 examples/Project.toml diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..161f88fd534a5482781be329a55511ed6987dec9 GIT binary patch literal 6148 zcmeHKUrPc(5T8}c6$yMO@bREmAybi}m&5D}bWsl#c9&8|=dF0!1tQ#YeW5-{pQoAK zH8krrLS|t1H#>jsnBN||Hvk|y)BYhq1pp)t!dwZbAB6lg)+A>w1w^5rF@gvNkOXNm zT8n1Ge`J96PU4=ufDc1h+P@%D0s7v9Q4}VVdi_J>@`bJKqAbd?y!9Sc>P`H~H0k)` zYg%0>6$K034=%$&+N}JbsvcO=lD%8kile*M!EuL!gyJ%T mUn!{Qs~BVHDz4(tf_8@#MAu?w5G^SDBcN&Ei5d7)20j6$iHJ)8 literal 0 HcmV?d00001 diff --git a/Project.toml b/Project.toml index 0768e27..8a007ec 100644 --- a/Project.toml +++ b/Project.toml @@ -4,13 +4,11 @@ authors = ["RainerHeintzmann "] version = "0.1.0" [deps] -Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" -ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" FourierTools = "b18b359b-aebc-45ac-a139-9c0ccbb2871e" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" -JLArrays = "27aeb0d3-9eb9-45fb-866b-73c2ecf80fcb" LineSearches = "d3d80556-e9d4-5f37-9878-2ab0fcc64255" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Optim = "429524aa-4258-5aef-a3af-852621145aeb" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" diff --git a/examples/Project.toml b/examples/Project.toml new file mode 100644 index 0000000..f64fdc3 --- /dev/null +++ b/examples/Project.toml @@ -0,0 +1,15 @@ +[deps] +CoordinateTransformations = "150eb455-5306-5404-9cee-2592286d6298" +DataToFunctions = "64cfdffa-4d02-49ee-ae8b-a805370874f5" +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +LineSearches = "d3d80556-e9d4-5f37-9878-2ab0fcc64255" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +OhMyREPL = "5fb14364-9ced-5910-84b2-373655c76a03" +Optim = "429524aa-4258-5aef-a3af-852621145aeb" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +Rotations = "6038ab10-8711-5258-84ad-4b1120ba62dc" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +View5D = "90d841e0-6953-4e90-9f3a-43681da8e949" +Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" diff --git a/examples/star_fitting.jl b/examples/star_fitting.jl index 0a0fd2c..0bd63c0 100644 --- a/examples/star_fitting.jl +++ b/examples/star_fitting.jl @@ -11,20 +11,23 @@ using CoordinateTransformations Base.show(io::IO, f::Float64) = @printf(io, "%.3f", f) -# defining the mean and the varixance of the test normal (Gaussian) distribution + +# size of the test array to fit +size_arr = 600.0 + +# defining the mean and the variance of the test normal (Gaussian) distribution μ = [0, 0] -Σ = [2 0.0; - 0.0 2] +Σ = [size_arr/2.0 0.0; + 0.0 size_arr/2.0] -Σ_d = [2 1.5; - 1.5 2] +Σ_d = [size_arr/0.5 1.5; + 1.5 size_arr/6.0] # initializing the multivariate normal distribution p = MvNormal(μ, Σ) p_d = MvNormal(μ, Σ_d) -# size of the test array to fit -size_arr = 12.0 + # this part of the code is to define the sample array based on a 2D normal distribution X = -1*size_arr/2.0:1*size_arr/2.0 @@ -33,7 +36,7 @@ Y = -1*size_arr/2.0:1*size_arr/2.0 z = [pdf(p, [x,y]) for y in Y, x in X] z_d = [pdf(p_d, [x,y]) for y in Y, x in X] -@vv z +#@vv z heatmap(z, aspect_ratio=1) heatmap(z_d, aspect_ratio=1) @@ -50,7 +53,7 @@ f = get_function(sample_data; super_sampling=2, extrapolation_bc=0.0); f_d = get_function(sample_data_d; super_sampling=2, extrapolation_bc=0.0); # adding some scaled random noise to the fitting data -fitting_data = f(true_vals) .+ rand(13, 13)./10.0 +fitting_data = f(true_vals) .+ rand(size(z)...)./8.0 # @vv fitting_data @@ -59,7 +62,7 @@ loss(p) = sum(abs2.(f(p) .- fitting_data)) #loss(p3) = loss([p3[1], p3[2]], [p3[3], p3[4]]) -heatmap(fitting_data, aspect_ratio=1) +heatmap(fitting_data, aspect_ratio=1, title="Fitting data") # perform the main fit to the fitting data by minimizing the loss function output = perform_fit(loss, fitting_data) diff --git a/examples/star_fitting_general.jl b/examples/star_fitting_general.jl index 47bb6f8..9a37832 100644 --- a/examples/star_fitting_general.jl +++ b/examples/star_fitting_general.jl @@ -3,16 +3,21 @@ using Optim, StaticArrays, LinearAlgebra using Zygote using ForwardDiff, LineSearches, Plots, Printf using View5D -using Distributions +using Distributions, Rotations using Plots Base.show(io::IO, f::Float64) = @printf(io, "%.3f", f) +# size of the test array to fit +size_arr = 31.0 + +noise_level = 10.0; + # defining the mean and the varixance of the test normal (Gaussian) distribution μ = [0, 0] -Σ = [2 0.0; - 0.0 2] +Σ = [size_arr/1.2 0.0; + 0.0 size_arr/1.2] Σ_d = [2 1.5; 1.5 2] @@ -20,8 +25,7 @@ Base.show(io::IO, f::Float64) = @printf(io, "%.3f", f) # initializing the multivariate normal distribution p = MvNormal(μ, Σ) -# size of the test array to fit -size_arr = 12.0 + # this part of the code is to define the sample array based on a 2D normal distribution X = -1*size_arr/2.0:1*size_arr/2.0 @@ -31,21 +35,39 @@ z = [pdf(p, [x,y]) for y in Y, x in X] # @vv z -heatmap(z, aspect_ratio=1) + # setting a typical values for the shift (1:2) and scale (3:4) # true_vals = [1.15, -0.73, 2.1, 0.9, 0.0, 0.0, pi/6] -true_vals = [2.3, -1.2, 0.9, 2.1, 0.1, 0.05, pi/6] +# true_vals = [2.3, -1.2, 0.9, 2.1, 0.1, 0.05, pi/2, 2.0, 3.0] # normalizing the sample data -sample_data = z./maximum(z) +sample_data = Float32.(z./maximum(z)) +sample_data .+= rand(size(sample_data)...)./noise_level; +heatmap(sample_data, aspect_ratio=1) + + +x_cen, y_cen = (size(sample_data) .÷ 2.0 .+1) + +ang = rand((0.0:0.1:pi)) +rot_mat = [cos(ang) -1.0*sin(ang) 0.0; sin(ang) cos(ang) 0.0; 0.0 0.0 1.0]; + +shear_mat = [1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0]; +scale_mat = [1/1.2 0.0 0.0; 0.0 1/1.8 0.0; 0.0 0.0 1.0]; + +t_to_origin = [1.0 0.0 1*x_cen; 0.0 1.0 y_cen; 0.0 0.0 1.0]; +t_to_center = [1.0 0.0 -1.0*x_cen; 0.0 1.0 -1.0*y_cen; 0.0 0.0 1.0]; + +matrix_c = t_to_origin * scale_mat * shear_mat * rot_mat * t_to_center + + # converting the data to function (DataToFunctions.get_function) -f_general = get_function_general(sample_data; super_sampling=1);#, extrapolation_bc=0.0); +f_general = get_function_general_matrix(sample_data);#; super_sampling=1);#, extrapolation_bc=0.0); # f_d = get_function_loop(sample_data_d; super_sampling=1);#, extrapolation_bc=0.0); # adding some scaled random noise to the fitting data -fitting_data = f_general(true_vals) .+ rand(13, 13)./10.0 +fitting_data = f_general(matrix_c) .+ rand(size(sample_data)...)./100.0; heatmap(fitting_data, aspect_ratio=1) # @vv fitting_data @@ -58,27 +80,31 @@ loss(p) = sum(abs2.(f_general(p) .- fitting_data)) # perform the main fit to the fitting data by minimizing the loss function -output, res = perform_fit_general(loss, fitting_data) +@time output, res = perform_fit_general(loss, fitting_data) # plotting the output of the fitting pocedure for further illustration begin p00 = heatmap(sample_data, aspect_ratio=1.0, clim=(0.0, 1.0), title="Sample data", legend = :none); p01 = heatmap(fitting_data, aspect_ratio=1.0, clim=(0.0,1.0), title="Fitting data", legend = :none); - p02 = heatmap(f_general(output), aspect_ratio=1.0, clim=(0.0,1.0), title="estimated fit", legend = :none); + p02 = heatmap(f_general(reshape(output, ndims(fitting_data)+1, ndims(fitting_data)+1)), aspect_ratio=1.0, clim=(0.0,1.0), title="estimated fit", legend = :none); p03 = heatmap(fitting_data .- f_general(output), aspect_ratio=1.0, clim=(0.0, 0.3), title="discrepancy", legend = :none); plot(p00, p01, p02, p03, layout=@layout([A B C D]), framestyle=nothing, showaxis=false, xticks=false, yticks=false, size=(1200, 500), - plot_title="True vals: $(true_vals) - est vals: $(output)", - plot_titlevspan=0.25 + #plot_title="True vals: $(matrix_c) + #est vals: $(reshape(output, ndims(fitting_data)+1, ndims(fitting_data)+1))", + #plot_titlevspan=0.2 ) end + +matrix_c +reshape(output, ndims(fitting_data)+1, ndims(fitting_data)+1) # comparing the true values to the best fitting parameters -println(string(true_vals) * "\n" * string(output)) + +#println(matrix_c) * reshape(output, ndims(fitting_data)+1, ndims(fitting_data)+1) @@ -104,4 +130,5 @@ anim = @animate for i1 in 1:length(Optim.x_trace(res)) end; -gif(anim, "examples/anim_general_3.mp4", fps=4) \ No newline at end of file +gif(anim, "DataToFunctions.jl/examples/anim_general_generalized.mp4", fps=2) + diff --git a/src/DataToFunctions.jl b/src/DataToFunctions.jl index faa0901..d136f2a 100644 --- a/src/DataToFunctions.jl +++ b/src/DataToFunctions.jl @@ -3,10 +3,10 @@ using Interpolations using FourierTools using Optim, LineSearches using Revise -using StaticArrays +using StaticArrays, LinearAlgebra using Zygote -export get_function, perform_fit, get_function_general, perform_fit_general +export get_function, perform_fit, get_function_general, perform_fit_general, get_function_general_matrix """ get_function(data::AbstractArray; super_sampling=2, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) @@ -166,6 +166,63 @@ function get_function_general(data::AbstractArray; extrapolation_bc=0.0, interp_ end + +""" + get_function_general_matrix(data::AbstractArray; super_sampling=1, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) + +returns a function `interpolated(p)` which generates a transformed version of the original data. +This is useful for fitting with a function which is itself defined by measured data. + +# Arguments +`data`: The data to represent by the function `dat` +`extrapolation_bc`: The extrapolation boundary condition to select for values outside the range. + By default the value 0.0 is used. Other options are `Flat()`, or `Line()`, See the package `Interpolation` for details. +`interp_type`: The type of interpolation to use. See the package `Interpolation` for details. + +""" +function get_function_general_matrix(data::AbstractArray{T}; extrapolation_bc=0.0, interp_type=Interpolations.BSpline(Linear())) where T + # new_size = super_sampling.*size(data) + # upsampled = fftshift(resample(ifftshift(data), new_size)) + + # building the extraplation + interpolation object + itp = extrapolate(interpolate(data, interp_type), extrapolation_bc); + + # multiplying the transformation matrix + function f(t::AbstractVector{T}, matrix_c::AbstractMatrix{T}) where T + return matrix_c * t + end + + function interpolated(p::AbstractMatrix{T}) where T + + # init a new array for the output + out = similar(data, T) + + # x_cen, y_cen = (size(data) .÷ 2.0 .+1) + + # t_orig_upsampled = @SMatrix [1.0 0.0 -1.0*x_cen_up; 0.0 1.0 -1.0*y_cen_up; 0.0 0.0 1.0] + + # building the overall transformation matrix + matrix_c = SMatrix{ndims(data)+1, ndims(data)+1, T}(reshape(p, ndims(data)+1, ndims(data)+1)) + #print(eltype(matrix_c)) + # looping all over the catesian indedices of the input image, + # first ading a new value to its third dimenstion: 1.0, + # converting to the new indices using the transformation matrix and then, + # using the "itp" object, we build the transfomed image "out" + for I1 in CartesianIndices(data) + #print("INSIde the loop!!") + #print(eltype(SVector{ndims(data)+1, T}(Tuple(I1)..., 1))) + out[I1] = itp(f(SVector{ndims(data)+1, T}(Tuple(I1)..., 1.0), matrix_c)[1:2]...) + #print(eltype(out[I1])) + end + + return out + end + + return interpolated +end + + + """ perform_fit_general(loss_function, fitting_data::AbstractArray) @@ -185,14 +242,14 @@ there is an example of this function in the `examples/star_fitting_genaral.jl` function perform_fit_general(loss_function, fitting_data::AbstractArray) # guess the shift parameters by taking the maximum values of the array and # centering the positions - a, b = Tuple(argmax(fitting_data)) .- size(fitting_data) ./2.0 .- 1.0 - + ##a, b = Tuple(argmax(fitting_data)) .- size(fitting_data) ./2.0 .- 1.0 + #print("INSIDE!!! hehe") # assigning the initial parameter estimates - init_x = [a, b, 1.0, 1.0, 0.001, 0.001, 0.001] + init_x = reshape(Matrix(1.0*I, ndims(fitting_data)+1, ndims(fitting_data)+1), 1, 9) #[a, b, 1.0, 1.0, 0.001, 0.001, 0.001] # setting the lower and upper boundary of the parameter values based on their limits - lower = [-1*size(fitting_data)[1], -1*size(fitting_data)[2], 0.0, 0.0, 0.0, 0.0, 0.0] - upper = [size(fitting_data)[1], size(fitting_data)[2], size(fitting_data)[1], size(fitting_data)[2], 5.0, 5.0, pi] + ##lower = [-1*size(fitting_data)[1], -1*size(fitting_data)[2], 0.0, 0.0, 0.0, 0.0, 0.0] + ##upper = [size(fitting_data)[1], size(fitting_data)[2], size(fitting_data)[1], size(fitting_data)[2], 5.0, 5.0, pi] # initializing the LBFGS optimizer inner_optimizer = LBFGS(; m=1, linesearch=LineSearches.BackTracking(order=2)) @@ -200,11 +257,12 @@ function perform_fit_general(loss_function, fitting_data::AbstractArray) # Computer, Optimize! :D res = optimize( loss_function, - lower, upper, - init_x, - Fminbox(inner_optimizer), + init_x, + LBFGS(), + # lower, upper, + # Fminbox(inner_optimizer), Optim.Options(store_trace = true, extended_trace = true, iterations=500), - autodiff = :finite + autodiff = :forward ) # return the estimated parameters From 0062797a74dcf414ab8e7a98560eba0cd218e8a4 Mon Sep 17 00:00:00 2001 From: Hossein Date: Thu, 25 Jan 2024 17:06:54 +0100 Subject: [PATCH 06/44] Removing clutters --- .DS_Store | Bin 6148 -> 6148 bytes Project.toml | 2 -- examples/star_fitting_general.jl | 1 - src/DataToFunctions.jl | 6 +++--- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.DS_Store b/.DS_Store index 161f88fd534a5482781be329a55511ed6987dec9..9016df2d8f6274264a97ba6d1f323565feb07d99 100644 GIT binary patch delta 15 WcmZoMXffEZjETv}a`OtN7*PN#GX(qq delta 16 XcmZoMXffEZjA`-|X2;FXnPf!)Hxvc> diff --git a/Project.toml b/Project.toml index 8a007ec..6fbf134 100644 --- a/Project.toml +++ b/Project.toml @@ -7,8 +7,6 @@ version = "0.1.0" CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" FourierTools = "b18b359b-aebc-45ac-a139-9c0ccbb2871e" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" -LineSearches = "d3d80556-e9d4-5f37-9878-2ab0fcc64255" -LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Optim = "429524aa-4258-5aef-a3af-852621145aeb" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" diff --git a/examples/star_fitting_general.jl b/examples/star_fitting_general.jl index 9a37832..07f0eec 100644 --- a/examples/star_fitting_general.jl +++ b/examples/star_fitting_general.jl @@ -70,7 +70,6 @@ f_general = get_function_general_matrix(sample_data);#; super_sampling=1);#, ext fitting_data = f_general(matrix_c) .+ rand(size(sample_data)...)./100.0; heatmap(fitting_data, aspect_ratio=1) -# @vv fitting_data # defining the loss function based on the gaussian noise loss(p) = sum(abs2.(f_general(p) .- fitting_data)) diff --git a/src/DataToFunctions.jl b/src/DataToFunctions.jl index d136f2a..cb511fc 100644 --- a/src/DataToFunctions.jl +++ b/src/DataToFunctions.jl @@ -1,9 +1,9 @@ module DataToFunctions using Interpolations using FourierTools -using Optim, LineSearches +using Optim #, LineSearches using Revise -using StaticArrays, LinearAlgebra +using StaticArrays #, LinearAlgebra using Zygote export get_function, perform_fit, get_function_general, perform_fit_general, get_function_general_matrix @@ -252,7 +252,7 @@ function perform_fit_general(loss_function, fitting_data::AbstractArray) ##upper = [size(fitting_data)[1], size(fitting_data)[2], size(fitting_data)[1], size(fitting_data)[2], 5.0, 5.0, pi] # initializing the LBFGS optimizer - inner_optimizer = LBFGS(; m=1, linesearch=LineSearches.BackTracking(order=2)) + # inner_optimizer = LBFGS(; m=1, linesearch=LineSearches.BackTracking(order=2)) # Computer, Optimize! :D res = optimize( From c1acf27b7006e6f8f1b274e343900e29e3aa86f2 Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Mon, 26 Feb 2024 14:38:36 +0100 Subject: [PATCH 07/44] cleaning the main code --- Project.toml | 4 - examples/Project.toml | 2 + examples/star_fitting_general.jl | 200 +++++++++++++++++++++++-------- src/DataToFunctions.jl | 186 +++++----------------------- 4 files changed, 186 insertions(+), 206 deletions(-) diff --git a/Project.toml b/Project.toml index 6fbf134..8f6eb53 100644 --- a/Project.toml +++ b/Project.toml @@ -4,10 +4,6 @@ authors = ["RainerHeintzmann "] version = "0.1.0" [deps] -CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" FourierTools = "b18b359b-aebc-45ac-a139-9c0ccbb2871e" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" -Optim = "429524aa-4258-5aef-a3af-852621145aeb" -Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" -Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" diff --git a/examples/Project.toml b/examples/Project.toml index f64fdc3..3268c51 100644 --- a/examples/Project.toml +++ b/examples/Project.toml @@ -1,4 +1,5 @@ [deps] +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" CoordinateTransformations = "150eb455-5306-5404-9cee-2592286d6298" DataToFunctions = "64cfdffa-4d02-49ee-ae8b-a805370874f5" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" @@ -11,5 +12,6 @@ Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Rotations = "6038ab10-8711-5258-84ad-4b1120ba62dc" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990" View5D = "90d841e0-6953-4e90-9f3a-43681da8e949" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" diff --git a/examples/star_fitting_general.jl b/examples/star_fitting_general.jl index 07f0eec..ba1b3f8 100644 --- a/examples/star_fitting_general.jl +++ b/examples/star_fitting_general.jl @@ -5,19 +5,117 @@ using ForwardDiff, LineSearches, Plots, Printf using View5D using Distributions, Rotations using Plots - +using TestImages Base.show(io::IO, f::Float64) = @printf(io, "%.3f", f) + + +""" + perform_fit_general(loss_function, fitting_data::AbstractArray) + +Performs a fit to the fitting data using a loss function defined by the user + +# Arguments +`loss_function`: User-defined loss function which is minimized +`fitting_data`: The data which is being fitted + +# Returns +a vector of 7 parameters: 2 for the shift, 2 for the scaling, 2 for shear, and 1 for rotation angle + +# Example +there is an example of this function in the `examples/star_fitting_genaral.jl` + +""" +function perform_fit_general(loss_function, fitting_data::AbstractArray) + # guess the shift parameters by taking the maximum values of the array and + # centering the positions + ##a, b = Tuple(argmax(fitting_data)) .- size(fitting_data) ./2.0 .- 1.0 + #print("INSIDE!!! hehe") + # assigning the initial parameter estimates + init_x = vec([1.0, -2.0, 1.0, 1.0, 0.0, 0.0, 0.0]) #ndims(fitting_data)+1, ndims(fitting_data)+1)) + # reshape(Matrix(1.0*I, ndims(fitting_data)+1, ndims(fitting_data)+1), 1, 9)) #[a, b, 1.0, 1.0, 0.001, 0.001, 0.001] + + # setting the lower and upper boundary of the parameter values based on their limits + lower = [-1*size(fitting_data)[1], -1*size(fitting_data)[2], 0.0, 0.0, 0.0, 0.0, 0.0] + upper = [size(fitting_data)[1], size(fitting_data)[2], size(fitting_data)[1], size(fitting_data)[2], 2.0, 2.0, pi] + + # initializing the LBFGS optimizer + inner_optimizer = LBFGS(; m=1, linesearch=LineSearches.BackTracking(order=2)) + + # Computer, Optimize! :D + res = optimize( + loss_function, + + #BFGS(), + lower, upper, init_x, + Fminbox(inner_optimizer), + Optim.Options(store_trace = true, extended_trace = true, iterations=5000), + #autodiff = :forward + ) + + # return the estimated parameters + return Optim.minimizer(res), res +end + + +""" + perform_fit(loss_function, fitting_data::AbstractArray) + +Performs a fit to the fitting data using a loss function defined by the user + +# Arguments +`loss_function`: User-defined loss function which is minimized +`fitting_data`: The data which is being fitted + +# Returns +a vector of 4 parameters: first 2 for the shift and other 2 for the scaling factors + +# Example +there is an example of this function in the `examples/star_fitting.jl` + +""" +function perform_fit(loss_function, fitting_data::AbstractArray) + # guess the shift parameters by taking the maximum values of the array and + # centering the positions + a, b = Tuple(argmax(fitting_data)) .- size(fitting_data) ./2.0 .- 1.0 + + # assigning the initial parameter estimates + init_x = [a, b, 1.0, 1.0] + + # setting the lower and upper boundary of the parameter values based on the limits of the shift and scaling + lower = [-1*size(fitting_data)[1], -1*size(fitting_data)[2], 0.0, 0.0] + upper = [size(fitting_data)[1], size(fitting_data)[2], size(fitting_data)[1], size(fitting_data)[2]] + + # initializing the LBFGS optimizer + inner_optimizer = LBFGS(; m=10, linesearch=LineSearches.BackTracking(order=2)) + + # Computer, Optimize! :D + res = optimize( + loss_function, + lower, upper, + init_x, + Fminbox(inner_optimizer), + Optim.Options(store_trace = true, extended_trace = true, iterations=500), + autodiff = :forward + ) + + # return the estimated parameters + return Optim.minimizer(res) +end + + + + # size of the test array to fit -size_arr = 31.0 +size_arr = 22 noise_level = 10.0; # defining the mean and the varixance of the test normal (Gaussian) distribution μ = [0, 0] -Σ = [size_arr/1.2 0.0; - 0.0 size_arr/1.2] +Σ = [size_arr/10 0.0; + 0.0 size_arr/10] Σ_d = [2 1.5; 1.5 2] @@ -38,41 +136,47 @@ z = [pdf(p, [x,y]) for y in Y, x in X] # setting a typical values for the shift (1:2) and scale (3:4) -# true_vals = [1.15, -0.73, 2.1, 0.9, 0.0, 0.0, pi/6] -# true_vals = [2.3, -1.2, 0.9, 2.1, 0.1, 0.05, pi/2, 2.0, 3.0] +true_vals = [0.5, -1.5, 1.0, 1.0, 0.0, 0.0, pi/4]#pi/6] +#true_vals = [2.3, -1.2, 0.9, 2.1, 0.1, 0.05, pi/2, 2.0, 3.0] # normalizing the sample data -sample_data = Float32.(z./maximum(z)) +#sample_data = Float32.(z./maximum(z)) +#sample_data = TestImages.shepp_logan(32); +sample_data = rand.(size_arr, size_arr); + sample_data .+= rand(size(sample_data)...)./noise_level; -heatmap(sample_data, aspect_ratio=1) -x_cen, y_cen = (size(sample_data) .÷ 2.0 .+1) -ang = rand((0.0:0.1:pi)) -rot_mat = [cos(ang) -1.0*sin(ang) 0.0; sin(ang) cos(ang) 0.0; 0.0 0.0 1.0]; +x_cen, y_cen = (size(sample_data) .÷ 2.0 .+1) shear_mat = [1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0]; scale_mat = [1/1.2 0.0 0.0; 0.0 1/1.8 0.0; 0.0 0.0 1.0]; t_to_origin = [1.0 0.0 1*x_cen; 0.0 1.0 y_cen; 0.0 0.0 1.0]; t_to_center = [1.0 0.0 -1.0*x_cen; 0.0 1.0 -1.0*y_cen; 0.0 0.0 1.0]; - -matrix_c = t_to_origin * scale_mat * shear_mat * rot_mat * t_to_center +# converting the data to function (DataToFunctions.get_function) +f_general = get_function_affine(sample_data);#; super_sampling=1);#, extrapolation_bc=0.0); -# converting the data to function (DataToFunctions.get_function) -f_general = get_function_general_matrix(sample_data);#; super_sampling=1);#, extrapolation_bc=0.0); +ang = rand((0.0:0.1:pi)) +rot_mat = [cos(ang) -1.0*sin(ang) 0.0; sin(ang) cos(ang) 0.0; 0.0 0.0 1.0]; + +matrix_c = Float32.(t_to_origin * scale_mat * shear_mat * rot_mat * t_to_center ) + # f_d = get_function_loop(sample_data_d; super_sampling=1);#, extrapolation_bc=0.0); # adding some scaled random noise to the fitting data -fitting_data = f_general(matrix_c) .+ rand(size(sample_data)...)./100.0; -heatmap(fitting_data, aspect_ratio=1) +# fitting_data = f_general(SMatrix{3,3}(matrix_c)); #.+ rand(size(sample_data)...)./100.0; +fitting_data = f_general(true_vals) .+ rand(size(sample_data)...)./10.0; + + +plot(heatmap(sample_data, aspect_ratio=1), heatmap(fitting_data, aspect_ratio=1)) # defining the loss function based on the gaussian noise -loss(p) = sum(abs2.(f_general(p) .- fitting_data)) +loss(p1::AbstractVector) = sum(abs2.(f_general(p1::AbstractVector) .- fitting_data)) # loss(x) = loss(x::AbstractVector{T} where T) # loss(p3) = loss([p3[1], p3[2], p3[3], p3[4], p3[5], p3[6], p3[7]]) @@ -81,53 +185,53 @@ loss(p) = sum(abs2.(f_general(p) .- fitting_data)) # perform the main fit to the fitting data by minimizing the loss function @time output, res = perform_fit_general(loss, fitting_data) + # plotting the output of the fitting pocedure for further illustration begin p00 = heatmap(sample_data, aspect_ratio=1.0, clim=(0.0, 1.0), title="Sample data", legend = :none); p01 = heatmap(fitting_data, aspect_ratio=1.0, clim=(0.0,1.0), title="Fitting data", legend = :none); - p02 = heatmap(f_general(reshape(output, ndims(fitting_data)+1, ndims(fitting_data)+1)), aspect_ratio=1.0, clim=(0.0,1.0), title="estimated fit", legend = :none); - p03 = heatmap(fitting_data .- f_general(output), aspect_ratio=1.0, clim=(0.0, 0.3), title="discrepancy", legend = :none); + p02 = heatmap(f_general(output), aspect_ratio=1.0, clim=(0.0,1.0), title="estimated fit", legend = :none); + p03 = heatmap(fitting_data .- f_general(output), aspect_ratio=1.0, clim=(0.0, 1.0), title="discrepancy", legend = :none); plot(p00, p01, p02, p03, layout=@layout([A B C D]), framestyle=nothing, showaxis=false, xticks=false, yticks=false, size=(1200, 500), - #plot_title="True vals: $(matrix_c) - #est vals: $(reshape(output, ndims(fitting_data)+1, ndims(fitting_data)+1))", - #plot_titlevspan=0.2 + plot_title="True vals: $(true_vals) + fitted vals: $(output)", + plot_titlevspan=0.2 ) end matrix_c -reshape(output, ndims(fitting_data)+1, ndims(fitting_data)+1) # comparing the true values to the best fitting parameters #println(matrix_c) * reshape(output, ndims(fitting_data)+1, ndims(fitting_data)+1) -anim = @animate for i1 in 1:length(Optim.x_trace(res)) - - begin - p00 = heatmap(sample_data, aspect_ratio=1.0, clim=(0.0, 1.0), title="Sample data", legend = :none); - p01 = heatmap(fitting_data, aspect_ratio=1.0, clim=(0.0,1.0), title="Fitting data", legend = :none); - p02 = heatmap(f_general(Optim.x_trace(res)[i1]), aspect_ratio=1.0, clim=(0.0,1.0), title="estimated fit", legend = :none); - p03 = heatmap(fitting_data .- f_general(Optim.x_trace(res)[i1]), aspect_ratio=1.0, clim=(0.0, 0.3), title="discrepancy", legend = :none); - - plot(p00, p01, p02, p03, layout=@layout([A B C D]), - framestyle=nothing, showaxis=false, - xticks=false, yticks=false, - size=(1200, 500), - plot_title="iteration: $(Int(i1))/$(length(Optim.x_trace(res))), - estimation: $(Optim.x_trace(res)[i1]) - true vals : $(true_vals)", - plot_titlevspan=0.25 - ) - end - - -end; - -gif(anim, "DataToFunctions.jl/examples/anim_general_generalized.mp4", fps=2) - +#anim = @animate for i1 in 1:length(Optim.x_trace(res)) +# +# begin +# p00 = heatmap(sample_data, aspect_ratio=1.0, clim=(0.0, 1.0), title="Sample data", legend = :none); +# p01 = heatmap(fitting_data, aspect_ratio=1.0, clim=(0.0,1.0), title="Fitting data", legend = :none); +# p02 = heatmap(f_general(Optim.x_trace(res)[i1]), aspect_ratio=1.0, clim=(0.0,1.0), title="estimated fit", legend = :none); +# p03 = heatmap(fitting_data .- f_general(Optim.x_trace(res)[i1]), aspect_ratio=1.0, clim=(0.0, 0.3), title="discrepancy", legend = :none); +# +# plot(p00, p01, p02, p03, layout=@layout([A B C D]), +# framestyle=nothing, showaxis=false, +# xticks=false, yticks=false, +# size=(1200, 500), +# plot_title="iteration: $(Int(i1))/$(length(Optim.x_trace(res))), +# estimation: $(Optim.x_trace(res)[i1]) +# true vals : $(true_vals)", +# plot_titlevspan=0.25 +# ) +# end +# +# +#end; +# +#gif(anim, "DataToFunctions.jl/examples/anim_general_generalized.mp4", fps=2) +# diff --git a/src/DataToFunctions.jl b/src/DataToFunctions.jl index cb511fc..b94c7b7 100644 --- a/src/DataToFunctions.jl +++ b/src/DataToFunctions.jl @@ -1,12 +1,9 @@ module DataToFunctions using Interpolations using FourierTools -using Optim #, LineSearches -using Revise -using StaticArrays #, LinearAlgebra -using Zygote +using StaticArrays -export get_function, perform_fit, get_function_general, perform_fit_general, get_function_general_matrix +export get_function, get_function_affine """ get_function(data::AbstractArray; super_sampling=2, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) @@ -60,54 +57,9 @@ end -""" - perform_fit(loss_function, fitting_data::AbstractArray) - -Performs a fit to the fitting data using a loss function defined by the user - -# Arguments -`loss_function`: User-defined loss function which is minimized -`fitting_data`: The data which is being fitted - -# Returns -a vector of 4 parameters: first 2 for the shift and other 2 for the scaling factors - -# Example -there is an example of this function in the `examples/star_fitting.jl` """ -function perform_fit(loss_function, fitting_data::AbstractArray) - # guess the shift parameters by taking the maximum values of the array and - # centering the positions - a, b = Tuple(argmax(fitting_data)) .- size(fitting_data) ./2.0 .- 1.0 - - # assigning the initial parameter estimates - init_x = [a, b, 1.0, 1.0] - - # setting the lower and upper boundary of the parameter values based on the limits of the shift and scaling - lower = [-1*size(fitting_data)[1], -1*size(fitting_data)[2], 0.0, 0.0] - upper = [size(fitting_data)[1], size(fitting_data)[2], size(fitting_data)[1], size(fitting_data)[2]] - - # initializing the LBFGS optimizer - inner_optimizer = LBFGS(; m=1, linesearch=LineSearches.BackTracking(order=2)) - - # Computer, Optimize! :D - res = optimize( - loss_function, - lower, upper, - init_x, - Fminbox(inner_optimizer), - Optim.Options(store_trace = true, extended_trace = true, iterations=500), - autodiff = :forward - ) - - # return the estimated parameters - return Optim.minimizer(res) -end - - -""" - get_function_general(data::AbstractArray; super_sampling=1, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) + get_function_affine(data::AbstractArray; super_sampling=1, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) returns a function `interpolated(p)` which generates a transformed version of the original data. This is useful for fitting with a function which is itself defined by measured data. @@ -119,9 +71,9 @@ This is useful for fitting with a function which is itself defined by measured d `interp_type`: The type of interpolation to use. See the package `Interpolation` for details. """ -function get_function_general(data::AbstractArray; extrapolation_bc=0.0, interp_type=Interpolations.BSpline(Linear())) - # new_size = super_sampling.*size(data) - # upsampled = fftshift(resample(ifftshift(data), new_size)) +function get_function_affine(data::AbstractArray{T}; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where T + #new_size = super_sampling.*size(data) + #upsampled = fftshift(resample(ifftshift(data), new_size)) # building the extraplation + interpolation object itp = extrapolate(interpolate(data, interp_type), extrapolation_bc); @@ -131,88 +83,60 @@ function get_function_general(data::AbstractArray; extrapolation_bc=0.0, interp_ return matrix_c * t end - function interpolated(p) + function interpolated(matrix_c::SMatrix) # init a new array for the output - out = similar(data) + out = similar(data, T) - x_cen, y_cen = (size(data) .÷ 2.0 .+1) - # x_cen_up, y_cen_up = (size(upsampled) .÷ 2.0 .+ 1.0) + # x_cen, y_cen = (size(data) .÷ 2.0 .+1) - # creating the matrices of rotation, shear, scale, and shift - rot_mat = @SMatrix [cos(p[7]) -1.0*sin(p[7]) 0.0; sin(p[7]) cos(p[7]) 0.0; 0.0 0.0 1.0]; - shear_mat = @SMatrix [1.0 p[5] 0.0; p[6] 1.0 0.0; 0.0 0.0 1.0]; - scale_mat = @SMatrix [1/p[3] 0.0 0.0; 0.0 1/p[4] 0.0; 0.0 0.0 1.0]; - shift_mat = @SMatrix [1.0 0.0 -1*p[1]; 0.0 1.0 -1*p[2]; 0.0 0.0 1.0]; - t_to_origin = @SMatrix [1.0 0.0 1*x_cen; 0.0 1.0 y_cen; 0.0 0.0 1.0]; - t_to_center = @SMatrix [1.0 0.0 -1.0*x_cen; 0.0 1.0 -1.0*y_cen; 0.0 0.0 1.0]; # t_orig_upsampled = @SMatrix [1.0 0.0 -1.0*x_cen_up; 0.0 1.0 -1.0*y_cen_up; 0.0 0.0 1.0] # building the overall transformation matrix - matrix_c = t_to_origin * scale_mat * shear_mat * rot_mat *shift_mat * t_to_center - + # matrix_c = SMatrix{ndims(data)+1, ndims(data)+1, T}(reshape(p, ndims(data)+1, ndims(data)+1)) + + #print(eltype(matrix_c)) # looping all over the catesian indedices of the input image, # first ading a new value to its third dimenstion: 1.0, # converting to the new indices using the transformation matrix and then, # using the "itp" object, we build the transfomed image "out" - for I in CartesianIndices(data) - out[I] = itp(f(SVector(Tuple(I)..., 1), matrix_c)[1:2]...) + for I1 in CartesianIndices(data) + #print("INSIde the loop!!") + #print(eltype(SVector{ndims(data)+1, T}(Tuple(I1)..., 1))) + out[I1] = itp(f(SVector{ndims(data)+1, T}(Tuple(I1)..., 1.0), matrix_c)[1:2]...) + #print(eltype(out[I1])) end return out end - return interpolated -end - - - -""" - get_function_general_matrix(data::AbstractArray; super_sampling=1, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) - -returns a function `interpolated(p)` which generates a transformed version of the original data. -This is useful for fitting with a function which is itself defined by measured data. - -# Arguments -`data`: The data to represent by the function `dat` -`extrapolation_bc`: The extrapolation boundary condition to select for values outside the range. - By default the value 0.0 is used. Other options are `Flat()`, or `Line()`, See the package `Interpolation` for details. -`interp_type`: The type of interpolation to use. See the package `Interpolation` for details. - -""" -function get_function_general_matrix(data::AbstractArray{T}; extrapolation_bc=0.0, interp_type=Interpolations.BSpline(Linear())) where T - # new_size = super_sampling.*size(data) - # upsampled = fftshift(resample(ifftshift(data), new_size)) - - # building the extraplation + interpolation object - itp = extrapolate(interpolate(data, interp_type), extrapolation_bc); - - # multiplying the transformation matrix - function f(t::AbstractVector{T}, matrix_c::AbstractMatrix{T}) where T - return matrix_c * t - end - - function interpolated(p::AbstractMatrix{T}) where T + function interpolated(p::AbstractVector{T}) where T # init a new array for the output - out = similar(data, T) + out = similar(data) - # x_cen, y_cen = (size(data) .÷ 2.0 .+1) + x_cen, y_cen = (size(data) .÷ 2.0 .+1) + # x_cen_up, y_cen_up = (size(upsampled) .÷ 2.0 .+ 1.0) + # creating the matrices of rotation, shear, scale, and shift + rot_mat = @SMatrix [cos(p[7]) -1.0*sin(p[7]) 0.0; sin(p[7]) cos(p[7]) 0.0; 0.0 0.0 1.0]; + shear_mat = @SMatrix [1.0 p[5] 0.0; p[6] 1.0 0.0; 0.0 0.0 1.0]; + scale_mat = @SMatrix [1/p[3] 0.0 0.0; 0.0 1/p[4] 0.0; 0.0 0.0 1.0]; + shift_mat = @SMatrix [1.0 0.0 -1*p[1]; 0.0 1.0 -1*p[2]; 0.0 0.0 1.0]; + t_to_origin = @SMatrix [1.0 0.0 1*x_cen; 0.0 1.0 y_cen; 0.0 0.0 1.0]; + t_to_center = @SMatrix [1.0 0.0 -1.0*x_cen; 0.0 1.0 -1.0*y_cen; 0.0 0.0 1.0]; # t_orig_upsampled = @SMatrix [1.0 0.0 -1.0*x_cen_up; 0.0 1.0 -1.0*y_cen_up; 0.0 0.0 1.0] # building the overall transformation matrix - matrix_c = SMatrix{ndims(data)+1, ndims(data)+1, T}(reshape(p, ndims(data)+1, ndims(data)+1)) - #print(eltype(matrix_c)) + # matrix_c = t_to_origin * scale_mat * shear_mat * rot_mat *shift_mat * t_to_center + matrix_c = t_to_origin * scale_mat * rot_mat *shift_mat * t_to_center + # looping all over the catesian indedices of the input image, # first ading a new value to its third dimenstion: 1.0, # converting to the new indices using the transformation matrix and then, # using the "itp" object, we build the transfomed image "out" for I1 in CartesianIndices(data) - #print("INSIde the loop!!") - #print(eltype(SVector{ndims(data)+1, T}(Tuple(I1)..., 1))) - out[I1] = itp(f(SVector{ndims(data)+1, T}(Tuple(I1)..., 1.0), matrix_c)[1:2]...) - #print(eltype(out[I1])) + out[I1] = itp(f(SVector(Tuple(I1)..., 1), matrix_c)[1:2]...) end return out @@ -223,50 +147,4 @@ end -""" - perform_fit_general(loss_function, fitting_data::AbstractArray) - -Performs a fit to the fitting data using a loss function defined by the user - -# Arguments -`loss_function`: User-defined loss function which is minimized -`fitting_data`: The data which is being fitted - -# Returns -a vector of 7 parameters: 2 for the shift, 2 for the scaling, 2 for shear, and 1 for rotation angle - -# Example -there is an example of this function in the `examples/star_fitting_genaral.jl` - -""" -function perform_fit_general(loss_function, fitting_data::AbstractArray) - # guess the shift parameters by taking the maximum values of the array and - # centering the positions - ##a, b = Tuple(argmax(fitting_data)) .- size(fitting_data) ./2.0 .- 1.0 - #print("INSIDE!!! hehe") - # assigning the initial parameter estimates - init_x = reshape(Matrix(1.0*I, ndims(fitting_data)+1, ndims(fitting_data)+1), 1, 9) #[a, b, 1.0, 1.0, 0.001, 0.001, 0.001] - - # setting the lower and upper boundary of the parameter values based on their limits - ##lower = [-1*size(fitting_data)[1], -1*size(fitting_data)[2], 0.0, 0.0, 0.0, 0.0, 0.0] - ##upper = [size(fitting_data)[1], size(fitting_data)[2], size(fitting_data)[1], size(fitting_data)[2], 5.0, 5.0, pi] - - # initializing the LBFGS optimizer - # inner_optimizer = LBFGS(; m=1, linesearch=LineSearches.BackTracking(order=2)) - - # Computer, Optimize! :D - res = optimize( - loss_function, - init_x, - LBFGS(), - # lower, upper, - # Fminbox(inner_optimizer), - Optim.Options(store_trace = true, extended_trace = true, iterations=500), - autodiff = :forward - ) - - # return the estimated parameters - return Optim.minimizer(res), res -end - end # module DataToFunctions \ No newline at end of file From 8ae0a6f9d4525d43a1953828cb54c8efb84a3b1a Mon Sep 17 00:00:00 2001 From: RainerHeintzmann Date: Mon, 26 Feb 2024 17:49:58 +0100 Subject: [PATCH 08/44] one allocation --- src/DataToFunctions.jl | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/DataToFunctions.jl b/src/DataToFunctions.jl index b94c7b7..a4f7bdf 100644 --- a/src/DataToFunctions.jl +++ b/src/DataToFunctions.jl @@ -3,7 +3,8 @@ using Interpolations using FourierTools using StaticArrays -export get_function, get_function_affine +export get_function, get_function_affine, add_dim, red_dim_apply, red_dim, f +export extrapolate, interpolate """ get_function(data::AbstractArray; super_sampling=2, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) @@ -55,8 +56,22 @@ function get_function(data::AbstractArray; super_sampling=2, extrapolation_bc=ze end +function add_dim(cind) + return SVector.((Tuple(cind))..., 1) +end +@inline function red_dim_apply(fct, svec::SVector{S,T})::Float64 where {S,T} + return fct((@view svec[1:2])...) +end +@inline function red_dim(svec::SVector{S,T})::SVector{S-1,T} where {S,T} + return @view svec[1:S-1] +end + +# multiplying the transformation matrix +@inline function f(t::SVector{N, Int}, matrix_c::SMatrix{N,N,T})::SVector{N,T} where {N,T} + return matrix_c * t +end """ get_function_affine(data::AbstractArray; super_sampling=1, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) @@ -78,10 +93,6 @@ function get_function_affine(data::AbstractArray{T}; super_sampling=2, extrapola # building the extraplation + interpolation object itp = extrapolate(interpolate(data, interp_type), extrapolation_bc); - # multiplying the transformation matrix - function f(t::SVector, matrix_c::SMatrix) - return matrix_c * t - end function interpolated(matrix_c::SMatrix) @@ -110,8 +121,7 @@ function get_function_affine(data::AbstractArray{T}; super_sampling=2, extrapola return out end - function interpolated(p::AbstractVector{T}) where T - + function interpolated(p::AbstractVector{T}) where T # init a new array for the output out = similar(data) @@ -138,7 +148,9 @@ function get_function_affine(data::AbstractArray{T}; super_sampling=2, extrapola for I1 in CartesianIndices(data) out[I1] = itp(f(SVector(Tuple(I1)..., 1), matrix_c)[1:2]...) end - + + # out[CartesianIndices(data)] .= red_dim_apply.(Ref(itp), f.(add_dim.(CartesianIndices(data)), Ref(matrix_c))) + return out end From fd56e1c1de724e1c11fab95f2d8e3cc8b335a06e Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Tue, 27 Feb 2024 18:04:06 +0100 Subject: [PATCH 09/44] added random seed and fixed type stability --- ...ting_general.jl => PSF_fitting_general.jl} | 23 ++++++++++++------- examples/Project.toml | 1 + src/DataToFunctions.jl | 13 ++++++----- 3 files changed, 23 insertions(+), 14 deletions(-) rename examples/{star_fitting_general.jl => PSF_fitting_general.jl} (93%) diff --git a/examples/star_fitting_general.jl b/examples/PSF_fitting_general.jl similarity index 93% rename from examples/star_fitting_general.jl rename to examples/PSF_fitting_general.jl index ba1b3f8..a579d30 100644 --- a/examples/star_fitting_general.jl +++ b/examples/PSF_fitting_general.jl @@ -7,6 +7,10 @@ using Distributions, Rotations using Plots using TestImages + +import Random +Random.seed!(1234) + Base.show(io::IO, f::Float64) = @printf(io, "%.3f", f) @@ -33,7 +37,7 @@ function perform_fit_general(loss_function, fitting_data::AbstractArray) ##a, b = Tuple(argmax(fitting_data)) .- size(fitting_data) ./2.0 .- 1.0 #print("INSIDE!!! hehe") # assigning the initial parameter estimates - init_x = vec([1.0, -2.0, 1.0, 1.0, 0.0, 0.0, 0.0]) #ndims(fitting_data)+1, ndims(fitting_data)+1)) + init_x = vec([0.5, -1.5, 1.0, 1.0, 0.0, 0.0, pi/5]) #ndims(fitting_data)+1, ndims(fitting_data)+1)) # reshape(Matrix(1.0*I, ndims(fitting_data)+1, ndims(fitting_data)+1), 1, 9)) #[a, b, 1.0, 1.0, 0.001, 0.001, 0.001] # setting the lower and upper boundary of the parameter values based on their limits @@ -133,18 +137,18 @@ z = [pdf(p, [x,y]) for y in Y, x in X] # @vv z - +dtype = Float64 # setting a typical values for the shift (1:2) and scale (3:4) -true_vals = [0.5, -1.5, 1.0, 1.0, 0.0, 0.0, pi/4]#pi/6] +true_vals = dtype.([0.2, -1.2, 1.0, 1.0, 0.0, 0.0, pi/3])#pi/6] #true_vals = [2.3, -1.2, 0.9, 2.1, 0.1, 0.05, pi/2, 2.0, 3.0] # normalizing the sample data #sample_data = Float32.(z./maximum(z)) #sample_data = TestImages.shepp_logan(32); -sample_data = rand.(size_arr, size_arr); +sample_data = rand(dtype, (size_arr, size_arr)) -sample_data .+= rand(size(sample_data)...)./noise_level; +sample_data .+= rand(dtype, (size(sample_data)...))./noise_level; @@ -163,13 +167,13 @@ f_general = get_function_affine(sample_data);#; super_sampling=1);#, extrapolati ang = rand((0.0:0.1:pi)) rot_mat = [cos(ang) -1.0*sin(ang) 0.0; sin(ang) cos(ang) 0.0; 0.0 0.0 1.0]; -matrix_c = Float32.(t_to_origin * scale_mat * shear_mat * rot_mat * t_to_center ) +matrix_c = dtype.(t_to_origin * scale_mat * shear_mat * rot_mat * t_to_center ) # f_d = get_function_loop(sample_data_d; super_sampling=1);#, extrapolation_bc=0.0); # adding some scaled random noise to the fitting data # fitting_data = f_general(SMatrix{3,3}(matrix_c)); #.+ rand(size(sample_data)...)./100.0; -fitting_data = f_general(true_vals) .+ rand(size(sample_data)...)./10.0; +fitting_data = f_general(true_vals) .+ dtype.(rand(size(sample_data)...))./5.0; plot(heatmap(sample_data, aspect_ratio=1), heatmap(fitting_data, aspect_ratio=1)) @@ -204,7 +208,10 @@ begin end -matrix_c + + + +#matrix_c # comparing the true values to the best fitting parameters #println(matrix_c) * reshape(output, ndims(fitting_data)+1, ndims(fitting_data)+1) diff --git a/examples/Project.toml b/examples/Project.toml index 3268c51..e9267f2 100644 --- a/examples/Project.toml +++ b/examples/Project.toml @@ -4,6 +4,7 @@ CoordinateTransformations = "150eb455-5306-5404-9cee-2592286d6298" DataToFunctions = "64cfdffa-4d02-49ee-ae8b-a805370874f5" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" LineSearches = "d3d80556-e9d4-5f37-9878-2ab0fcc64255" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" OhMyREPL = "5fb14364-9ced-5910-84b2-373655c76a03" diff --git a/src/DataToFunctions.jl b/src/DataToFunctions.jl index a4f7bdf..2bf46fa 100644 --- a/src/DataToFunctions.jl +++ b/src/DataToFunctions.jl @@ -60,7 +60,7 @@ function add_dim(cind) return SVector.((Tuple(cind))..., 1) end -@inline function red_dim_apply(fct, svec::SVector{S,T})::Float64 where {S,T} +@inline function red_dim_apply(fct::AbstractArray{R}, svec::SVector{S,T})::R where {S,T, R} return fct((@view svec[1:2])...) end @@ -121,7 +121,7 @@ function get_function_affine(data::AbstractArray{T}; super_sampling=2, extrapola return out end - function interpolated(p::AbstractVector{T}) where T + function interpolated(p::AbstractVector{T}) where T # init a new array for the output out = similar(data) @@ -145,11 +145,12 @@ function get_function_affine(data::AbstractArray{T}; super_sampling=2, extrapola # first ading a new value to its third dimenstion: 1.0, # converting to the new indices using the transformation matrix and then, # using the "itp" object, we build the transfomed image "out" - for I1 in CartesianIndices(data) - out[I1] = itp(f(SVector(Tuple(I1)..., 1), matrix_c)[1:2]...) - end + + #for I1 in CartesianIndices(data) + # out[I1] = itp(f(SVector(Tuple(I1)..., 1), matrix_c)[1:2]...) + #end - # out[CartesianIndices(data)] .= red_dim_apply.(Ref(itp), f.(add_dim.(CartesianIndices(data)), Ref(matrix_c))) + out[CartesianIndices(data)] .= red_dim_apply.(Ref(itp), f.(add_dim.(CartesianIndices(data)), Ref(matrix_c))); return out end From c236014db80f28a7d507a5821903c0fe71a90f04 Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Thu, 28 Mar 2024 17:20:19 +0100 Subject: [PATCH 10/44] added the function input support --- examples/PSF_fitting_general.jl | 8 ++--- examples/Project.toml | 2 ++ src/DataToFunctions.jl | 64 +++++++++++++++++++++++++++++---- 3 files changed, 63 insertions(+), 11 deletions(-) diff --git a/examples/PSF_fitting_general.jl b/examples/PSF_fitting_general.jl index a579d30..6bc31d2 100644 --- a/examples/PSF_fitting_general.jl +++ b/examples/PSF_fitting_general.jl @@ -42,7 +42,7 @@ function perform_fit_general(loss_function, fitting_data::AbstractArray) # setting the lower and upper boundary of the parameter values based on their limits lower = [-1*size(fitting_data)[1], -1*size(fitting_data)[2], 0.0, 0.0, 0.0, 0.0, 0.0] - upper = [size(fitting_data)[1], size(fitting_data)[2], size(fitting_data)[1], size(fitting_data)[2], 2.0, 2.0, pi] + upper = [size(fitting_data)[1], size(fitting_data)[2], size(fitting_data)[1], size(fitting_data)[2], 1.0, 1.0, pi] # initializing the LBFGS optimizer inner_optimizer = LBFGS(; m=1, linesearch=LineSearches.BackTracking(order=2)) @@ -145,10 +145,10 @@ true_vals = dtype.([0.2, -1.2, 1.0, 1.0, 0.0, 0.0, pi/3])#pi/6] # normalizing the sample data #sample_data = Float32.(z./maximum(z)) -#sample_data = TestImages.shepp_logan(32); -sample_data = rand(dtype, (size_arr, size_arr)) +sample_data = dtype.(TestImages.shepp_logan(32)); +#sample_data = rand(dtype, (size_arr, size_arr)) -sample_data .+= rand(dtype, (size(sample_data)...))./noise_level; +#sample_data .+= rand(dtype, (size(sample_data)...))./noise_level; diff --git a/examples/Project.toml b/examples/Project.toml index e9267f2..f6ed356 100644 --- a/examples/Project.toml +++ b/examples/Project.toml @@ -11,8 +11,10 @@ OhMyREPL = "5fb14364-9ced-5910-84b2-373655c76a03" Optim = "429524aa-4258-5aef-a3af-852621145aeb" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +RandomExtensions = "fb686558-2515-59ef-acaa-46db3789a887" Rotations = "6038ab10-8711-5258-84ad-4b1120ba62dc" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990" +TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" View5D = "90d841e0-6953-4e90-9f3a-43681da8e949" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" diff --git a/src/DataToFunctions.jl b/src/DataToFunctions.jl index 2bf46fa..9188d6b 100644 --- a/src/DataToFunctions.jl +++ b/src/DataToFunctions.jl @@ -3,7 +3,7 @@ using Interpolations using FourierTools using StaticArrays -export get_function, get_function_affine, add_dim, red_dim_apply, red_dim, f +export get_function, get_function_affine, add_dim, red_dim_apply, red_dim, f, func_transform export extrapolate, interpolate """ @@ -69,10 +69,17 @@ end end # multiplying the transformation matrix -@inline function f(t::SVector{N, Int}, matrix_c::SMatrix{N,N,T})::SVector{N,T} where {N,T} +@inline function f(t::SVector{N, Int64}, matrix_c::SMatrix{N,N,T})::SVector{N,T} where {N,T} return matrix_c * t end + +# Applying the coordinate transformation function +@inline function func_transform(t, coord_transform_func::Function)::SVector + return coord_transform_func(Tuple(t)) +end + + """ get_function_affine(data::AbstractArray; super_sampling=1, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) @@ -97,7 +104,7 @@ function get_function_affine(data::AbstractArray{T}; super_sampling=2, extrapola function interpolated(matrix_c::SMatrix) # init a new array for the output - out = similar(data, T) + out = data # x_cen, y_cen = (size(data) .÷ 2.0 .+1) @@ -111,19 +118,30 @@ function get_function_affine(data::AbstractArray{T}; super_sampling=2, extrapola # first ading a new value to its third dimenstion: 1.0, # converting to the new indices using the transformation matrix and then, # using the "itp" object, we build the transfomed image "out" - for I1 in CartesianIndices(data) + + #for I1 in CartesianIndices(data) #print("INSIde the loop!!") #print(eltype(SVector{ndims(data)+1, T}(Tuple(I1)..., 1))) - out[I1] = itp(f(SVector{ndims(data)+1, T}(Tuple(I1)..., 1.0), matrix_c)[1:2]...) + # out[I1] = itp(f(SVector{ndims(data)+1, T}(Tuple(I1)..., 1.0), matrix_c)[1:2]...) #print(eltype(out[I1])) - end + #end + + out[CartesianIndices(data)] .= red_dim_apply.(Ref(itp), f.(add_dim.(CartesianIndices(data)), Ref(matrix_c))); return out end + function interpolated(matrix_c::SMatrix, out::AbstractArray) + + out[CartesianIndices(data)] .= red_dim_apply.(Ref(itp), f.(add_dim.(CartesianIndices(data)), Ref(matrix_c))); + + #return out + end + function interpolated(p::AbstractVector{T}) where T + # init a new array for the output - out = similar(data) + out = data x_cen, y_cen = (size(data) .÷ 2.0 .+1) # x_cen_up, y_cen_up = (size(upsampled) .÷ 2.0 .+ 1.0) @@ -155,6 +173,38 @@ function get_function_affine(data::AbstractArray{T}; super_sampling=2, extrapola return out end + + function interpolated(p::AbstractVector{T}, out::AbstractArray) where T + + x_cen, y_cen = (size(data) .÷ 2.0 .+1) + # x_cen_up, y_cen_up = (size(upsampled) .÷ 2.0 .+ 1.0) + + # creating the matrices of rotation, shear, scale, and shift + rot_mat = @SMatrix [cos(p[7]) -1.0*sin(p[7]) 0.0; sin(p[7]) cos(p[7]) 0.0; 0.0 0.0 1.0]; + shear_mat = @SMatrix [1.0 p[5] 0.0; p[6] 1.0 0.0; 0.0 0.0 1.0]; + scale_mat = @SMatrix [1/p[3] 0.0 0.0; 0.0 1/p[4] 0.0; 0.0 0.0 1.0]; + shift_mat = @SMatrix [1.0 0.0 -1*p[1]; 0.0 1.0 -1*p[2]; 0.0 0.0 1.0]; + t_to_origin = @SMatrix [1.0 0.0 1*x_cen; 0.0 1.0 y_cen; 0.0 0.0 1.0]; + t_to_center = @SMatrix [1.0 0.0 -1.0*x_cen; 0.0 1.0 -1.0*y_cen; 0.0 0.0 1.0]; + # t_orig_upsampled = @SMatrix [1.0 0.0 -1.0*x_cen_up; 0.0 1.0 -1.0*y_cen_up; 0.0 0.0 1.0] + + # building the overall transformation matrix + # matrix_c = t_to_origin * scale_mat * shear_mat * rot_mat *shift_mat * t_to_center + matrix_c = t_to_origin * scale_mat * rot_mat *shift_mat * t_to_center + + out[CartesianIndices(data)] .= red_dim_apply.(Ref(itp), f.(add_dim.(CartesianIndices(data)), Ref(matrix_c))); + end + + + # How to run this function: + # f_general((t) -> (t[1]*1.01, t[2]*1.01), out2) + function interpolated(coord_transf_func::Function, out::AbstractArray) + + out[CartesianIndices(data)] .= red_dim_apply.(Ref(itp), func_transform.(CartesianIndices(data), Ref(coord_transf_func))); + #return out + end + + return interpolated end From 63f548c3515e63feb5f143aeecbe3c93ec7ddf10 Mon Sep 17 00:00:00 2001 From: RainerHeintzmann Date: Wed, 3 Apr 2024 12:18:07 +0200 Subject: [PATCH 11/44] rewrote some bits towards modularity --- examples/PSF_fitting_general.jl | 54 +++++----- src/DataToFunctions.jl | 180 ++++++++++++++++++++------------ 2 files changed, 135 insertions(+), 99 deletions(-) diff --git a/examples/PSF_fitting_general.jl b/examples/PSF_fitting_general.jl index 6bc31d2..a41fd7c 100644 --- a/examples/PSF_fitting_general.jl +++ b/examples/PSF_fitting_general.jl @@ -9,11 +9,6 @@ using TestImages import Random -Random.seed!(1234) - -Base.show(io::IO, f::Float64) = @printf(io, "%.3f", f) - - """ perform_fit_general(loss_function, fitting_data::AbstractArray) @@ -110,10 +105,12 @@ end - +function main() +Random.seed!(1234) +# Base.show(io::IO, f::Float64) = @printf(io, "%.3f", f) + # size of the test array to fit size_arr = 22 - noise_level = 10.0; # defining the mean and the varixance of the test normal (Gaussian) distribution @@ -128,7 +125,6 @@ noise_level = 10.0; p = MvNormal(μ, Σ) - # this part of the code is to define the sample array based on a 2D normal distribution X = -1*size_arr/2.0:1*size_arr/2.0 Y = -1*size_arr/2.0:1*size_arr/2.0 @@ -140,7 +136,7 @@ z = [pdf(p, [x,y]) for y in Y, x in X] dtype = Float64 # setting a typical values for the shift (1:2) and scale (3:4) -true_vals = dtype.([0.2, -1.2, 1.0, 1.0, 0.0, 0.0, pi/3])#pi/6] +true_vals = dtype.([0.2, -1.2, 1.0, 1.0, 0.0, 0.0, pi/3]) #pi/6] #true_vals = [2.3, -1.2, 0.9, 2.1, 0.1, 0.05, pi/2, 2.0, 3.0] # normalizing the sample data @@ -150,8 +146,6 @@ sample_data = dtype.(TestImages.shepp_logan(32)); #sample_data .+= rand(dtype, (size(sample_data)...))./noise_level; - - x_cen, y_cen = (size(sample_data) .÷ 2.0 .+1) shear_mat = [1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0]; @@ -161,8 +155,7 @@ t_to_origin = [1.0 0.0 1*x_cen; 0.0 1.0 y_cen; 0.0 0.0 1.0]; t_to_center = [1.0 0.0 -1.0*x_cen; 0.0 1.0 -1.0*y_cen; 0.0 0.0 1.0]; # converting the data to function (DataToFunctions.get_function) -f_general = get_function_affine(sample_data);#; super_sampling=1);#, extrapolation_bc=0.0); - +f_affine = get_function_affine(sample_data);#; super_sampling=1);#, extrapolation_bc=0.0); ang = rand((0.0:0.1:pi)) rot_mat = [cos(ang) -1.0*sin(ang) 0.0; sin(ang) cos(ang) 0.0; 0.0 0.0 1.0]; @@ -173,14 +166,14 @@ matrix_c = dtype.(t_to_origin * scale_mat * shear_mat * rot_mat * t_to_center ) # adding some scaled random noise to the fitting data # fitting_data = f_general(SMatrix{3,3}(matrix_c)); #.+ rand(size(sample_data)...)./100.0; -fitting_data = f_general(true_vals) .+ dtype.(rand(size(sample_data)...))./5.0; +fitting_data = f_affine(true_vals) .+ dtype.(rand(size(sample_data)...))./5.0; plot(heatmap(sample_data, aspect_ratio=1), heatmap(fitting_data, aspect_ratio=1)) # defining the loss function based on the gaussian noise -loss(p1::AbstractVector) = sum(abs2.(f_general(p1::AbstractVector) .- fitting_data)) +loss(p1::AbstractVector) = sum(abs2.(f_affine(p1::AbstractVector) .- fitting_data)) # loss(x) = loss(x::AbstractVector{T} where T) # loss(p3) = loss([p3[1], p3[2], p3[3], p3[4], p3[5], p3[6], p3[7]]) @@ -191,22 +184,23 @@ loss(p1::AbstractVector) = sum(abs2.(f_general(p1::AbstractVector) .- fitting_da # plotting the output of the fitting pocedure for further illustration -begin - p00 = heatmap(sample_data, aspect_ratio=1.0, clim=(0.0, 1.0), title="Sample data", legend = :none); - p01 = heatmap(fitting_data, aspect_ratio=1.0, clim=(0.0,1.0), title="Fitting data", legend = :none); - p02 = heatmap(f_general(output), aspect_ratio=1.0, clim=(0.0,1.0), title="estimated fit", legend = :none); - p03 = heatmap(fitting_data .- f_general(output), aspect_ratio=1.0, clim=(0.0, 1.0), title="discrepancy", legend = :none); - - plot(p00, p01, p02, p03, layout=@layout([A B C D]), - framestyle=nothing, showaxis=false, - xticks=false, yticks=false, - size=(1200, 500), - plot_title="True vals: $(true_vals) - fitted vals: $(output)", - plot_titlevspan=0.2 - ) -end + begin + p00 = heatmap(sample_data, aspect_ratio=1.0, clim=(0.0, 1.0), title="Sample data", legend = :none); + p01 = heatmap(fitting_data, aspect_ratio=1.0, clim=(0.0,1.0), title="Fitting data", legend = :none); + p02 = heatmap(f_affine(output), aspect_ratio=1.0, clim=(0.0,1.0), title="estimated fit", legend = :none); + p03 = heatmap(fitting_data .- f_affine(output), aspect_ratio=1.0, clim=(0.0, 1.0), title="discrepancy", legend = :none); + + plot(p00, p01, p02, p03, layout=@layout([A B C D]), + framestyle=nothing, showaxis=false, + xticks=false, yticks=false, + size=(1200, 500), + plot_title="True vals: $(true_vals) + fitted vals: $(output)", + plot_titlevspan=0.2 + ) + end +end diff --git a/src/DataToFunctions.jl b/src/DataToFunctions.jl index 9188d6b..af6016e 100644 --- a/src/DataToFunctions.jl +++ b/src/DataToFunctions.jl @@ -3,7 +3,7 @@ using Interpolations using FourierTools using StaticArrays -export get_function, get_function_affine, add_dim, red_dim_apply, red_dim, f, func_transform +export get_function, get_function_affine, add_dim, red_dim_apply, red_dim, mat_mul, func_transform export extrapolate, interpolate """ @@ -60,16 +60,20 @@ function add_dim(cind) return SVector.((Tuple(cind))..., 1) end +@inline function red_dim(svec::SVector{S,T})::SVector{S-1,T} where {S,T} + return @view svec[1:S-1] +end + @inline function red_dim_apply(fct::AbstractArray{R}, svec::SVector{S,T})::R where {S,T, R} - return fct((@view svec[1:2])...) + return fct((@view svec[1:S-1])...) end -@inline function red_dim(svec::SVector{S,T})::SVector{S-1,T} where {S,T} - return @view svec[1:S-1] +@inline function idx_apply(fct::AbstractArray{R}, svec::SVector{S,T})::R where {S,T, R} + return fct(svec...) end # multiplying the transformation matrix -@inline function f(t::SVector{N, Int64}, matrix_c::SMatrix{N,N,T})::SVector{N,T} where {N,T} +@inline function mat_mul(t::SVector{N, Int64}, matrix_c::SMatrix{N,N,T})::SVector{N,T} where {N,T} return matrix_c * t end @@ -80,11 +84,74 @@ end end +# How to run this function: +# f_general((t) -> (t[1]*1.01, t[2]*1.01), out2) +""" + apply_transform_tuple!(coord_transf_func::Function, data, itp, out) + +applies a tuple-based coordinate transformation function to the indices of an array and returns the transformed array + +coord_transf_func: A function that takes a tuple of coordinates and returns a new tuple of coordinates +data: The data to transform +itp: The interpolation object to use + +# Example +```jldoctest +``` +""" +function apply_transform_tuple!(coord_transf_func::Function, data, itp, out) + out .= red_dim_apply.(Ref(itp), func_transform.(CartesianIndices(data), Ref(coord_transf_func))); +end + +""" + apply_transform_svec!(coord_transf_func::Function, data, itp, out) +applies a homogeneous coordinate-based coordinate transformation function to the indices of an array and returns the transformed array + +coord_transf_func: A function that takes a N-+1 dimensional homogeneous SVector returns a new N+1 dimensional SVector +data: The data to transform +itp: The interpolation object to use + +""" +function apply_transform_svec!(coord_transf_func::Function, data, itp, out) + out .= idx_apply.(Ref(itp), coord_transf_func.(CartesianIndices(data))); +end + +""" + apply_transform_homogen!(coord_transf_func::Function, data, itp, out) +applies a homogeneous coordinate-based coordinate transformation function to the indices of an array and returns the transformed array + +coord_transf_func: A function that takes a N-+1 dimensional homogeneous SVector returns a new N+1 dimensional SVector +data: The data to transform +itp: The interpolation object to use + +""" +function apply_transform_homogen!(coord_transf_func::Function, data, itp, out) + h_coord_transf_func = (c) -> red_dim(coord_transf_func(add_dim(c))) + apply_transform_svec!(h_coord_transf_func, data, itp, out); + # out .= itp.(red_dim.(coord_transf_func.(add_dim.(CartesianIndices(data))))); + # out .= red_dim_apply.(Ref(itp), coord_transf_func.(add_dim.(CartesianIndices(data)))); +end + +function apply_transform_affine!(mymat, data, itp, out) + # red_dim_apply.(Ref(itp), func_transform.(CartesianIndices(data), Ref(coord_transf_func))); + # return red_dim_apply.(Ref(itp), mat_mul.(add_dim.(CartesianIndices(data)), Ref(mymat))); + + homogenous_transform = (c) -> mat_mul(c, mymat) + apply_transform_homogen!(homogenous_transform, data, itp, out); + #return out +end + +function get_function_homogen(data::AbstractArray{T}, fct_hom::Function; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where T +end + """ get_function_affine(data::AbstractArray; super_sampling=1, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) -returns a function `interpolated(p)` which generates a transformed version of the original data. +returns a function `interpolated(p, [out])` which generates a transformed version of the original data parameterized by transform parameters. This is useful for fitting with a function which is itself defined by measured data. +The returned function supports two ways to be used, with an affine transform matrix `p` as in input or with a vector `p` of parameters. +The optional argument `out` can be used to store the result of the transformation. + # Arguments `data`: The data to represent by the function `dat` @@ -101,47 +168,30 @@ function get_function_affine(data::AbstractArray{T}; super_sampling=2, extrapola itp = extrapolate(interpolate(data, interp_type), extrapolation_bc); - function interpolated(matrix_c::SMatrix) - - # init a new array for the output - out = data - - # x_cen, y_cen = (size(data) .÷ 2.0 .+1) - - # t_orig_upsampled = @SMatrix [1.0 0.0 -1.0*x_cen_up; 0.0 1.0 -1.0*y_cen_up; 0.0 0.0 1.0] - - # building the overall transformation matrix - # matrix_c = SMatrix{ndims(data)+1, ndims(data)+1, T}(reshape(p, ndims(data)+1, ndims(data)+1)) - - #print(eltype(matrix_c)) - # looping all over the catesian indedices of the input image, - # first ading a new value to its third dimenstion: 1.0, - # converting to the new indices using the transformation matrix and then, - # using the "itp" object, we build the transfomed image "out" + @inline function interpolated(matrix_c::SMatrix, out = similar(data)) - #for I1 in CartesianIndices(data) - #print("INSIde the loop!!") - #print(eltype(SVector{ndims(data)+1, T}(Tuple(I1)..., 1))) - # out[I1] = itp(f(SVector{ndims(data)+1, T}(Tuple(I1)..., 1.0), matrix_c)[1:2]...) - #print(eltype(out[I1])) - #end + # # init a new array for the output + # out = similar(data) + # out = data - out[CartesianIndices(data)] .= red_dim_apply.(Ref(itp), f.(add_dim.(CartesianIndices(data)), Ref(matrix_c))); + # out[CartesianIndices(data)] .= red_dim_apply.(Ref(itp), mat_mul.(add_dim.(CartesianIndices(data)), Ref(matrix_c))); + return apply_transform_affine!(matrix_c, data, itp, out); + # return red_dim_apply.(Ref(itp), f.(add_dim.(CartesianIndices(data)), Ref(matrix_c))); - return out + # return out end - function interpolated(matrix_c::SMatrix, out::AbstractArray) + # function interpolated!(matrix_c::SMatrix, out::AbstractArray) - out[CartesianIndices(data)] .= red_dim_apply.(Ref(itp), f.(add_dim.(CartesianIndices(data)), Ref(matrix_c))); + # out[CartesianIndices(data)] .= red_dim_apply.(Ref(itp), f.(add_dim.(CartesianIndices(data)), Ref(matrix_c))); - #return out - end + # #return out + # end - function interpolated(p::AbstractVector{T}) where T + @inline function interpolated(p::AbstractVector{T}, out = similar(data)) where T # init a new array for the output - out = data + # out = data x_cen, y_cen = (size(data) .÷ 2.0 .+1) # x_cen_up, y_cen_up = (size(upsampled) .÷ 2.0 .+ 1.0) @@ -168,42 +218,34 @@ function get_function_affine(data::AbstractArray{T}; super_sampling=2, extrapola # out[I1] = itp(f(SVector(Tuple(I1)..., 1), matrix_c)[1:2]...) #end - out[CartesianIndices(data)] .= red_dim_apply.(Ref(itp), f.(add_dim.(CartesianIndices(data)), Ref(matrix_c))); - - return out - end - - - function interpolated(p::AbstractVector{T}, out::AbstractArray) where T - - x_cen, y_cen = (size(data) .÷ 2.0 .+1) - # x_cen_up, y_cen_up = (size(upsampled) .÷ 2.0 .+ 1.0) - - # creating the matrices of rotation, shear, scale, and shift - rot_mat = @SMatrix [cos(p[7]) -1.0*sin(p[7]) 0.0; sin(p[7]) cos(p[7]) 0.0; 0.0 0.0 1.0]; - shear_mat = @SMatrix [1.0 p[5] 0.0; p[6] 1.0 0.0; 0.0 0.0 1.0]; - scale_mat = @SMatrix [1/p[3] 0.0 0.0; 0.0 1/p[4] 0.0; 0.0 0.0 1.0]; - shift_mat = @SMatrix [1.0 0.0 -1*p[1]; 0.0 1.0 -1*p[2]; 0.0 0.0 1.0]; - t_to_origin = @SMatrix [1.0 0.0 1*x_cen; 0.0 1.0 y_cen; 0.0 0.0 1.0]; - t_to_center = @SMatrix [1.0 0.0 -1.0*x_cen; 0.0 1.0 -1.0*y_cen; 0.0 0.0 1.0]; - # t_orig_upsampled = @SMatrix [1.0 0.0 -1.0*x_cen_up; 0.0 1.0 -1.0*y_cen_up; 0.0 0.0 1.0] + # out[CartesianIndices(data)] .= red_dim_apply.(Ref(itp), f.(add_dim.(CartesianIndices(data)), Ref(matrix_c))); + return interpolated(matrix_c, out); + # return red_dim_apply.(Ref(itp), f.(add_dim.(CartesianIndices(data)), Ref(matrix_c))); - # building the overall transformation matrix - # matrix_c = t_to_origin * scale_mat * shear_mat * rot_mat *shift_mat * t_to_center - matrix_c = t_to_origin * scale_mat * rot_mat *shift_mat * t_to_center - - out[CartesianIndices(data)] .= red_dim_apply.(Ref(itp), f.(add_dim.(CartesianIndices(data)), Ref(matrix_c))); + # return out end - # How to run this function: - # f_general((t) -> (t[1]*1.01, t[2]*1.01), out2) - function interpolated(coord_transf_func::Function, out::AbstractArray) - - out[CartesianIndices(data)] .= red_dim_apply.(Ref(itp), func_transform.(CartesianIndices(data), Ref(coord_transf_func))); - #return out - end - + # function interpolated(p::AbstractVector{T}, out::AbstractArray) where T + + # x_cen, y_cen = (size(data) .÷ 2.0 .+1) + # # x_cen_up, y_cen_up = (size(upsampled) .÷ 2.0 .+ 1.0) + + # # creating the matrices of rotation, shear, scale, and shift + # rot_mat = @SMatrix [cos(p[7]) -1.0*sin(p[7]) 0.0; sin(p[7]) cos(p[7]) 0.0; 0.0 0.0 1.0]; + # shear_mat = @SMatrix [1.0 p[5] 0.0; p[6] 1.0 0.0; 0.0 0.0 1.0]; + # scale_mat = @SMatrix [1/p[3] 0.0 0.0; 0.0 1/p[4] 0.0; 0.0 0.0 1.0]; + # shift_mat = @SMatrix [1.0 0.0 -1*p[1]; 0.0 1.0 -1*p[2]; 0.0 0.0 1.0]; + # t_to_origin = @SMatrix [1.0 0.0 1*x_cen; 0.0 1.0 y_cen; 0.0 0.0 1.0]; + # t_to_center = @SMatrix [1.0 0.0 -1.0*x_cen; 0.0 1.0 -1.0*y_cen; 0.0 0.0 1.0]; + # # t_orig_upsampled = @SMatrix [1.0 0.0 -1.0*x_cen_up; 0.0 1.0 -1.0*y_cen_up; 0.0 0.0 1.0] + + # # building the overall transformation matrix + # # matrix_c = t_to_origin * scale_mat * shear_mat * rot_mat *shift_mat * t_to_center + # matrix_c = t_to_origin * scale_mat * rot_mat *shift_mat * t_to_center + + # out[CartesianIndices(data)] .= red_dim_apply.(Ref(itp), f.(add_dim.(CartesianIndices(data)), Ref(matrix_c))); + # end return interpolated end From 09f49bf6eca8427047b4b51fcb27a856294b4538 Mon Sep 17 00:00:00 2001 From: RainerHeintzmann Date: Wed, 3 Apr 2024 12:49:01 +0200 Subject: [PATCH 12/44] cleanup --- src/DataToFunctions.jl | 87 ++++++++++++------------------------------ 1 file changed, 25 insertions(+), 62 deletions(-) diff --git a/src/DataToFunctions.jl b/src/DataToFunctions.jl index af6016e..701832c 100644 --- a/src/DataToFunctions.jl +++ b/src/DataToFunctions.jl @@ -141,7 +141,25 @@ function apply_transform_affine!(mymat, data, itp, out) #return out end -function get_function_homogen(data::AbstractArray{T}, fct_hom::Function; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where T +function get_function_tuple(data::AbstractArray{T}, fct_tup::Function; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where T + # building the extraplation + interpolation object + itp = extrapolate(interpolate(data, interp_type), extrapolation_bc); + function interpolated(params, out = similar(data)) + fct_tup_noparams(c) = fct_tup(c, params) + return apply_transform_tuple!(fct_tup_noparams, data, itp, out); + end + return interpolated +end + +function get_function_svec(data::AbstractArray{T}, fct_hom::Function; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where T + # building the extraplation + interpolation object + itp = extrapolate(interpolate(data, interp_type), extrapolation_bc); + function interpolated(params::SVector, out = similar(data)) + fct_hom_noparams(c) = fct_hom(c, params) + apply_transform_homogen!(fct_hom_noparams, data, itp, out); + return out; + end + return interpolated end """ @@ -167,32 +185,12 @@ function get_function_affine(data::AbstractArray{T}; super_sampling=2, extrapola # building the extraplation + interpolation object itp = extrapolate(interpolate(data, interp_type), extrapolation_bc); - - @inline function interpolated(matrix_c::SMatrix, out = similar(data)) - - # # init a new array for the output - # out = similar(data) - # out = data - - # out[CartesianIndices(data)] .= red_dim_apply.(Ref(itp), mat_mul.(add_dim.(CartesianIndices(data)), Ref(matrix_c))); - return apply_transform_affine!(matrix_c, data, itp, out); - # return red_dim_apply.(Ref(itp), f.(add_dim.(CartesianIndices(data)), Ref(matrix_c))); - - # return out + function interpolated(matrix_c::SMatrix{T}, out = similar(data)) where T + apply_transform_affine!(matrix_c, data, itp, out); + return out; end - # function interpolated!(matrix_c::SMatrix, out::AbstractArray) - - # out[CartesianIndices(data)] .= red_dim_apply.(Ref(itp), f.(add_dim.(CartesianIndices(data)), Ref(matrix_c))); - - # #return out - # end - - @inline function interpolated(p::AbstractVector{T}, out = similar(data)) where T - - # init a new array for the output - # out = data - + function interpolated(p::AbstractVector{T}, out = similar(data)) where T x_cen, y_cen = (size(data) .÷ 2.0 .+1) # x_cen_up, y_cen_up = (size(upsampled) .÷ 2.0 .+ 1.0) @@ -206,47 +204,12 @@ function get_function_affine(data::AbstractArray{T}; super_sampling=2, extrapola # t_orig_upsampled = @SMatrix [1.0 0.0 -1.0*x_cen_up; 0.0 1.0 -1.0*y_cen_up; 0.0 0.0 1.0] # building the overall transformation matrix - # matrix_c = t_to_origin * scale_mat * shear_mat * rot_mat *shift_mat * t_to_center matrix_c = t_to_origin * scale_mat * rot_mat *shift_mat * t_to_center - # looping all over the catesian indedices of the input image, - # first ading a new value to its third dimenstion: 1.0, - # converting to the new indices using the transformation matrix and then, - # using the "itp" object, we build the transfomed image "out" - - #for I1 in CartesianIndices(data) - # out[I1] = itp(f(SVector(Tuple(I1)..., 1), matrix_c)[1:2]...) - #end - - # out[CartesianIndices(data)] .= red_dim_apply.(Ref(itp), f.(add_dim.(CartesianIndices(data)), Ref(matrix_c))); - return interpolated(matrix_c, out); - # return red_dim_apply.(Ref(itp), f.(add_dim.(CartesianIndices(data)), Ref(matrix_c))); - - # return out + apply_transform_affine!(matrix_c, data, itp, out); # do not call interolated here for type stability reasons + return out; end - - # function interpolated(p::AbstractVector{T}, out::AbstractArray) where T - - # x_cen, y_cen = (size(data) .÷ 2.0 .+1) - # # x_cen_up, y_cen_up = (size(upsampled) .÷ 2.0 .+ 1.0) - - # # creating the matrices of rotation, shear, scale, and shift - # rot_mat = @SMatrix [cos(p[7]) -1.0*sin(p[7]) 0.0; sin(p[7]) cos(p[7]) 0.0; 0.0 0.0 1.0]; - # shear_mat = @SMatrix [1.0 p[5] 0.0; p[6] 1.0 0.0; 0.0 0.0 1.0]; - # scale_mat = @SMatrix [1/p[3] 0.0 0.0; 0.0 1/p[4] 0.0; 0.0 0.0 1.0]; - # shift_mat = @SMatrix [1.0 0.0 -1*p[1]; 0.0 1.0 -1*p[2]; 0.0 0.0 1.0]; - # t_to_origin = @SMatrix [1.0 0.0 1*x_cen; 0.0 1.0 y_cen; 0.0 0.0 1.0]; - # t_to_center = @SMatrix [1.0 0.0 -1.0*x_cen; 0.0 1.0 -1.0*y_cen; 0.0 0.0 1.0]; - # # t_orig_upsampled = @SMatrix [1.0 0.0 -1.0*x_cen_up; 0.0 1.0 -1.0*y_cen_up; 0.0 0.0 1.0] - - # # building the overall transformation matrix - # # matrix_c = t_to_origin * scale_mat * shear_mat * rot_mat *shift_mat * t_to_center - # matrix_c = t_to_origin * scale_mat * rot_mat *shift_mat * t_to_center - - # out[CartesianIndices(data)] .= red_dim_apply.(Ref(itp), f.(add_dim.(CartesianIndices(data)), Ref(matrix_c))); - # end - return interpolated end From 82d886c9e19c66300f08d1f7f0310845f28a75ee Mon Sep 17 00:00:00 2001 From: RainerHeintzmann Date: Wed, 3 Apr 2024 16:33:55 +0200 Subject: [PATCH 13/44] Tried taylor_test.jl --- examples/Project.toml | 1 + examples/taylor_test.jl | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 examples/taylor_test.jl diff --git a/examples/Project.toml b/examples/Project.toml index f6ed356..9b4fdde 100644 --- a/examples/Project.toml +++ b/examples/Project.toml @@ -14,6 +14,7 @@ Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" RandomExtensions = "fb686558-2515-59ef-acaa-46db3789a887" Rotations = "6038ab10-8711-5258-84ad-4b1120ba62dc" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +TaylorSeries = "6aa5eb33-94cf-58f4-a9d0-e4b2c4fc25ea" TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990" TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" View5D = "90d841e0-6953-4e90-9f3a-43681da8e949" diff --git a/examples/taylor_test.jl b/examples/taylor_test.jl new file mode 100644 index 0000000..b4f9c88 --- /dev/null +++ b/examples/taylor_test.jl @@ -0,0 +1,13 @@ +using TaylorSeries +using StaticArrays + +function to_SVec(c::CartesianIndex) + return [Tuple(c)...] +end + +function main() + t = set_variables("x", numvars=3, order=4) + p = exp.(t) + @time q = evaluate.(Ref(p), to_SVec.(CartesianIndices(img))); + @time q .= evaluate.(Ref(p), to_SVec.(CartesianIndices(img))); +end From 9790667aed58f732c265e7bfd1feb7977b895ea2 Mon Sep 17 00:00:00 2001 From: RainerHeintzmann Date: Thu, 4 Apr 2024 18:10:56 +0200 Subject: [PATCH 14/44] added a test for a multidimensional taylor expansion --- examples/taylor_test.jl | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/examples/taylor_test.jl b/examples/taylor_test.jl index b4f9c88..893054a 100644 --- a/examples/taylor_test.jl +++ b/examples/taylor_test.jl @@ -1,13 +1,37 @@ -using TaylorSeries -using StaticArrays +# using TaylorSeries +# using StaticArrays -function to_SVec(c::CartesianIndex) - return [Tuple(c)...] +# function to_SVec(c::CartesianIndex) +# return [Tuple(c)...] +# end + +# function main() +# t = set_variables("x", numvars=3, order=4) +# p = exp.(t) +# @time q = evaluate.(Ref(p), to_SVec.(CartesianIndices(img))); +# @time q .= evaluate.(Ref(p), to_SVec.(CartesianIndices(img))); +# end + + +function get_polynomial(::Val{numvars}, ::Val{0}) where {numvars} + # @info "Creating polynomials of order 0" + return (t, c) -> begin + # println("c: $(c) $(length(c))"); + c[1] + end +end + +function get_polynomial(::Val{numvars}, ::Val{N}) where {numvars, N} + # @info "Creating polynomials with $(numvars) variables of order , $(N). Required constants: $((numvars+1)^N)" + p1 = get_polynomial(Val(numvars), Val(N-1)); # is reused multiple times + return (t, c) -> begin + # println("N: $(N), c: $(c) $(length(c))"); + p1(t, c[1:length(c)/(numvars+1)]) + sum(p1(t, c[1+n*length(c)/(numvars+1):(n+1)*length(c)/(numvars+1)]) * t[n] for n in 1:numvars) + end end function main() - t = set_variables("x", numvars=3, order=4) - p = exp.(t) - @time q = evaluate.(Ref(p), to_SVec.(CartesianIndices(img))); - @time q .= evaluate.(Ref(p), to_SVec.(CartesianIndices(img))); + p = p = get_polynomial(Val(2), Val(3)) # 27 indices required + @time p.(Tuple.(CartesianIndices((100,100))),Ref((1.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))); end + From 3fcc2905427e90e20db24c8df6184b4e5a097061 Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Fri, 5 Apr 2024 11:27:03 +0200 Subject: [PATCH 15/44] Fixed the speed and allocations, tip: don't assign `Array` type to `itp` --- examples/PSF_fitting_general.jl | 129 ++++++++++++++++---------------- src/DataToFunctions.jl | 4 +- 2 files changed, 67 insertions(+), 66 deletions(-) diff --git a/examples/PSF_fitting_general.jl b/examples/PSF_fitting_general.jl index a41fd7c..ddd43c7 100644 --- a/examples/PSF_fitting_general.jl +++ b/examples/PSF_fitting_general.jl @@ -106,99 +106,100 @@ end function main() -Random.seed!(1234) -# Base.show(io::IO, f::Float64) = @printf(io, "%.3f", f) - -# size of the test array to fit -size_arr = 22 -noise_level = 10.0; + Random.seed!(1234) + # Base.show(io::IO, f::Float64) = @printf(io, "%.3f", f) + + # size of the test array to fit + size_arr = 22 + noise_level = 10.0; -# defining the mean and the varixance of the test normal (Gaussian) distribution -μ = [0, 0] -Σ = [size_arr/10 0.0; - 0.0 size_arr/10] + # defining the mean and the varixance of the test normal (Gaussian) distribution + μ = [0, 0] + Σ = [size_arr/10 0.0; + 0.0 size_arr/10] -Σ_d = [2 1.5; - 1.5 2] + Σ_d = [2 1.5; + 1.5 2] -# initializing the multivariate normal distribution -p = MvNormal(μ, Σ) + # initializing the multivariate normal distribution + p = MvNormal(μ, Σ) -# this part of the code is to define the sample array based on a 2D normal distribution -X = -1*size_arr/2.0:1*size_arr/2.0 -Y = -1*size_arr/2.0:1*size_arr/2.0 + # this part of the code is to define the sample array based on a 2D normal distribution + X = -1*size_arr/2.0:1*size_arr/2.0 + Y = -1*size_arr/2.0:1*size_arr/2.0 -z = [pdf(p, [x,y]) for y in Y, x in X] + z = [pdf(p, [x,y]) for y in Y, x in X] -# @vv z + # @vv z -dtype = Float64 + dtype = Float64 -# setting a typical values for the shift (1:2) and scale (3:4) -true_vals = dtype.([0.2, -1.2, 1.0, 1.0, 0.0, 0.0, pi/3]) #pi/6] -#true_vals = [2.3, -1.2, 0.9, 2.1, 0.1, 0.05, pi/2, 2.0, 3.0] + # setting a typical values for the shift (1:2) and scale (3:4) + true_vals = dtype.([0.2, -1.2, 1.0, 1.0, 0.0, 0.0, pi/3]) #pi/6] + #true_vals = [2.3, -1.2, 0.9, 2.1, 0.1, 0.05, pi/2, 2.0, 3.0] -# normalizing the sample data -#sample_data = Float32.(z./maximum(z)) -sample_data = dtype.(TestImages.shepp_logan(32)); -#sample_data = rand(dtype, (size_arr, size_arr)) + # normalizing the sample data + #sample_data = Float32.(z./maximum(z)) + sample_data = dtype.(TestImages.shepp_logan(32)); + #sample_data = rand(dtype, (size_arr, size_arr)) -#sample_data .+= rand(dtype, (size(sample_data)...))./noise_level; + #sample_data .+= rand(dtype, (size(sample_data)...))./noise_level; -x_cen, y_cen = (size(sample_data) .÷ 2.0 .+1) + x_cen, y_cen = (size(sample_data) .÷ 2.0 .+1) -shear_mat = [1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0]; -scale_mat = [1/1.2 0.0 0.0; 0.0 1/1.8 0.0; 0.0 0.0 1.0]; + shear_mat = [1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0]; + scale_mat = [1/1.2 0.0 0.0; 0.0 1/1.8 0.0; 0.0 0.0 1.0]; -t_to_origin = [1.0 0.0 1*x_cen; 0.0 1.0 y_cen; 0.0 0.0 1.0]; -t_to_center = [1.0 0.0 -1.0*x_cen; 0.0 1.0 -1.0*y_cen; 0.0 0.0 1.0]; + t_to_origin = [1.0 0.0 1*x_cen; 0.0 1.0 y_cen; 0.0 0.0 1.0]; + t_to_center = [1.0 0.0 -1.0*x_cen; 0.0 1.0 -1.0*y_cen; 0.0 0.0 1.0]; -# converting the data to function (DataToFunctions.get_function) -f_affine = get_function_affine(sample_data);#; super_sampling=1);#, extrapolation_bc=0.0); + # converting the data to function (DataToFunctions.get_function) + f_affine = get_function_affine(sample_data);#; super_sampling=1);#, extrapolation_bc=0.0); -ang = rand((0.0:0.1:pi)) -rot_mat = [cos(ang) -1.0*sin(ang) 0.0; sin(ang) cos(ang) 0.0; 0.0 0.0 1.0]; + ang = rand((0.0:0.1:pi)) + rot_mat = [cos(ang) -1.0*sin(ang) 0.0; sin(ang) cos(ang) 0.0; 0.0 0.0 1.0]; -matrix_c = dtype.(t_to_origin * scale_mat * shear_mat * rot_mat * t_to_center ) + matrix_c = dtype.(t_to_origin * scale_mat * shear_mat * rot_mat * t_to_center ) -# f_d = get_function_loop(sample_data_d; super_sampling=1);#, extrapolation_bc=0.0); + # f_d = get_function_loop(sample_data_d; super_sampling=1);#, extrapolation_bc=0.0); -# adding some scaled random noise to the fitting data -# fitting_data = f_general(SMatrix{3,3}(matrix_c)); #.+ rand(size(sample_data)...)./100.0; -fitting_data = f_affine(true_vals) .+ dtype.(rand(size(sample_data)...))./5.0; + f2 = similar(sample_data) + # adding some scaled random noise to the fitting data + # fitting_data = f_general(SMatrix{3,3}(matrix_c)); #.+ rand(size(sample_data)...)./100.0; + f_affine(true_vals, f2); #.+ dtype.(rand(size(sample_data)...))./5.0; -plot(heatmap(sample_data, aspect_ratio=1), heatmap(fitting_data, aspect_ratio=1)) + plot(heatmap(sample_data, aspect_ratio=1), heatmap(fitting_data, aspect_ratio=1)) -# defining the loss function based on the gaussian noise -loss(p1::AbstractVector) = sum(abs2.(f_affine(p1::AbstractVector) .- fitting_data)) -# loss(x) = loss(x::AbstractVector{T} where T) + # defining the loss function based on the gaussian noise + loss(p1::AbstractVector) = sum(abs2.(f_affine(p1::AbstractVector) .- fitting_data)) + # loss(x) = loss(x::AbstractVector{T} where T) -# loss(p3) = loss([p3[1], p3[2], p3[3], p3[4], p3[5], p3[6], p3[7]]) + # loss(p3) = loss([p3[1], p3[2], p3[3], p3[4], p3[5], p3[6], p3[7]]) -# perform the main fit to the fitting data by minimizing the loss function -@time output, res = perform_fit_general(loss, fitting_data) + # perform the main fit to the fitting data by minimizing the loss function + @time output, res = perform_fit_general(loss, fitting_data) -# plotting the output of the fitting pocedure for further illustration - begin - p00 = heatmap(sample_data, aspect_ratio=1.0, clim=(0.0, 1.0), title="Sample data", legend = :none); - p01 = heatmap(fitting_data, aspect_ratio=1.0, clim=(0.0,1.0), title="Fitting data", legend = :none); - p02 = heatmap(f_affine(output), aspect_ratio=1.0, clim=(0.0,1.0), title="estimated fit", legend = :none); - p03 = heatmap(fitting_data .- f_affine(output), aspect_ratio=1.0, clim=(0.0, 1.0), title="discrepancy", legend = :none); + # plotting the output of the fitting pocedure for further illustration + begin + p00 = heatmap(sample_data, aspect_ratio=1.0, clim=(0.0, 1.0), title="Sample data", legend = :none); + p01 = heatmap(fitting_data, aspect_ratio=1.0, clim=(0.0,1.0), title="Fitting data", legend = :none); + p02 = heatmap(f_affine(output), aspect_ratio=1.0, clim=(0.0,1.0), title="estimated fit", legend = :none); + p03 = heatmap(fitting_data .- f_affine(output), aspect_ratio=1.0, clim=(0.0, 1.0), title="discrepancy", legend = :none); - plot(p00, p01, p02, p03, layout=@layout([A B C D]), - framestyle=nothing, showaxis=false, - xticks=false, yticks=false, - size=(1200, 500), - plot_title="True vals: $(true_vals) - fitted vals: $(output)", - plot_titlevspan=0.2 - ) - end + plot(p00, p01, p02, p03, layout=@layout([A B C D]), + framestyle=nothing, showaxis=false, + xticks=false, yticks=false, + size=(1200, 500), + plot_title="True vals: $(true_vals) + fitted vals: $(output)", + plot_titlevspan=0.2 + ) + end end diff --git a/src/DataToFunctions.jl b/src/DataToFunctions.jl index 701832c..2634eb5 100644 --- a/src/DataToFunctions.jl +++ b/src/DataToFunctions.jl @@ -64,11 +64,11 @@ end return @view svec[1:S-1] end -@inline function red_dim_apply(fct::AbstractArray{R}, svec::SVector{S,T})::R where {S,T, R} +@inline function red_dim_apply(fct, svec::SVector{S,T})::R where {S,T, R} return fct((@view svec[1:S-1])...) end -@inline function idx_apply(fct::AbstractArray{R}, svec::SVector{S,T})::R where {S,T, R} +@inline function idx_apply(fct, svec::SVector{S,T}) where {S,T} return fct(svec...) end From c2338f944f00296a616acda63cf01e4966414f14 Mon Sep 17 00:00:00 2001 From: RainerHeintzmann Date: Fri, 5 Apr 2024 11:56:28 +0200 Subject: [PATCH 16/44] fixed polynomial memory up to order 2 --- examples/taylor_test.jl | 54 +++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/examples/taylor_test.jl b/examples/taylor_test.jl index 893054a..d506fe1 100644 --- a/examples/taylor_test.jl +++ b/examples/taylor_test.jl @@ -1,37 +1,49 @@ -# using TaylorSeries -# using StaticArrays - -# function to_SVec(c::CartesianIndex) -# return [Tuple(c)...] -# end - -# function main() -# t = set_variables("x", numvars=3, order=4) -# p = exp.(t) -# @time q = evaluate.(Ref(p), to_SVec.(CartesianIndices(img))); -# @time q .= evaluate.(Ref(p), to_SVec.(CartesianIndices(img))); -# end - - function get_polynomial(::Val{numvars}, ::Val{0}) where {numvars} # @info "Creating polynomials of order 0" return (t, c) -> begin - # println("c: $(c) $(length(c))"); - c[1] + # println("c: $(c) $(length(c))"); + c[1] end end function get_polynomial(::Val{numvars}, ::Val{N}) where {numvars, N} # @info "Creating polynomials with $(numvars) variables of order , $(N). Required constants: $((numvars+1)^N)" p1 = get_polynomial(Val(numvars), Val(N-1)); # is reused multiple times - return (t, c) -> begin + p2(t, c) = begin + s = p1(t, c[1+length(c)÷(numvars+1):(2)*length(c)÷(numvars+1)]) * t[1] # int devision needed for type stability! + # s = 0 + for n in 2:numvars + s += p1(t, c[1+n*length(c)÷(numvars+1):(n+1)*length(c)÷(numvars+1)]) * t[n] # int devision needed for type stability! + end + return s + end + function p3(t, c) # println("N: $(N), c: $(c) $(length(c))"); - p1(t, c[1:length(c)/(numvars+1)]) + sum(p1(t, c[1+n*length(c)/(numvars+1):(n+1)*length(c)/(numvars+1)]) * t[n] for n in 1:numvars) + p1(t, c[1:length(c)÷(numvars+1)]) + p2(t,c) # int devision needed for type stability! end + + return p3 end function main() - p = p = get_polynomial(Val(2), Val(3)) # 27 indices required - @time p.(Tuple.(CartesianIndices((100,100))),Ref((1.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))); + (2+1)^3 + p = get_polynomial(Val(2), Val(3)) # 27 indices required + @time p.(Tuple.(CartesianIndices((200,200))),Ref((1.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))); + # does allocate + + (2+1)^2 + p = get_polynomial(Val(2), Val(2)) # 3 indices required + @time p.(Tuple.(CartesianIndices((200,200))),Ref((1.1,2.1,3.1,4,5,6,7,8,9))); + # essentially allocation-free + + (2+1)^1 + p = get_polynomial(Val(1), Val(1)) # 3 indices required + @time p.(Tuple.(CartesianIndices((200,200))),Ref((1.1,2.1,3.1))); + # essentially allocation-free + p((100,),((1.1,2.2, 3.3))) + + p = get_polynomial(Val(1), Val(0)) # 27 indices required + @time p.(Tuple.(CartesianIndices((100,100))),Ref((1.1))); + # essentially allocation-free end From 739bbc954b20ae8bbbe02c80b1ca8277ad7a46ef Mon Sep 17 00:00:00 2001 From: RainerHeintzmann Date: Fri, 5 Apr 2024 11:57:16 +0200 Subject: [PATCH 17/44] merged --- examples/PSF_fitting_general.jl | 129 ++++++++++++++++---------------- src/DataToFunctions.jl | 4 +- 2 files changed, 67 insertions(+), 66 deletions(-) diff --git a/examples/PSF_fitting_general.jl b/examples/PSF_fitting_general.jl index a41fd7c..ddd43c7 100644 --- a/examples/PSF_fitting_general.jl +++ b/examples/PSF_fitting_general.jl @@ -106,99 +106,100 @@ end function main() -Random.seed!(1234) -# Base.show(io::IO, f::Float64) = @printf(io, "%.3f", f) - -# size of the test array to fit -size_arr = 22 -noise_level = 10.0; + Random.seed!(1234) + # Base.show(io::IO, f::Float64) = @printf(io, "%.3f", f) + + # size of the test array to fit + size_arr = 22 + noise_level = 10.0; -# defining the mean and the varixance of the test normal (Gaussian) distribution -μ = [0, 0] -Σ = [size_arr/10 0.0; - 0.0 size_arr/10] + # defining the mean and the varixance of the test normal (Gaussian) distribution + μ = [0, 0] + Σ = [size_arr/10 0.0; + 0.0 size_arr/10] -Σ_d = [2 1.5; - 1.5 2] + Σ_d = [2 1.5; + 1.5 2] -# initializing the multivariate normal distribution -p = MvNormal(μ, Σ) + # initializing the multivariate normal distribution + p = MvNormal(μ, Σ) -# this part of the code is to define the sample array based on a 2D normal distribution -X = -1*size_arr/2.0:1*size_arr/2.0 -Y = -1*size_arr/2.0:1*size_arr/2.0 + # this part of the code is to define the sample array based on a 2D normal distribution + X = -1*size_arr/2.0:1*size_arr/2.0 + Y = -1*size_arr/2.0:1*size_arr/2.0 -z = [pdf(p, [x,y]) for y in Y, x in X] + z = [pdf(p, [x,y]) for y in Y, x in X] -# @vv z + # @vv z -dtype = Float64 + dtype = Float64 -# setting a typical values for the shift (1:2) and scale (3:4) -true_vals = dtype.([0.2, -1.2, 1.0, 1.0, 0.0, 0.0, pi/3]) #pi/6] -#true_vals = [2.3, -1.2, 0.9, 2.1, 0.1, 0.05, pi/2, 2.0, 3.0] + # setting a typical values for the shift (1:2) and scale (3:4) + true_vals = dtype.([0.2, -1.2, 1.0, 1.0, 0.0, 0.0, pi/3]) #pi/6] + #true_vals = [2.3, -1.2, 0.9, 2.1, 0.1, 0.05, pi/2, 2.0, 3.0] -# normalizing the sample data -#sample_data = Float32.(z./maximum(z)) -sample_data = dtype.(TestImages.shepp_logan(32)); -#sample_data = rand(dtype, (size_arr, size_arr)) + # normalizing the sample data + #sample_data = Float32.(z./maximum(z)) + sample_data = dtype.(TestImages.shepp_logan(32)); + #sample_data = rand(dtype, (size_arr, size_arr)) -#sample_data .+= rand(dtype, (size(sample_data)...))./noise_level; + #sample_data .+= rand(dtype, (size(sample_data)...))./noise_level; -x_cen, y_cen = (size(sample_data) .÷ 2.0 .+1) + x_cen, y_cen = (size(sample_data) .÷ 2.0 .+1) -shear_mat = [1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0]; -scale_mat = [1/1.2 0.0 0.0; 0.0 1/1.8 0.0; 0.0 0.0 1.0]; + shear_mat = [1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0]; + scale_mat = [1/1.2 0.0 0.0; 0.0 1/1.8 0.0; 0.0 0.0 1.0]; -t_to_origin = [1.0 0.0 1*x_cen; 0.0 1.0 y_cen; 0.0 0.0 1.0]; -t_to_center = [1.0 0.0 -1.0*x_cen; 0.0 1.0 -1.0*y_cen; 0.0 0.0 1.0]; + t_to_origin = [1.0 0.0 1*x_cen; 0.0 1.0 y_cen; 0.0 0.0 1.0]; + t_to_center = [1.0 0.0 -1.0*x_cen; 0.0 1.0 -1.0*y_cen; 0.0 0.0 1.0]; -# converting the data to function (DataToFunctions.get_function) -f_affine = get_function_affine(sample_data);#; super_sampling=1);#, extrapolation_bc=0.0); + # converting the data to function (DataToFunctions.get_function) + f_affine = get_function_affine(sample_data);#; super_sampling=1);#, extrapolation_bc=0.0); -ang = rand((0.0:0.1:pi)) -rot_mat = [cos(ang) -1.0*sin(ang) 0.0; sin(ang) cos(ang) 0.0; 0.0 0.0 1.0]; + ang = rand((0.0:0.1:pi)) + rot_mat = [cos(ang) -1.0*sin(ang) 0.0; sin(ang) cos(ang) 0.0; 0.0 0.0 1.0]; -matrix_c = dtype.(t_to_origin * scale_mat * shear_mat * rot_mat * t_to_center ) + matrix_c = dtype.(t_to_origin * scale_mat * shear_mat * rot_mat * t_to_center ) -# f_d = get_function_loop(sample_data_d; super_sampling=1);#, extrapolation_bc=0.0); + # f_d = get_function_loop(sample_data_d; super_sampling=1);#, extrapolation_bc=0.0); -# adding some scaled random noise to the fitting data -# fitting_data = f_general(SMatrix{3,3}(matrix_c)); #.+ rand(size(sample_data)...)./100.0; -fitting_data = f_affine(true_vals) .+ dtype.(rand(size(sample_data)...))./5.0; + f2 = similar(sample_data) + # adding some scaled random noise to the fitting data + # fitting_data = f_general(SMatrix{3,3}(matrix_c)); #.+ rand(size(sample_data)...)./100.0; + f_affine(true_vals, f2); #.+ dtype.(rand(size(sample_data)...))./5.0; -plot(heatmap(sample_data, aspect_ratio=1), heatmap(fitting_data, aspect_ratio=1)) + plot(heatmap(sample_data, aspect_ratio=1), heatmap(fitting_data, aspect_ratio=1)) -# defining the loss function based on the gaussian noise -loss(p1::AbstractVector) = sum(abs2.(f_affine(p1::AbstractVector) .- fitting_data)) -# loss(x) = loss(x::AbstractVector{T} where T) + # defining the loss function based on the gaussian noise + loss(p1::AbstractVector) = sum(abs2.(f_affine(p1::AbstractVector) .- fitting_data)) + # loss(x) = loss(x::AbstractVector{T} where T) -# loss(p3) = loss([p3[1], p3[2], p3[3], p3[4], p3[5], p3[6], p3[7]]) + # loss(p3) = loss([p3[1], p3[2], p3[3], p3[4], p3[5], p3[6], p3[7]]) -# perform the main fit to the fitting data by minimizing the loss function -@time output, res = perform_fit_general(loss, fitting_data) + # perform the main fit to the fitting data by minimizing the loss function + @time output, res = perform_fit_general(loss, fitting_data) -# plotting the output of the fitting pocedure for further illustration - begin - p00 = heatmap(sample_data, aspect_ratio=1.0, clim=(0.0, 1.0), title="Sample data", legend = :none); - p01 = heatmap(fitting_data, aspect_ratio=1.0, clim=(0.0,1.0), title="Fitting data", legend = :none); - p02 = heatmap(f_affine(output), aspect_ratio=1.0, clim=(0.0,1.0), title="estimated fit", legend = :none); - p03 = heatmap(fitting_data .- f_affine(output), aspect_ratio=1.0, clim=(0.0, 1.0), title="discrepancy", legend = :none); + # plotting the output of the fitting pocedure for further illustration + begin + p00 = heatmap(sample_data, aspect_ratio=1.0, clim=(0.0, 1.0), title="Sample data", legend = :none); + p01 = heatmap(fitting_data, aspect_ratio=1.0, clim=(0.0,1.0), title="Fitting data", legend = :none); + p02 = heatmap(f_affine(output), aspect_ratio=1.0, clim=(0.0,1.0), title="estimated fit", legend = :none); + p03 = heatmap(fitting_data .- f_affine(output), aspect_ratio=1.0, clim=(0.0, 1.0), title="discrepancy", legend = :none); - plot(p00, p01, p02, p03, layout=@layout([A B C D]), - framestyle=nothing, showaxis=false, - xticks=false, yticks=false, - size=(1200, 500), - plot_title="True vals: $(true_vals) - fitted vals: $(output)", - plot_titlevspan=0.2 - ) - end + plot(p00, p01, p02, p03, layout=@layout([A B C D]), + framestyle=nothing, showaxis=false, + xticks=false, yticks=false, + size=(1200, 500), + plot_title="True vals: $(true_vals) + fitted vals: $(output)", + plot_titlevspan=0.2 + ) + end end diff --git a/src/DataToFunctions.jl b/src/DataToFunctions.jl index 701832c..2634eb5 100644 --- a/src/DataToFunctions.jl +++ b/src/DataToFunctions.jl @@ -64,11 +64,11 @@ end return @view svec[1:S-1] end -@inline function red_dim_apply(fct::AbstractArray{R}, svec::SVector{S,T})::R where {S,T, R} +@inline function red_dim_apply(fct, svec::SVector{S,T})::R where {S,T, R} return fct((@view svec[1:S-1])...) end -@inline function idx_apply(fct::AbstractArray{R}, svec::SVector{S,T})::R where {S,T, R} +@inline function idx_apply(fct, svec::SVector{S,T}) where {S,T} return fct(svec...) end From 5c533d2a9abf8a910d1eeadcfbcd63e2f6ad41b7 Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Fri, 5 Apr 2024 12:07:18 +0200 Subject: [PATCH 18/44] adding functional input as `get_function_general`, cleaning `export`s --- src/DataToFunctions.jl | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/DataToFunctions.jl b/src/DataToFunctions.jl index 2634eb5..293883c 100644 --- a/src/DataToFunctions.jl +++ b/src/DataToFunctions.jl @@ -3,8 +3,7 @@ using Interpolations using FourierTools using StaticArrays -export get_function, get_function_affine, add_dim, red_dim_apply, red_dim, mat_mul, func_transform -export extrapolate, interpolate +export get_function, get_function_affine, get_function_general """ get_function(data::AbstractArray; super_sampling=2, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) @@ -64,7 +63,7 @@ end return @view svec[1:S-1] end -@inline function red_dim_apply(fct, svec::SVector{S,T})::R where {S,T, R} +@inline function red_dim_apply(fct, svec::SVector{S,T}) where {S,T} return fct((@view svec[1:S-1])...) end @@ -100,7 +99,7 @@ itp: The interpolation object to use ``` """ function apply_transform_tuple!(coord_transf_func::Function, data, itp, out) - out .= red_dim_apply.(Ref(itp), func_transform.(CartesianIndices(data), Ref(coord_transf_func))); + out .= idx_apply.(Ref(itp), func_transform.(CartesianIndices(data), Ref(coord_transf_func))); end """ @@ -214,5 +213,19 @@ function get_function_affine(data::AbstractArray{T}; super_sampling=2, extrapola end +function get_function_general(data::AbstractArray{T}; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where T + #new_size = super_sampling.*size(data) + #upsampled = fftshift(resample(ifftshift(data), new_size)) + + # building the extraplation + interpolation object + itp = extrapolate(interpolate(data, interp_type), extrapolation_bc); + + function interpolated(transf_fcn::Function, out = similar(data)) + apply_transform_tuple!(transf_fcn, data, itp, out); + return out; + end + +end + end # module DataToFunctions \ No newline at end of file From e94bbcd82ccd14cc9229cb461c79fde08552cd76 Mon Sep 17 00:00:00 2001 From: RainerHeintzmann Date: Fri, 5 Apr 2024 19:04:22 +0200 Subject: [PATCH 19/44] working version of non-linear deformations --- examples/taylor_test.jl | 59 +++------- src/DataToFunctions.jl | 217 +--------------------------------- src/polynomials.jl | 85 ++++++++++++++ src/transformators.jl | 251 ++++++++++++++++++++++++++++++++++++++++ src/utils.jl | 18 +++ 5 files changed, 375 insertions(+), 255 deletions(-) create mode 100644 src/polynomials.jl create mode 100644 src/transformators.jl create mode 100644 src/utils.jl diff --git a/examples/taylor_test.jl b/examples/taylor_test.jl index d506fe1..ad771f1 100644 --- a/examples/taylor_test.jl +++ b/examples/taylor_test.jl @@ -1,49 +1,26 @@ -function get_polynomial(::Val{numvars}, ::Val{0}) where {numvars} - # @info "Creating polynomials of order 0" - return (t, c) -> begin - # println("c: $(c) $(length(c))"); - c[1] - end -end +using DataToFunctions +using TestImages -function get_polynomial(::Val{numvars}, ::Val{N}) where {numvars, N} - # @info "Creating polynomials with $(numvars) variables of order , $(N). Required constants: $((numvars+1)^N)" - p1 = get_polynomial(Val(numvars), Val(N-1)); # is reused multiple times - p2(t, c) = begin - s = p1(t, c[1+length(c)÷(numvars+1):(2)*length(c)÷(numvars+1)]) * t[1] # int devision needed for type stability! - # s = 0 - for n in 2:numvars - s += p1(t, c[1+n*length(c)÷(numvars+1):(n+1)*length(c)÷(numvars+1)]) * t[n] # int devision needed for type stability! - end - return s - end - function p3(t, c) - # println("N: $(N), c: $(c) $(length(c))"); - p1(t, c[1:length(c)÷(numvars+1)]) + p2(t,c) # int devision needed for type stability! - end +function main() - return p3 -end + obj = Float32.(TestImages.shepp_logan(320)); -function main() - (2+1)^3 - p = get_polynomial(Val(2), Val(3)) # 27 indices required - @time p.(Tuple.(CartesianIndices((200,200))),Ref((1.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))); - # does allocate + f = get_function_poly(obj,1) + c0 = (-10.5, 1.1, 0.1, -20.2, 0.1, 1.1) + @time warped = f(c0); # 1.7 ms - (2+1)^2 - p = get_polynomial(Val(2), Val(2)) # 3 indices required - @time p.(Tuple.(CartesianIndices((200,200))),Ref((1.1,2.1,3.1,4,5,6,7,8,9))); - # essentially allocation-free + fa = get_function_affine(obj); #.+ dtype.(rand(size(sample_data)...))./5.0; + ca = [1.5, 1.1, 0.6, 1.2, 0.1, 1.1, 2.0] + @time warpeda = fa(ca); # 1.7 ms - (2+1)^1 - p = get_polynomial(Val(1), Val(1)) # 3 indices required - @time p.(Tuple.(CartesianIndices((200,200))),Ref((1.1,2.1,3.1))); - # essentially allocation-free - p((100,),((1.1,2.2, 3.3))) + # non-linear deformation warp + f2 = get_function_poly(obj, 2); #.+ dtype.(rand(size(sample_data)...))./5.0; + c02 = (-150.5, 1.1, 0.1, 0.001, 0.001 ,0.001, 0.001, 0.001, 0.001, + -110.2, 0.1, 1.5, 0.001, 0.0015,-0.001, 0.0014,0.001,-0.001) + @time warped2 = f2(c02); # 3.3 ms + @time warped2 .= f2(c02); + @time f2(c02, warped2); # 3.0 ms + # @vt obj warpeda warped warped2 - p = get_polynomial(Val(1), Val(0)) # 27 indices required - @time p.(Tuple.(CartesianIndices((100,100))),Ref((1.1))); - # essentially allocation-free end diff --git a/src/DataToFunctions.jl b/src/DataToFunctions.jl index 2634eb5..c4ac502 100644 --- a/src/DataToFunctions.jl +++ b/src/DataToFunctions.jl @@ -1,218 +1,7 @@ module DataToFunctions -using Interpolations -using FourierTools -using StaticArrays - -export get_function, get_function_affine, add_dim, red_dim_apply, red_dim, mat_mul, func_transform -export extrapolate, interpolate - -""" - get_function(data::AbstractArray; super_sampling=2, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) - -returns a function `dat(shift, zoom)` which generates a shifted and scaled version of the original data. -This is useful for fitting with a function which is itself defined by measured data. - -# Arguments -`data`: The data to represent by the function `dat` -`super_sampling`: The factor by which the data is internally represented as a supersampled version (Fourier-based upsampling, see `FourierTools.resample`) -`extrapolation_bc`: The extrapolation boundary condition to select for values outside the range. - By default the value 0.0 is used. Other options are `Flat()`, or `Line()`, See the package `Interpolation` for details. -`interp_type`: The type of interpolation to use. See the package `Interpolation` for details. - -# Example -```jldoctest -``` -""" -function get_function(data::AbstractArray; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) - new_size = super_sampling.*size(data) - upsampled = fftshift(resample(ifftshift(data), new_size)) - # @show upsampled - # return upsampled - # itp = LinearInterpolation(axes(upsampled), upsampled, extrapolation_bc=extrapolation_bc); - interpolation = Interpolations.interpolate(upsampled, interp_type) - interpolation = extrapolate(interpolation, extrapolation_bc) - # center of the original data (too keep the axis and number of datapointsi dentical to the original) - center_orig = (size(data) .÷2 .+1) - # create zero-centered original ranges (== axes) - zero_axes = Tuple(ax .- c for (ax, c) in zip(axes(data), center_orig)) - # center of the upsampled data. This is where to access the upsampled data - function zoomed(shift, zoom) - zoom = zoom .* super_sampling - # careful: The center of the original data is not at the expected position! But rather at: - center_upsamp = new_size .÷2 .+1 # ((center_orig .-1) .*super_sampling .+1) # new_size .÷2 .+1 - scaled_axes = ((ax.-myc) .* z .+ cen for (ax, myc, cen, z) in zip(zero_axes, shift, center_upsamp, zoom)) - # @show Tuple(scaled_axes) - return interpolation[scaled_axes...] - # return extrapolate(scale(interpolation, scaled_axes...), extrapolation_bc) - end - - return zoomed - - zoomed(p) = zoomed([p[1], p[2]], [p[3], p[4]]) - # return (pos) -> interp_linear((center .+ pos)...) - # fitp(t) = interp_linear(t...) - # @time res1 = fitp.(tcoords); # 1 sec - # function my_zoom - -end - -function add_dim(cind) - return SVector.((Tuple(cind))..., 1) -end - -@inline function red_dim(svec::SVector{S,T})::SVector{S-1,T} where {S,T} - return @view svec[1:S-1] -end - -@inline function red_dim_apply(fct, svec::SVector{S,T})::R where {S,T, R} - return fct((@view svec[1:S-1])...) -end - -@inline function idx_apply(fct, svec::SVector{S,T}) where {S,T} - return fct(svec...) -end - -# multiplying the transformation matrix -@inline function mat_mul(t::SVector{N, Int64}, matrix_c::SMatrix{N,N,T})::SVector{N,T} where {N,T} - return matrix_c * t -end - - -# Applying the coordinate transformation function -@inline function func_transform(t, coord_transform_func::Function)::SVector - return coord_transform_func(Tuple(t)) -end - - -# How to run this function: -# f_general((t) -> (t[1]*1.01, t[2]*1.01), out2) -""" - apply_transform_tuple!(coord_transf_func::Function, data, itp, out) - -applies a tuple-based coordinate transformation function to the indices of an array and returns the transformed array - -coord_transf_func: A function that takes a tuple of coordinates and returns a new tuple of coordinates -data: The data to transform -itp: The interpolation object to use - -# Example -```jldoctest -``` -""" -function apply_transform_tuple!(coord_transf_func::Function, data, itp, out) - out .= red_dim_apply.(Ref(itp), func_transform.(CartesianIndices(data), Ref(coord_transf_func))); -end - -""" - apply_transform_svec!(coord_transf_func::Function, data, itp, out) -applies a homogeneous coordinate-based coordinate transformation function to the indices of an array and returns the transformed array - -coord_transf_func: A function that takes a N-+1 dimensional homogeneous SVector returns a new N+1 dimensional SVector -data: The data to transform -itp: The interpolation object to use - -""" -function apply_transform_svec!(coord_transf_func::Function, data, itp, out) - out .= idx_apply.(Ref(itp), coord_transf_func.(CartesianIndices(data))); -end - -""" - apply_transform_homogen!(coord_transf_func::Function, data, itp, out) -applies a homogeneous coordinate-based coordinate transformation function to the indices of an array and returns the transformed array - -coord_transf_func: A function that takes a N-+1 dimensional homogeneous SVector returns a new N+1 dimensional SVector -data: The data to transform -itp: The interpolation object to use - -""" -function apply_transform_homogen!(coord_transf_func::Function, data, itp, out) - h_coord_transf_func = (c) -> red_dim(coord_transf_func(add_dim(c))) - apply_transform_svec!(h_coord_transf_func, data, itp, out); - # out .= itp.(red_dim.(coord_transf_func.(add_dim.(CartesianIndices(data))))); - # out .= red_dim_apply.(Ref(itp), coord_transf_func.(add_dim.(CartesianIndices(data)))); -end - -function apply_transform_affine!(mymat, data, itp, out) - # red_dim_apply.(Ref(itp), func_transform.(CartesianIndices(data), Ref(coord_transf_func))); - # return red_dim_apply.(Ref(itp), mat_mul.(add_dim.(CartesianIndices(data)), Ref(mymat))); - - homogenous_transform = (c) -> mat_mul(c, mymat) - apply_transform_homogen!(homogenous_transform, data, itp, out); - #return out -end - -function get_function_tuple(data::AbstractArray{T}, fct_tup::Function; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where T - # building the extraplation + interpolation object - itp = extrapolate(interpolate(data, interp_type), extrapolation_bc); - function interpolated(params, out = similar(data)) - fct_tup_noparams(c) = fct_tup(c, params) - return apply_transform_tuple!(fct_tup_noparams, data, itp, out); - end - return interpolated -end - -function get_function_svec(data::AbstractArray{T}, fct_hom::Function; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where T - # building the extraplation + interpolation object - itp = extrapolate(interpolate(data, interp_type), extrapolation_bc); - function interpolated(params::SVector, out = similar(data)) - fct_hom_noparams(c) = fct_hom(c, params) - apply_transform_homogen!(fct_hom_noparams, data, itp, out); - return out; - end - return interpolated -end - -""" - get_function_affine(data::AbstractArray; super_sampling=1, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) - -returns a function `interpolated(p, [out])` which generates a transformed version of the original data parameterized by transform parameters. -This is useful for fitting with a function which is itself defined by measured data. -The returned function supports two ways to be used, with an affine transform matrix `p` as in input or with a vector `p` of parameters. -The optional argument `out` can be used to store the result of the transformation. - - -# Arguments -`data`: The data to represent by the function `dat` -`extrapolation_bc`: The extrapolation boundary condition to select for values outside the range. - By default the value 0.0 is used. Other options are `Flat()`, or `Line()`, See the package `Interpolation` for details. -`interp_type`: The type of interpolation to use. See the package `Interpolation` for details. - -""" -function get_function_affine(data::AbstractArray{T}; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where T - #new_size = super_sampling.*size(data) - #upsampled = fftshift(resample(ifftshift(data), new_size)) - - # building the extraplation + interpolation object - itp = extrapolate(interpolate(data, interp_type), extrapolation_bc); - - function interpolated(matrix_c::SMatrix{T}, out = similar(data)) where T - apply_transform_affine!(matrix_c, data, itp, out); - return out; - end - - function interpolated(p::AbstractVector{T}, out = similar(data)) where T - x_cen, y_cen = (size(data) .÷ 2.0 .+1) - # x_cen_up, y_cen_up = (size(upsampled) .÷ 2.0 .+ 1.0) - - # creating the matrices of rotation, shear, scale, and shift - rot_mat = @SMatrix [cos(p[7]) -1.0*sin(p[7]) 0.0; sin(p[7]) cos(p[7]) 0.0; 0.0 0.0 1.0]; - shear_mat = @SMatrix [1.0 p[5] 0.0; p[6] 1.0 0.0; 0.0 0.0 1.0]; - scale_mat = @SMatrix [1/p[3] 0.0 0.0; 0.0 1/p[4] 0.0; 0.0 0.0 1.0]; - shift_mat = @SMatrix [1.0 0.0 -1*p[1]; 0.0 1.0 -1*p[2]; 0.0 0.0 1.0]; - t_to_origin = @SMatrix [1.0 0.0 1*x_cen; 0.0 1.0 y_cen; 0.0 0.0 1.0]; - t_to_center = @SMatrix [1.0 0.0 -1.0*x_cen; 0.0 1.0 -1.0*y_cen; 0.0 0.0 1.0]; - # t_orig_upsampled = @SMatrix [1.0 0.0 -1.0*x_cen_up; 0.0 1.0 -1.0*y_cen_up; 0.0 0.0 1.0] - - # building the overall transformation matrix - matrix_c = t_to_origin * scale_mat * rot_mat *shift_mat * t_to_center - - apply_transform_affine!(matrix_c, data, itp, out); # do not call interolated here for type stability reasons - return out; - end - - return interpolated -end - +include("utils.jl") +include("polynomials.jl") +include("transformators.jl") end # module DataToFunctions \ No newline at end of file diff --git a/src/polynomials.jl b/src/polynomials.jl new file mode 100644 index 0000000..82a02c9 --- /dev/null +++ b/src/polynomials.jl @@ -0,0 +1,85 @@ +export get_polynomial, get_multi_poly, get_num_poly_vars, get_num_multipoly_vars + +function get_polynomial(::Val{numvars}, ::Val{0}) where {numvars} + # @info "Creating polynomials of order 0" + return (t, c) -> begin + # println("c: $(c) $(length(c))"); + return c[1] + end #, (t,c) -> ntuple(n->c[1], Val(numvars)) +end + +function get_polynomial(::Val{numvars}, ::Val{N}) where {numvars, N} + # @info "Creating polynomials with $(numvars) variables of order , $(N). Required constants: $((numvars+1)^N)" + p1 = get_polynomial(Val(numvars), Val(N-1)); # is reused multiple times + p2(t, c) = begin + s = p1(t, c[1+length(c)÷(numvars+1):(2)*length(c)÷(numvars+1)]) * t[1] # int devision needed for type stability! + # s = 0 + for n in 2:numvars + s += p1(t, c[1+n*length(c)÷(numvars+1):(n+1)*length(c)÷(numvars+1)]) * t[n] # int devision needed for type stability! + end + return s + end + function p3(t, c)::Float32 + # println("N: $(N), c: $(c) $(length(c))"); + p1(t, c[1:length(c)÷(numvars+1)]) + p2(t,c) # int devision needed for type stability! + end + + # function p3m(t, c)::NTuple{numvars, Float32} + # # println("N: $(N), c: $(c) $(length(c))"); + # ntuple(n -> p1(t, c[1+(n-1)*((numvars+1)^N):length(c)÷(numvars+1) + (n-1)*((numvars+1)^N)]) + p2(t,c), Val(numvars)) # int devision needed for type stability! + # end + + return p3 # , p3m +end + +function get_multi_poly(::Val{numvars}, ::Val{N}) where {numvars, N} + # cs_per_comp = ((numvars+1)^N) + @info "Creating polynomials with $(numvars) variables of order , $(N). Required constants: $(numvars*((numvars+1)^N))" + p = get_polynomial(Val(numvars), Val(N)) + # return p + function mpol(t,c)::NTuple{numvars, Float32} + return ntuple(n->p(t, split_tuple(c,Val(numvars))[n]), Val(numvars)) + + # println("N: $(N), c: $(c) $(length(c))"); + # return Tuple(p(t, c[1+(n-1)*((numvars+1)^N):n*((numvars+1)^N)]) for n=1:numvars) + # return ntuple(n->p(t, c[1+(n-1)*cs_per_comp:n*cs_per_comp]), Val(numvars)) + # return ntuple(n->p(t, c[1+(n-1)*((numvars+1)^N):n*((numvars+1)^N)]), Val(numvars)) + # return (p(t, c[1+(1-1)*((numvars+1)^N):1*((numvars+1)^N)]), p(t, c[1+(2-1)*((numvars+1)^N):2*((numvars+1)^N)])) + end + return mpol # (t, c)->ntuple(n->p(t, c[1+(n-1)*((numvars+1)^N):n*((numvars+1)^N)]), Val(numvars)) + # return (t, c) -> Tuple(p(t,c[1+(n-1)*((numvars+1)^N):n*((numvars+1)^N)]) for n=1:numvars) +end + +function get_num_poly_vars(::Val{numvars}, ::Val{N}) where {numvars, N} + return (numvars+1)^N +end + +function get_num_multipoly_vars(::Val{numvars}, ::Val{N}) where {numvars, N} + return numvars*get_num_poly_vars(Val(numvars), Val(N)) +end + +function test_poly_allocations() + get_num_poly_vars(Val(2), Val(3)) # (2+1)^3 + p = get_polynomial(Val(2), Val(3)) # 27 indices required + @time p.(Tuple.(CartesianIndices((200,200))),Ref((1.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))); + # does allocate 9 Mb ! + + p = get_polynomial(Val(3), Val(2)) # 9 indices required + get_num_poly_vars(Val(3), Val(2)) + @time p.(Tuple.(CartesianIndices((100,100,10))),Ref((1.1,2.1,3.1,4,5,6,7,8,9,10,11,12,13,14,15,16))); + # does allocate 160 Mb ! + + p = get_polynomial(Val(2), Val(2)) # 9 indices required + @time p.(Tuple.(CartesianIndices((200,200))),Ref((1.1,2.1,3.1,4,5,6,7,8,9))); + # essentially allocation-free + + p = get_polynomial(Val(2), Val(1)) # 3 indices required + @time p.(Tuple.(CartesianIndices((200,200))),Ref((1.1,2.1,3.1))); + # essentially allocation-free + # p((100,100),((1.1,2.2, 3.3))) + + p = get_polynomial(Val(1), Val(0)) # 27 indices required + @time p.(Tuple.(CartesianIndices((100,100))),Ref((1.1))); + # essentially allocation-free + +end diff --git a/src/transformators.jl b/src/transformators.jl new file mode 100644 index 0000000..3c7c99f --- /dev/null +++ b/src/transformators.jl @@ -0,0 +1,251 @@ +using Interpolations +using FourierTools +using StaticArrays + +export get_function, get_function_tuple, get_function_svec, get_function_affine, get_function_poly +export add_dim, red_dim_apply, red_dim, mat_mul, func_transform +export extrapolate, interpolate + +""" + get_function(data::AbstractArray; super_sampling=2, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) + +returns a function `dat(shift, zoom)` which generates a shifted and scaled version of the original data. +This is useful for fitting with a function which is itself defined by measured data. + +# Arguments +`data`: The data to represent by the function `dat` +`super_sampling`: The factor by which the data is internally represented as a supersampled version (Fourier-based upsampling, see `FourierTools.resample`) +`extrapolation_bc`: The extrapolation boundary condition to select for values outside the range. + By default the value 0.0 is used. Other options are `Flat()`, or `Line()`, See the package `Interpolation` for details. +`interp_type`: The type of interpolation to use. See the package `Interpolation` for details. + +# Example +```jldoctest +``` +""" +function get_function(data::AbstractArray; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) + new_size = super_sampling.*size(data) + upsampled = fftshift(resample(ifftshift(data), new_size)) + # @show upsampled + # return upsampled + # itp = LinearInterpolation(axes(upsampled), upsampled, extrapolation_bc=extrapolation_bc); + interpolation = Interpolations.interpolate(upsampled, interp_type) + interpolation = extrapolate(interpolation, extrapolation_bc) + # center of the original data (too keep the axis and number of datapointsi dentical to the original) + center_orig = (size(data) .÷2 .+1) + # create zero-centered original ranges (== axes) + zero_axes = Tuple(ax .- c for (ax, c) in zip(axes(data), center_orig)) + # center of the upsampled data. This is where to access the upsampled data + function zoomed(shift, zoom) + zoom = zoom .* super_sampling + # careful: The center of the original data is not at the expected position! But rather at: + center_upsamp = new_size .÷2 .+1 # ((center_orig .-1) .*super_sampling .+1) # new_size .÷2 .+1 + scaled_axes = ((ax.-myc) .* z .+ cen for (ax, myc, cen, z) in zip(zero_axes, shift, center_upsamp, zoom)) + # @show Tuple(scaled_axes) + return interpolation[scaled_axes...] + # return extrapolate(scale(interpolation, scaled_axes...), extrapolation_bc) + end + + return zoomed + + zoomed(p) = zoomed([p[1], p[2]], [p[3], p[4]]) + # return (pos) -> interp_linear((center .+ pos)...) + # fitp(t) = interp_linear(t...) + # @time res1 = fitp.(tcoords); # 1 sec + # function my_zoom + +end + +function add_dim(cind) + return SVector.((Tuple(cind))..., 1) +end + +@inline function red_dim(svec::SVector{S,T})::SVector{S-1,T} where {S,T} + return @view svec[1:S-1] +end + +@inline function red_dim_apply(fct, svec::SVector{S,T}) where {S,T} + return fct((@view svec[1:S-1])...) +end + +@inline function red_dim_apply(fct, tup::NTuple{S,T}) where {S,T} + return fct(tup[1:S-1]...) +end + +@inline function idx_apply(fct, svec::SVector{S,T}) where {S,T} + return fct(svec...) +end + +@inline function idx_apply(fct, tup::NTuple{S,T}) where {S,T} + return fct(tup...) +end + +# multiplying the transformation matrix +@inline function mat_mul(t::SVector{N, Int64}, matrix_c::SMatrix{N,N,T})::SVector{N,T} where {N,T} + return matrix_c * t +end + +# Applying the coordinate transformation function +@inline function func_transform(t, coord_transform_func::Function)::SVector + return coord_transform_func(Tuple(t)) +end + +@inline function func_transform_tup(t, coord_transform_func::Function) + return coord_transform_func(Tuple(t)) +end + + +# How to run this function: +# f_general((t) -> (t[1]*1.01, t[2]*1.01), out2) + +""" + apply_transform!(coord_transf_func::Function, data, itp, out) +applies a homogeneous coordinate-based coordinate transformation function to the indices of an array and returns the transformed array + +coord_transf_func: A function that takes a N-+1 dimensional CartesianIndex and returns a new N+1 dimensional SVector or Tuple +data: The data to transform +itp: The interpolation object to use + +""" +function apply_transform!(coord_transf_func::Function, data, itp, out) + @info "Applying tuple transformation" + out .= idx_apply.(Ref(itp), coord_transf_func.(CartesianIndices(data))); +end + +""" + apply_transform_homogen!(coord_transf_func::Function, data, itp, out) +applies a homogeneous coordinate-based coordinate transformation function to the indices of an array and returns the transformed array + +coord_transf_func: A function that takes a N-+1 dimensional homogeneous SVector returns a new N+1 dimensional SVector +data: The data to transform +itp: The interpolation object to use + +""" +function apply_transform_homogen!(coord_transf_func::Function, data, itp, out) + h_coord_transf_func = (c) -> red_dim(coord_transf_func(add_dim(c))) + apply_transform!(h_coord_transf_func, data, itp, out); + # out .= itp.(red_dim.(coord_transf_func.(add_dim.(CartesianIndices(data))))); + # out .= red_dim_apply.(Ref(itp), coord_transf_func.(add_dim.(CartesianIndices(data)))); +end + +function apply_transform_affine!(mymat::SMatrix{T}, data, itp, out) where {T} # The SMatrix spec is important to avoid allocations + # red_dim_apply.(Ref(itp), func_transform.(CartesianIndices(data), Ref(coord_transf_func))); + # return red_dim_apply.(Ref(itp), mat_mul.(add_dim.(CartesianIndices(data)), Ref(mymat))); + # @info "Applying affine transformation" + + homogenous_transform = (c) -> mat_mul(c, mymat) + apply_transform_homogen!(homogenous_transform, data, itp, out); + #return out +end + +""" + get_function_tuple(data::AbstractArray, fct_tup::Function; super_sampling=2, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) + +returns a function `dat(shift, zoom)` which generates a shifted and scaled version of the original data. +This is useful for fitting with a function which is itself defined by measured data. + +# Arguments +`data`: The data to represent by the function `dat` +`fct_tup`: The function to apply to the data +`super_sampling`: The factor by which the data is internally represented as a supersampled version (Fourier-based upsampling, see `FourierTools.resample`) +`extrapolation_bc`: The extrapolation boundary condition to select for values outside the range. + By default the value 0.0 is used. Other options are `Flat()`, or `Line()`, See the package `Interpolation` for details. +`interp_type`: The type of interpolation to use. See the package `Interpolation` for details. + +# Example +```jldoctest +``` +""" +function get_function_tuple(data::AbstractArray{T}, fct_tup::Function; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where T + # building the extraplation + interpolation object + itp = extrapolate(interpolate(data, interp_type), extrapolation_bc); + function interpolated(params, out = similar(data)) + fct_tup_noparams(ci) = fct_tup(ci, params) + return apply_transform!(fct_tup_noparams, data, itp, out); + end + return interpolated +end + +function get_function_svec(data::AbstractArray{T}, fct_hom::Function; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where T + # building the extraplation + interpolation object + itp = extrapolate(interpolate(data, interp_type), extrapolation_bc); + function interpolated(params::SVector, out = similar(data)) + fct_hom_noparams(c) = fct_hom(c, params) + apply_transform_homogen!(fct_hom_noparams, data, itp, out); + return out; + end + return interpolated +end + +""" + get_function_affine(data::AbstractArray; super_sampling=1, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) + +returns a function `interpolated(p, [out])` which generates a transformed version of the original data parameterized by transform parameters. +This is useful for fitting with a function which is itself defined by measured data. +The returned function supports two ways to be used, with an affine transform matrix `p` as in input or with a vector `p` of parameters. +The optional argument `out` can be used to store the result of the transformation. + + +# Arguments +`data`: The data to represent by the function `dat` +`extrapolation_bc`: The extrapolation boundary condition to select for values outside the range. + By default the value 0.0 is used. Other options are `Flat()`, or `Line()`, See the package `Interpolation` for details. +`interp_type`: The type of interpolation to use. See the package `Interpolation` for details. + +""" +function get_function_affine(data::AbstractArray{T}; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where T + #new_size = super_sampling.*size(data) + #upsampled = fftshift(resample(ifftshift(data), new_size)) + + # building the extraplation + interpolation object + itp = extrapolate(interpolate(data, interp_type), extrapolation_bc); + + function interpolated(matrix_c::SMatrix{T}, out = similar(data)) where T + apply_transform_affine!(matrix_c, data, itp, out); + return out; + end + + function interpolated(p::AbstractVector{T}, out = similar(data)) where T + x_cen, y_cen = (size(data) .÷ 2.0 .+1) + # x_cen_up, y_cen_up = (size(upsampled) .÷ 2.0 .+ 1.0) + + # creating the matrices of rotation, shear, scale, and shift + rot_mat = @SMatrix [cos(p[7]) -1.0*sin(p[7]) 0.0; sin(p[7]) cos(p[7]) 0.0; 0.0 0.0 1.0]; + shear_mat = @SMatrix [1.0 p[5] 0.0; p[6] 1.0 0.0; 0.0 0.0 1.0]; + scale_mat = @SMatrix [1/p[3] 0.0 0.0; 0.0 1/p[4] 0.0; 0.0 0.0 1.0]; + shift_mat = @SMatrix [1.0 0.0 -1*p[1]; 0.0 1.0 -1*p[2]; 0.0 0.0 1.0]; + t_to_origin = @SMatrix [1.0 0.0 1*x_cen; 0.0 1.0 y_cen; 0.0 0.0 1.0]; + t_to_center = @SMatrix [1.0 0.0 -1.0*x_cen; 0.0 1.0 -1.0*y_cen; 0.0 0.0 1.0]; + # t_orig_upsampled = @SMatrix [1.0 0.0 -1.0*x_cen_up; 0.0 1.0 -1.0*y_cen_up; 0.0 0.0 1.0] + + # building the overall transformation matrix + matrix_c = t_to_origin * scale_mat * rot_mat *shift_mat * t_to_center + + apply_transform_affine!(matrix_c, data, itp, out); # do not call interolated here for type stability reasons + return out; + end + + return interpolated +end + + +""" + get_function_poly(data::AbstractArray, order; super_sampling=1, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) + +returns a function `interpolated(p, [out])` which generates a transformed version of the original data parameterized by transform parameters. +This is useful for fitting with a function which is itself defined by measured data. +The returned function supports two ways to be used, with an affine transform matrix `p` as in input or with a vector `p` of parameters. +The optional argument `out` can be used to store the result of the transformation. + + +# Arguments +`data`: The data to represent by the function `dat` +`extrapolation_bc`: The extrapolation boundary condition to select for values outside the range. + By default the value 0.0 is used. Other options are `Flat()`, or `Line()`, See the package `Interpolation` for details. +`interp_type`: The type of interpolation to use. See the package `Interpolation` for details. + +""" +function get_function_poly(data::AbstractArray{T}, order; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where T + pm = get_multi_poly(Val(ndims(data)), Val(order)) + return get_function_tuple(data, pm; super_sampling= super_sampling, extrapolation_bc=extrapolation_bc, interp_type=interp_type); +end \ No newline at end of file diff --git a/src/utils.jl b/src/utils.jl new file mode 100644 index 0000000..b04e514 --- /dev/null +++ b/src/utils.jl @@ -0,0 +1,18 @@ +export split_tuple + +""" + split_tuple(t::NTuple{S,T},::Val{numvars}) where {S,T,numvars} + +Split a tuple into `numvars` parts packed into a tuple of tuples. The tuple `t` is assumed to have a length that is a multiple of `numvars`. + +Example: +```juliadoc +julia> t = (1,2,3,4,5,6) +julia> split_tuple(t, Val{2}) +((1,2), (3,4), (5,6)) +``` +""" +function split_tuple(t::NTuple{S,T},::Val{numvars}) where {S,T,numvars} + return ntuple(n->t[1+(n-1)*(S÷numvars):n*(S÷numvars)], Val(numvars)) +end + From ef19af570bfea670ed7ee0cb04ec5aa9853867a2 Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Wed, 10 Jul 2024 12:37:56 +0200 Subject: [PATCH 20/44] modded --- .gitignore | 1 + Project.toml | 17 ++ README.md | 6 +- docs/Project.toml | 4 + docs/make.jl | 23 ++ docs/src/api.md | 20 ++ docs/src/index.md | 5 + docs/src/tutorial.jl | 57 +++++ docs/src/tutorial.md | 91 ++++++++ examples/PSF_fitting_general.jl | 391 +++++++++++++++++++++++--------- examples/PSF_fitting_new | 0 examples/PSF_fitting_new.jl | 17 ++ examples/Project.toml | 12 + examples/polynomial_apply.jl | 69 ++++++ src/polynomials.jl | 4 +- src/transformators.jl | 196 +++++++++++----- test/Aqua.jl | 6 + test/runtests.jl | 40 ++-- 18 files changed, 776 insertions(+), 183 deletions(-) create mode 100644 docs/Project.toml create mode 100644 docs/make.jl create mode 100644 docs/src/api.md create mode 100644 docs/src/index.md create mode 100644 docs/src/tutorial.jl create mode 100644 docs/src/tutorial.md create mode 100644 examples/PSF_fitting_new create mode 100644 examples/PSF_fitting_new.jl create mode 100644 examples/polynomial_apply.jl create mode 100644 test/Aqua.jl diff --git a/.gitignore b/.gitignore index e688be6..1cc978c 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ Manifest.toml *.mp4 *.png +examples/figures/* diff --git a/Project.toml b/Project.toml index 8f6eb53..9f7abd6 100644 --- a/Project.toml +++ b/Project.toml @@ -7,3 +7,20 @@ version = "0.1.0" FourierTools = "b18b359b-aebc-45ac-a139-9c0ccbb2871e" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + +[compat] +Aqua = "0.8" +FourierTools = "0.4.4" +Interpolations = "0.15.1" +StaticArrays = "1.9.6" +Test = "1.9.3" +Zygote = "0.6.70" +julia = "1" + +[extras] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" + +[targets] +test = ["Aqua", "Test", "Zygote"] diff --git a/README.md b/README.md index 8f912d3..7f863c3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ # DataToFunctions.jl -Represents (measured) data as a function, which supports scaling and shifting. It is intended to be used as a tool for fitting data, where the fitting function is given by measured data. +Represents (measured) data as a function, which supports affine and generally, matrix transformations for arrays. It is intended to be used as a tool for fitting data, where the fitting function is given by measured data. + +It assumes the data as an Interpolation object (function), then it can transform the data to any arbitrary affine or matrix transformations. + +What is important about this package is that it does not assume any pre-defined function as the data for the inverse modeling procedures. \ No newline at end of file diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..4401354 --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,4 @@ +[deps] +DataToFunctions = "64cfdffa-4d02-49ee-ae8b-a805370874f5" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 0000000..4fd18e0 --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,23 @@ +import Pkg +Pkg.activate(@__DIR__) + +cd(@__DIR__) # go into `docs` folder + +using Documenter, Literate, DataToFunctions + +# convert tutorial/examples to markdown +Literate.markdown("./src/tutorial.jl", "./src") +# Which markdown files to compile to HTML +# (which is also the sidebar and the table +# of contents for your documentation) + +pages = Any[ + "Introduction" => "index.md", + "Tutorial" => "tutorial.md", + "API" => "api.md", + ] + +# compile to HTML: +makedocs(; sitename="DataToFunctions.jl", pages, modules = [DataToFunctions], warnonly = true) + +deploydocs(repo = "github.com/RainerHeintzmann/DataToFunctions.jl.git") \ No newline at end of file diff --git a/docs/src/api.md b/docs/src/api.md new file mode 100644 index 0000000..2502d57 --- /dev/null +++ b/docs/src/api.md @@ -0,0 +1,20 @@ +# API + +```@docs +get_function +add_dim +red_dim +red_dim_apply +idx_appy +mat_mul +func_transform +func_transform_tup +apply_transform +apply_transform_homogen +apply_transform_affine +get_function_tuple +get_function_svec +get_function_affine +get_function_poly +split_tuple +``` \ No newline at end of file diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..fa5f238 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,5 @@ +# DataTofunctions.jl + +```@docs +DataToFunctions +``` \ No newline at end of file diff --git a/docs/src/tutorial.jl b/docs/src/tutorial.jl new file mode 100644 index 0000000..e63f312 --- /dev/null +++ b/docs/src/tutorial.jl @@ -0,0 +1,57 @@ +# Tutorial for the DataToFunctions.jl package + +# Load the packages +using DataToFunctions +using SyntheticObjects +using StaticArrays +using Random +using Plots + +# We set the Random.seed for reproducibility +Random.seed!(14) + +# Define the data +sz = 128 +data = filaments3D((sz, sz, 1), num_filaments=3)[:, :, 1] + +# define the interpolation function using this package +f_affine = get_function_affine(data) + +# define the transformation parameters +params = [1.0, 2.0, 1.2, 0.8, 0.0, 0.0, 0.0] + +# apply the transformation +data_transformed = f_affine(params) +heatmap(data_transformed, aspect_ratio=1 + , title="Transformed data using a transformation parameters vector" + , titlefontsize=10, size=(500, 500) + , xlabel="X", ylabel="Y") + + +## The next example is to use a transformation matrix +matrix_c = [1.0 0.0 2.0; + 0.0 1.0 3.0; + 0.0 0.0 1.0] + +data_transformed_m = f_affine(SMatrix{3, 3}(matrix_c)) +heatmap(data_transformed_m, aspect_ratio=1 + , title="Transformed data using a transformation matrix" + , titlefontsize=10, size=(500, 500) + , xlabel="X", ylabel="Y") + + +# Now we try to do the transformation using a polynomial function +# we first define the interpolation object using the DataToFunctions package with a polynomial of order 1 +f_polynomial = get_function_polynomial(data, 1) + +# define the polynomial coefficients +params = [1.0, 0.0, 1.0, 0.0, 1.0, 0.0] + +# apply the transformation +data_transformed_polynomial = f_polynomial(params) + +heatmap(data_transformed_polynomial, aspect_ratio=1 + , title="Transformed data using a polynomial function of order 1" + , titlefontsize=10, size=(500, 500) + , xlabel="X", ylabel="Y") + \ No newline at end of file diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md new file mode 100644 index 0000000..924e00b --- /dev/null +++ b/docs/src/tutorial.md @@ -0,0 +1,91 @@ +```@meta +EditURL = "tutorial.jl" +``` + +Tutorial for the DataToFunctions.jl package + +Load the packages + +````@example tutorial +using DataToFunctions +using SyntheticObjects +using StaticArrays +using Random +using Plots +```` + +We set the Random.seed for reproducibility + +````@example tutorial +Random.seed!(14) +```` + +Define the data + +````@example tutorial +sz = 128 +data = filaments3D((sz, sz, 1), num_filaments=3)[:, :, 1] +```` + +define the interpolation function using this package + +````@example tutorial +f_affine = get_function_affine(data) +```` + +define the transformation parameters + +````@example tutorial +params = [1.0, 2.0, 1.2, 0.8, 0.0, 0.0, 0.0] +```` + +apply the transformation + +````@example tutorial +data_transformed = f_affine(params) +heatmap(data_transformed, aspect_ratio=1 + , title="Transformed data using a transformation parameters vector" + , titlefontsize=10, size=(500, 500) + , xlabel="X", ylabel="Y") + + +# The next example is to use a transformation matrix +matrix_c = [1.0 0.0 2.0; + 0.0 1.0 3.0; + 0.0 0.0 1.0] + +data_transformed_m = f_affine(SMatrix{3, 3}(matrix_c)) +heatmap(data_transformed_m, aspect_ratio=1 + , title="Transformed data using a transformation matrix" + , titlefontsize=10, size=(500, 500) + , xlabel="X", ylabel="Y") +```` + +Now we try to do the transformation using a polynomial function +we first define the interpolation object using the DataToFunctions package with a polynomial of order 1 + +````@example tutorial +f_polynomial = get_function_polynomial(data, 1) +```` + +define the polynomial coefficients + +````@example tutorial +params = [1.0, 0.0, 1.0, 0.0, 1.0, 0.0] +```` + +apply the transformation + +````@example tutorial +data_transformed_polynomial = f_polynomial(params) + +heatmap(data_transformed_polynomial, aspect_ratio=1 + , title="Transformed data using a polynomial function of order 1" + , titlefontsize=10, size=(500, 500) + , xlabel="X", ylabel="Y") +```` + +--- + +*This page was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).* + diff --git a/examples/PSF_fitting_general.jl b/examples/PSF_fitting_general.jl index ddd43c7..6bc5d8f 100644 --- a/examples/PSF_fitting_general.jl +++ b/examples/PSF_fitting_general.jl @@ -1,14 +1,17 @@ using DataToFunctions using Optim, StaticArrays, LinearAlgebra +using PointSpreadFunctions using Zygote using ForwardDiff, LineSearches, Plots, Printf using View5D using Distributions, Rotations using Plots using TestImages - - +using BenchmarkTools +#using InverseModeling import Random +using Noise, Images, CSV, TiffImages +using ProgressBars """ perform_fit_general(loss_function, fitting_data::AbstractArray) @@ -26,192 +29,362 @@ a vector of 7 parameters: 2 for the shift, 2 for the scaling, 2 for shear, and 1 there is an example of this function in the `examples/star_fitting_genaral.jl` """ -function perform_fit_general(loss_function, fitting_data::AbstractArray) +function perform_fit_general(loss_function, fitting_data::AbstractArray, init_x::AbstractArray{T}) where T # guess the shift parameters by taking the maximum values of the array and # centering the positions ##a, b = Tuple(argmax(fitting_data)) .- size(fitting_data) ./2.0 .- 1.0 #print("INSIDE!!! hehe") # assigning the initial parameter estimates - init_x = vec([0.5, -1.5, 1.0, 1.0, 0.0, 0.0, pi/5]) #ndims(fitting_data)+1, ndims(fitting_data)+1)) + # init_x = vec([0.5, -1.5, 1.0, 1.0, 0.0, 0.0, pi/5]) #ndims(fitting_data)+1, ndims(fitting_data)+1)) # reshape(Matrix(1.0*I, ndims(fitting_data)+1, ndims(fitting_data)+1), 1, 9)) #[a, b, 1.0, 1.0, 0.001, 0.001, 0.001] # setting the lower and upper boundary of the parameter values based on their limits - lower = [-1*size(fitting_data)[1], -1*size(fitting_data)[2], 0.0, 0.0, 0.0, 0.0, 0.0] - upper = [size(fitting_data)[1], size(fitting_data)[2], size(fitting_data)[1], size(fitting_data)[2], 1.0, 1.0, pi] + lower = T[-1*size(fitting_data)[1], -1*size(fitting_data)[2], 0.0, 0.0, -0.01, -0.01, 0.0] + upper = T[size(fitting_data)[1], size(fitting_data)[2], size(fitting_data)[1], size(fitting_data)[2], 0.01, 0.01, pi/2.0] # initializing the LBFGS optimizer - inner_optimizer = LBFGS(; m=1, linesearch=LineSearches.BackTracking(order=2)) + inner_optimizer = BFGS()#; m=3, linesearch=LineSearches.BackTracking(order=3)) # Computer, Optimize! :D res = optimize( loss_function, - #BFGS(), - lower, upper, init_x, + #LBFGS(), + lower, upper, + init_x, Fminbox(inner_optimizer), Optim.Options(store_trace = true, extended_trace = true, iterations=5000), - #autodiff = :forward + autodiff = :forward ) # return the estimated parameters return Optim.minimizer(res), res end +function perform_fit(loss_function, init_x::AbstractArray{T}) where T + + # Computer, Optimize! :D + res = optimize( + loss_function, + init_x, + #Newton(), + #BFGS(),#; linesearch=LineSearches.BackTracking(order=3)), + LBFGS(),#; linesearch=LineSearches.BackTracking(order=3)), + #lower, upper, + #init_x, + #Fminbox(inner_optimizer), + Optim.Options(store_trace = true, extended_trace = true, iterations=5000), + autodiff = :forward + ) + + # return the estimated parameters + return Optim.minimizer(res), res +end """ - perform_fit(loss_function, fitting_data::AbstractArray) + apply_transform(;matrix=true, sz=64, dtype=Float32, noise_level=1/20.0) -Performs a fit to the fitting data using a loss function defined by the user +This function is designed for applying a transformation to a sample data using either matrix transformations or +parametric transformations based on the provided arguments. # Arguments -`loss_function`: User-defined loss function which is minimized -`fitting_data`: The data which is being fitted +`matrix`: if true, the fitting is done using a matrix transformation, otherwise, the fitting is done using a parametric transformation +`sz`: the size of the sample data +`dtype`: the data type of the sample data +`noise_level`: the noise level to add to the sample data # Returns -a vector of 4 parameters: first 2 for the shift and other 2 for the scaling factors +a tuple of two arrays: the first array is the fitting data, and the second array is the estimated fitting data # Example -there is an example of this function in the `examples/star_fitting.jl` +apply_transform(matrix=true, sz=64, dtype=Float32, noise_level=1/20.0) """ -function perform_fit(loss_function, fitting_data::AbstractArray) - # guess the shift parameters by taking the maximum values of the array and - # centering the positions - a, b = Tuple(argmax(fitting_data)) .- size(fitting_data) ./2.0 .- 1.0 - - # assigning the initial parameter estimates - init_x = [a, b, 1.0, 1.0] +function apply_transform(;matrix=false, sz=64, dtype=Float32, n_photons=1000, pure_rand=false, from_params=true, plotting=false) + Random.seed!(14) - # setting the lower and upper boundary of the parameter values based on the limits of the shift and scaling - lower = [-1*size(fitting_data)[1], -1*size(fitting_data)[2], 0.0, 0.0] - upper = [size(fitting_data)[1], size(fitting_data)[2], size(fitting_data)[1], size(fitting_data)[2]] + # defining the mean and the varixance of the test normal (Gaussian) distribution + μ = [0, 0] + Σ = [sz/10 0.0; + 0.0 sz/10] - # initializing the LBFGS optimizer - inner_optimizer = LBFGS(; m=10, linesearch=LineSearches.BackTracking(order=2)) - - # Computer, Optimize! :D - res = optimize( - loss_function, - lower, upper, - init_x, - Fminbox(inner_optimizer), - Optim.Options(store_trace = true, extended_trace = true, iterations=500), - autodiff = :forward - ) - - # return the estimated parameters - return Optim.minimizer(res) -end + # initializing the multivariate normal distribution + p = MvNormal(μ, Σ) + + # to define the sample array based on a 2D normal distribution + X = -1*sz/2.0:1*sz/2.0 + Y = -1*sz/2.0:1*sz/2.0 + + z = [pdf(p, [x,y]) for y in Y, x in X] + + + # creating a PSF for the widefield microscope + sz_psf = (sz, sz, 100) + sampling = (0.040, 0.040, 0.050) + # simulate a confocal PSF + aberrations = Aberrations([Zernike_HorizontalComa],[0.8]); + pp = PSFParams(0.5,1.4,1.52, method=MethodPropagateIterative, aberrations=aberrations); + + #pp_ex = PSFParams(pp_em; λ=0.488);#, method=MethodPropagateIterative, aplanatic=aplanatic_illumination, aberrations=aberrations); + p_psf_3d = psf(sz_psf, pp, sampling=sampling); + p_psf = p_psf_3d[:, :, 50] + #sample_data = p_psf ./ maximum(p_psf) + + # normalizing the sample data + sample_data = p_psf ./ maximum(p_psf) + # sample_data = dtype.(z[1:sz, 1:sz]./maximum(z)) + # sample_data = dtype.(TestImages.shepp_logan(sz)) + # sample_data = rand(dtype, (sz, sz)) + # sample_data .+= rand(dtype, (size(sample_data)...)).*noise_level; + p_img = n_photons .* (sample_data);# ./ maximum(sample_data)) + n_img = dtype.(poisson(Float64.(p_img))) + x_cen, y_cen = (size(n_img) ./ 2.0) + t_to_origin = dtype[1.0 0.0 1*x_cen; 0.0 1.0 y_cen; 0.0 0.0 1.0]; + t_to_center = dtype[1.0 0.0 -1.0*x_cen; 0.0 1.0 -1.0*y_cen; 0.0 0.0 1.0]; -function main() - Random.seed!(1234) - # Base.show(io::IO, f::Float64) = @printf(io, "%.3f", f) + true_vals = dtype[rand(-4.0:0.001:4.0), rand(-4.0:0.001:4.0), 1.0, 1.0, 0.0, 0.0, 0.0];#rand(0.9:0.001:1.1),rand(0.9:0.001:1.1), 0.0, 0.0, 0.0];#rand(0.001:0.001:pi/2.001)] + + if !pure_rand + + shear_mat = dtype[1.0 true_vals[5] 0.0; true_vals[6] 1.0 0.0; 0.0 0.0 1.0]; + scale_mat = dtype[1.0/true_vals[3] 0.0 0.0; 0.0 1/true_vals[4] 0.0; 0.0 0.0 1.0]; + + shift_mat = dtype[1.0 0.0 true_vals[1]; 0.0 1.0 true_vals[2]; 0.0 0.0 1.0]; + # converting the data to function (DataToFunctions.get_function) - # size of the test array to fit - size_arr = 22 - noise_level = 10.0; + rot_mat = dtype[cos(true_vals[7]) -1.0*sin(true_vals[7]) 0.0; sin(true_vals[7]) cos(true_vals[7]) 0.0; 0.0 0.0 1.0]; + + matrix_c = (t_to_origin * scale_mat * shear_mat * rot_mat * shift_mat * t_to_center) + else + matrix_c = dtype.(t_to_origin * rand(0.1:0.001:1.0, (3, 3)) * t_to_center) + end + + f_affine_sim_img = get_function_affine(sample_data);#; super_sampling=1);#, extrapolation_bc=0.0); + if matrix + t_img = f_affine_sim_img(SMatrix{3, 3}(matrix_c))#, fitting_data); #.+ dtype.(rand(size(sample_data)...))./5.0; + else + t_img = f_affine_sim_img(true_vals)#, fitting_data); #.+ dtype.(rand(size(sample_data)...))./5.0; + end + fitting_data = dtype.(poisson(Float64.(t_img ./ maximum(t_img) .* n_photons))) #.+= rand(dtype, size(p_img)...).*noise_level; + + heatmap(fitting_data, aspect_ratio=1, size=(600, 600), title="fitting data", titlefont = font(20), legend=:none, axis=([], false)) + annotate!(vec(map(x -> Tuple((reverse(Tuple(x))..., text(@sprintf("%.0f", fitting_data[x]), :center, font(5), :white))), CartesianIndices(sample_data)))) + savefig("figures/fitting/sample_data_1.png") + + # plot(heatmap(sample_data, aspect_ratio=1), heatmap(fitting_data, aspect_ratio=1)) - # defining the mean and the varixance of the test normal (Gaussian) distribution - μ = [0, 0] - Σ = [size_arr/10 0.0; - 0.0 size_arr/10] + contour(sample_data, length=200, fill=false, title="Sample data", titlefont = font(20), legend=:none, aspect_ratio=1, size=(600, 600)) + savefig("figures/fitting/sample_data_1_contour.png") + return sample_data, fitting_data +end - Σ_d = [2 1.5; - 1.5 2] - # initializing the multivariate normal distribution - p = MvNormal(μ, Σ) +function gauss_psf_comp() + x = -10.0:0.01:10.0 + p = Normal(0.0, 1.0) + y = pdf(p, x) + y_psf(x) = (sin(x) /x)^2 + plot(x, y_psf.(x), label="PSF of a circular aperture", title="Comparison of a Gaussian and a PSF", titlefont=20, size=(800, 400)) + plot!(x, y./maximum(y), label="Gaussian with μ=0.0 & σ=1.0") + savefig("figures/fitting/comp_psf_gaussian_1.png") + + plot(x, map(x -> (gradient(y_psf, x)[1]), x), label="Gradient of the PSF") + plot!(x, map(x -> (gradient(x -> pdf(p, x), x)[1]), x), label="Gradient of the Gaussian", title="Comparison of the Gradients", titlefont=20, size=(800, 400)) + savefig("figures/fitting/comp_psf_gaussian_1_gradients.png") +end +""" + main_fitting(;matrix=true, sz=64, dtype=Float32, iterations=20, noise_level=1/20.0, pure_rand=false, from_params=true) - # this part of the code is to define the sample array based on a 2D normal distribution - X = -1*size_arr/2.0:1*size_arr/2.0 - Y = -1*size_arr/2.0:1*size_arr/2.0 +This function is designed for performing fitting operations on sample data using either matrix transformations or +parametric transformations based on the provided arguments. - z = [pdf(p, [x,y]) for y in Y, x in X] +# Arguments +`matrix`: if true, the fitting is done using a matrix transformation, otherwise, the fitting is done using a parametric transformation +`sz`: the size of the sample data +`dtype`: the data type of the sample data +`iterations`: the number of iterations to perform the fitting +`noise_level`: the noise level to add to the sample data +`pure_rand`: if true, the fitting is done using a random matrix transformation +`from_params`: if true, the fitting is done using the true values as the initial values + +# Returns +a tuple of two arrays: the first array is the fitting data, and the second array is the estimated fitting data - # @vv z +# Example +x, y = main_fitting(matrix=true, sz=32, dtype=Float32, iterations=10, noise_level=1/20.0, pure_rand=false, from_params=true); - dtype = Float64 - # setting a typical values for the shift (1:2) and scale (3:4) - true_vals = dtype.([0.2, -1.2, 1.0, 1.0, 0.0, 0.0, pi/3]) #pi/6] - #true_vals = [2.3, -1.2, 0.9, 2.1, 0.1, 0.05, pi/2, 2.0, 3.0] +""" +function main_fitting(;matrix=true, sz=64, dtype=Float32, iterations=20, use_psf=true, n_photons=1000, pure_rand=false, from_params=true, plotting=false) + Random.seed!(14) - # normalizing the sample data - #sample_data = Float32.(z./maximum(z)) - sample_data = dtype.(TestImages.shepp_logan(32)); - #sample_data = rand(dtype, (size_arr, size_arr)) + # defining the mean and the varixance of the test normal (Gaussian) distribution + μ = [0, 0] + Σ = [sz/30 0.0; + 0.0 sz/30] - #sample_data .+= rand(dtype, (size(sample_data)...))./noise_level; + # initializing the multivariate normal distribution + p = MvNormal(μ, Σ) - x_cen, y_cen = (size(sample_data) .÷ 2.0 .+1) + # to define the sample array based on a 2D normal distribution + X = -1*sz/2.0:1*sz/2.0 + Y = -1*sz/2.0:1*sz/2.0 - shear_mat = [1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0]; - scale_mat = [1/1.2 0.0 0.0; 0.0 1/1.8 0.0; 0.0 0.0 1.0]; + z = [pdf(p, [x,y]) for y in Y, x in X] - t_to_origin = [1.0 0.0 1*x_cen; 0.0 1.0 y_cen; 0.0 0.0 1.0]; - t_to_center = [1.0 0.0 -1.0*x_cen; 0.0 1.0 -1.0*y_cen; 0.0 0.0 1.0]; - # converting the data to function (DataToFunctions.get_function) - f_affine = get_function_affine(sample_data);#; super_sampling=1);#, extrapolation_bc=0.0); + # creating a PSF for the widefield microscope + sz_psf = (sz, sz, 100) + sampling = (0.040, 0.040, 0.050) + # simulate a confocal PSF + aberrations = Aberrations([Zernike_HorizontalComa],[0.8]); + pp = PSFParams(0.5,1.4,1.52, method=MethodPropagateIterative, aberrations=aberrations); - ang = rand((0.0:0.1:pi)) - rot_mat = [cos(ang) -1.0*sin(ang) 0.0; sin(ang) cos(ang) 0.0; 0.0 0.0 1.0]; + #pp_ex = PSFParams(pp_em; λ=0.488);#, method=MethodPropagateIterative, aplanatic=aplanatic_illumination, aberrations=aberrations); + p_psf_3d = psf(sz_psf, pp, sampling=sampling); + p_psf = p_psf_3d[:, :, 50] + #sample_data = p_psf ./ maximum(p_psf) - matrix_c = dtype.(t_to_origin * scale_mat * shear_mat * rot_mat * t_to_center ) + # normalizing the sample data + sample_data = p_psf ./ maximum(p_psf) + sample_data_gaussian = dtype.(z[1:sz, 1:sz]./maximum(z)) + # sample_data = dtype.(TestImages.shepp_logan(sz)) + # sample_data = rand(dtype, (sz, sz)) - # f_d = get_function_loop(sample_data_d; super_sampling=1);#, extrapolation_bc=0.0); + # sample_data .+= rand(dtype, (size(sample_data)...)).*noise_level; + p_img = n_photons .* (sample_data);# ./ maximum(sample_data)) + n_img = dtype.(poisson(Float64.(p_img))) - f2 = similar(sample_data) - # adding some scaled random noise to the fitting data - # fitting_data = f_general(SMatrix{3,3}(matrix_c)); #.+ rand(size(sample_data)...)./100.0; - f_affine(true_vals, f2); #.+ dtype.(rand(size(sample_data)...))./5.0; + y = similar(n_img, (size(n_img)..., iterations)) + x = similar(n_img, (size(n_img)..., iterations)) + pos_res = zeros(Float32, iterations, 2) + pos_arr = zeros(Float32, iterations, 2) + for i in ProgressBar(1:iterations) + # println("iteration: ", i) - plot(heatmap(sample_data, aspect_ratio=1), heatmap(fitting_data, aspect_ratio=1)) + x_cen, y_cen = (size(n_img) ./ 2.0) + t_to_origin = dtype[1.0 0.0 1*x_cen; 0.0 1.0 y_cen; 0.0 0.0 1.0]; + t_to_center = dtype[1.0 0.0 -1.0*x_cen; 0.0 1.0 -1.0*y_cen; 0.0 0.0 1.0]; + true_vals = dtype[rand(-4.0:0.001:4.0), rand(-4.0:0.001:4.0), 1.0, 1.0, 0.0, 0.0, 0.0];#rand(0.9:0.001:1.1),rand(0.9:0.001:1.1), 0.0, 0.0, 0.0];#rand(0.001:0.001:pi/2.001)] - # defining the loss function based on the gaussian noise - loss(p1::AbstractVector) = sum(abs2.(f_affine(p1::AbstractVector) .- fitting_data)) - # loss(x) = loss(x::AbstractVector{T} where T) + if !pure_rand - # loss(p3) = loss([p3[1], p3[2], p3[3], p3[4], p3[5], p3[6], p3[7]]) + shear_mat = dtype[1.0 true_vals[5] 0.0; true_vals[6] 1.0 0.0; 0.0 0.0 1.0]; + scale_mat = dtype[1.0/true_vals[3] 0.0 0.0; 0.0 1/true_vals[4] 0.0; 0.0 0.0 1.0]; + + shift_mat = dtype[1.0 0.0 true_vals[1]; 0.0 1.0 true_vals[2]; 0.0 0.0 1.0]; + # converting the data to function (DataToFunctions.get_function) + + rot_mat = dtype[cos(true_vals[7]) -1.0*sin(true_vals[7]) 0.0; sin(true_vals[7]) cos(true_vals[7]) 0.0; 0.0 0.0 1.0]; + + matrix_c = (t_to_origin * scale_mat * shear_mat * rot_mat * shift_mat * t_to_center) + else + matrix_c = dtype.(t_to_origin * rand(0.1:0.001:1.0, (3, 3)) * t_to_center) + end + f_affine_sim_img = get_function_affine(sample_data);#; super_sampling=1);#, extrapolation_bc=0.0); + if matrix + t_img = f_affine_sim_img(SMatrix{3, 3}(matrix_c))#, fitting_data); #.+ dtype.(rand(size(sample_data)...))./5.0; + else + t_img = f_affine_sim_img(true_vals)#, fitting_data); #.+ dtype.(rand(size(sample_data)...))./5.0; + end + fitting_data = dtype.(poisson(Float64.(t_img ./ maximum(t_img) .* n_photons))) #.+= rand(dtype, size(p_img)...).*noise_level; - # perform the main fit to the fitting data by minimizing the loss function - @time output, res = perform_fit_general(loss, fitting_data) + if use_psf + f_affine = get_function_affine(p_img);#; super_sampling=1);#, extrapolation_bc=0.0); + else + f_affine = get_function_affine(n_photons .* sample_data_gaussian);#; super_sampling=1);#, extrapolation_bc=0.0); + end + #f_affine = get_function_affine(n_img);#; super_sampling=1);#, extrapolation_bc=0.0); + # defining the loss function based on the gaussian noise + loss_m(p1::AbstractMatrix) = sum(abs2.(f_affine(SMatrix{size(p1)...}(p1)) .- fitting_data)) + # loss_m(p1::AbstractVector) = sum(abs2.(f_affine(p1::AbstractVector) .- fitting_data)) + loss_m(p1::AbstractVector) = sum(abs2.(f_affine([p1[1], p1[2], 0.0, 0.0, 0.0, 0.0, 0.0]) .- fitting_data)) + + + if matrix + if from_params + st_vals = dtype[1.0 0.0 -1.0*(argmax(fitting_data)[1]-size(fitting_data)[1]/2.0); 0.0 1.0 -1.0*(argmax(fitting_data)[1]-size(fitting_data)[2]/2.0); 0.0 0.0 1.0] + else + st_vals = dtype[1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0] + end + elseif from_params + st_vals = dtype[argmax(fitting_data)[1]-size(fitting_data)[1]/2.0, argmax(fitting_data)[2]-size(fitting_data)[2]/2.0, 1.0, 1.0, 0.0, 0.0, 0.0];#pi/8.0] + else + st_vals = dtype[0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0];#pi/8.0] + end + + # perform the main fit to the fitting data by minimizing the loss function + stats = @timed output, res = perform_fit(loss_m, st_vals) + + + if !matrix + pos_arr[i, :] = true_vals[1:2] + pos_res[i, :] = output[1:2] + else + pos_arr[i, :] = matrix_c[1:2, 3] + pos_res[i, :] = output[1:2, 3] + end - # plotting the output of the fitting pocedure for further illustration + y[:, :, i] = matrix ? f_affine(SMatrix{size(matrix_c)...}(output)) : f_affine(output) + x[:, :, i] = fitting_data + + # plotting the output of the fitting pocedure for further illustration + if plotting begin - p00 = heatmap(sample_data, aspect_ratio=1.0, clim=(0.0, 1.0), title="Sample data", legend = :none); - p01 = heatmap(fitting_data, aspect_ratio=1.0, clim=(0.0,1.0), title="Fitting data", legend = :none); - p02 = heatmap(f_affine(output), aspect_ratio=1.0, clim=(0.0,1.0), title="estimated fit", legend = :none); - p03 = heatmap(fitting_data .- f_affine(output), aspect_ratio=1.0, clim=(0.0, 1.0), title="discrepancy", legend = :none); - - plot(p00, p01, p02, p03, layout=@layout([A B C D]), - framestyle=nothing, showaxis=false, - xticks=false, yticks=false, - size=(1200, 500), - plot_title="True vals: $(true_vals) - fitted vals: $(output)", - plot_titlevspan=0.2 + p00 = heatmap(n_img, aspect_ratio=1.0, title="Simulated sample PSF", colormap= :gist_gray); + p01 = heatmap(fitting_data, aspect_ratio=1.0, title="Simulated PSF", colormap= :gist_gray); + p02 = heatmap(matrix ? f_affine(SMatrix{size(matrix_c)...}(output)) : f_affine(output), aspect_ratio=1.0, title="Estimated fit", colormap= :gist_gray); + p03 = heatmap(fitting_data .- (matrix ? f_affine(SMatrix{size(matrix_c)...}(output)) : f_affine(output)), aspect_ratio=1.0, title="Residuals", colormap= :bwr, clim=(-maximum((abs.(fitting_data .- (matrix ? f_affine(SMatrix{size(matrix_c)...}(output)) : f_affine(output))))), maximum((abs.(fitting_data .- (matrix ? f_affine(SMatrix{size(matrix_c)...}(output)) : f_affine(output))))))); + + plot(p00, p01, p03, p02, layout=@layout([A B; C D]), + #framestyle=nothing, + #showaxis=false, + #xticks=false, yticks=false, + size=(1200, 1200), + plot_title=" $(if matrix "Matrix" else "Parametric" end) fitting +True vals: $(map(x -> @sprintf("%.3f",x), (matrix ? matrix_c : true_vals))) +fitted vals: $(map(x -> @sprintf("%.3f",x), output)) +n. of photons: $(@sprintf("%.0f", n_photons)) +time elapsed: $(@sprintf("%.1f", 1000.0*stats.time))ms, loss: $(@sprintf("%.2f", res.trace[1].value)) -> $(@sprintf("%.2f", res.trace[end].value))", + plot_titlevspan=0.14 ) + savefig("figures/fitting/$(matrix ? "Matrix" : "Parametric")_fitting_$(i).png") end - + end + end + return x, y, pos_arr, pos_res end +x, y, pos_arr, pos_res = main_fitting(matrix=false, iterations=1000, sz=65, pure_rand=false, n_photons=10, from_params=true, plotting=false); println(mean(pos_res[:, 1] .- pos_arr[:, 1])); +println(std(pos_res[:, 1] .- pos_arr[:, 1])); + +#, title="Positional errors", markersize=2.0, xlabel="X error (pixels)", ylabel="Y error (pixels)", legend=:none, size=(600, 600), xlim=(-1.0, 1.0), ylim=(-1.0, 1.0), alpha=0.4) + +histogram2d(pos_res[:, 1] .- pos_arr[:, 1], pos_res[:, 2] .- pos_arr[:, 2], title="Positional errors histogram for 100 photons", xlabel="X error (pixels)", ylabel="Y error (pixels)", xlim=(-1.0, 1.0), ylim=(-1.0, 1.0), bins=20, aspect_ratio=1) +# +imgg = Gray{N0f16}.(x./maximum(x)); +ff = TiffImages.DenseTaggedImage(imgg); +TiffImages.save("test_4_aberrated.tif", ff); -#matrix_c -# comparing the true values to the best fitting parameters +res_fiji = CSV.File(open(raw"C:\Users\ho82nat\Desktop\thunderstorm_res_100photons.csv")) +res_fiji_x = res_fiji["x [nm]"] ./ 80.0 .- 65.0 ./ 2.0; +res_fiji_y = res_fiji["y [nm]"] ./ 80.0 .- 65.0 ./ 2.0; -#println(matrix_c) * reshape(output, ndims(fitting_data)+1, ndims(fitting_data)+1) +scatter(pos_res[:, 1] .- pos_arr[:, 1], pos_res[:, 2] .- pos_arr[:, 2], markershape= :circle, title="Positional errors", markersize=2.0, xlabel="X error (pixels)", ylabel="Y error (pixels)", legend=:none, size=(600, 600), label="DataToFunctions fitting")#, xlim=(-1.0, 1.0), ylim=(-1.0, 1.0), alpha=0.4) +#scatter!(res_fiji_y .- pos_arr[:, 1], res_fiji_x .- pos_arr[:, 2], markershape= :rect, markersize=2.0, alpha=0.2, label="ThunderSTORM fitting") +println((std(pos_res[:, 1] .- pos_arr[:, 1]), std(pos_res[:, 2] .- pos_arr[:, 2])), (mean(pos_res[:, 1] .- pos_arr[:, 1]), mean(pos_res[:, 2] .- pos_arr[:, 2]))); +#println((std(res_fiji_y .- pos_arr[:, 1]), std(res_fiji_x .- pos_arr[:, 2])), (mean(res_fiji_y .- pos_arr[:, 1]), mean(res_fiji_x .- pos_arr[:, 2]))); #anim = @animate for i1 in 1:length(Optim.x_trace(res)) # diff --git a/examples/PSF_fitting_new b/examples/PSF_fitting_new new file mode 100644 index 0000000..e69de29 diff --git a/examples/PSF_fitting_new.jl b/examples/PSF_fitting_new.jl new file mode 100644 index 0000000..dc9012f --- /dev/null +++ b/examples/PSF_fitting_new.jl @@ -0,0 +1,17 @@ +using PointSpreadFunctions +using Plots + +λ_em = 0.5; NA = 1.4; n = 1.52 +λ_ex = 0.488 # only needed for some PointSpreadFunctions, such as confocal, ISM or TwoPhoton +pp = PSFParams(λ_em, NA, n; pol=pol_x) + +sz = (256, 256, 256) +sampling = (0.020,0.020,0.020) + +aberr_sp = Aberrations([Zernike_VerticalAstigmatism],[1.0]); sz=(256,256,256) +pp_sp = PSFParams(λ_em, NA, n; method=MethodPropagateIterative, aberrations= aberr_sp) +p_sp = psf(sz, pp_sp; sampling=sampling); + +psf_example = sum(p_sp, dims=3)[:,:,1] + +heatmap(p_sp[:, :, 128], aspect_ratio=1) \ No newline at end of file diff --git a/examples/Project.toml b/examples/Project.toml index 9b4fdde..630815d 100644 --- a/examples/Project.toml +++ b/examples/Project.toml @@ -1,21 +1,33 @@ [deps] BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" +CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" CoordinateTransformations = "150eb455-5306-5404-9cee-2592286d6298" DataToFunctions = "64cfdffa-4d02-49ee-ae8b-a805370874f5" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +FourierTools = "b18b359b-aebc-45ac-a139-9c0ccbb2871e" +GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" +Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" +InverseModeling = "ce844058-9528-415d-a63d-06f3dd08b29f" LineSearches = "d3d80556-e9d4-5f37-9878-2ab0fcc64255" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +Noise = "81d43f40-5267-43b7-ae1c-8b967f377efa" OhMyREPL = "5fb14364-9ced-5910-84b2-373655c76a03" Optim = "429524aa-4258-5aef-a3af-852621145aeb" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +PointSpreadFunctions = "e8810a93-244e-46c5-8da3-35c5dd956001" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" +ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568" RandomExtensions = "fb686558-2515-59ef-acaa-46db3789a887" Rotations = "6038ab10-8711-5258-84ad-4b1120ba62dc" +SeparableFunctions = "c8c7ead4-852c-491e-a42d-3d43bc74259e" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +SyntheticObjects = "e7028c27-0967-45e9-8fdb-dbc10ccb2b0a" TaylorSeries = "6aa5eb33-94cf-58f4-a9d0-e4b2c4fc25ea" TestImages = "5e47fb64-e119-507b-a336-dd2b206d9990" +TiffImages = "731e570b-9d59-4bfa-96dc-6df516fadf69" TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" View5D = "90d841e0-6953-4e90-9f3a-43681da8e949" Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f" diff --git a/examples/polynomial_apply.jl b/examples/polynomial_apply.jl new file mode 100644 index 0000000..93ae825 --- /dev/null +++ b/examples/polynomial_apply.jl @@ -0,0 +1,69 @@ +using DataToFunctions +using Optim, StaticArrays, LinearAlgebra +using PointSpreadFunctions +using Zygote +using ForwardDiff, LineSearches, Plots, Printf +using View5D +using Distributions, Rotations +using Plots +using TestImages +using BenchmarkTools +#using InverseModeling +import Random +using Noise + + + +function poly_test(;sz=64, dtype=Float64, n_photons=1000) + sz_psf = (sz, sz, 100) + sampling = (0.040, 0.040, 0.050) + # simulate a confocal PSF + #aberrations = Aberrations([Zernike_HorizontalComa,Zernike_Tip],[0.8,0.7]); + pp = PSFParams(0.5,1.4,1.52, method=MethodPropagateIterative);#, aberrations=aberrations); + + #pp_ex = PSFParams(pp_em; λ=0.488);#, method=MethodPropagateIterative, aplanatic=aplanatic_illumination, aberrations=aberrations); + p_psf_3d = psf(sz_psf, pp, sampling=sampling); + p_psf = p_psf_3d[:, :, 50] + #sample_data = p_psf ./ maximum(p_psf) + + # normalizing the sample data + sample_data = p_psf ./ maximum(p_psf) + + f_affine = get_function_affine(sample_data); + true_vals = dtype[10.0, 2.0, 1.1, 1.7, 0.0, 0.0, 0.0] + # true_vals = dtype[rand(-4.0:0.001:4.0), rand(-4.0:0.001:4.0), rand(0.5:0.001:1.5),rand(0.5:0.001:1.5), 0.0, 0.0, rand(0.001:0.001:pi/2.001)] + dat2 = f_affine(true_vals) + + p_img = dat2 # n_photons .* (dat2 ./ maximum(dat2)) + n_img = p_img#dtype.(poisson(Float64.(p_img))) + + s_data = sample_data#poisson(Float64.(sample_data .* n_photons)) + #sample_data = dtype.(TestImages.shepp_logan(sz)) + f = get_function_poly(Float64.(s_data), 1); + + + + loss_m(p1::AbstractVector) = sum(abs2.(f(Tuple(p1)) .- n_img)) + st_vals = [-5.0, 1.0, 0.0, 0.0, 0.0, 1.0] #ones(Float64, 6)./10 + # st_vals = Float64[2.0, 0, 0, 0, 0, 0, 1, 0, 0, 1.0, 0, 0, 1, 0, 0, 0, 0, 0] + # @vv f(Tuple(st_vals)) + # loss_m(st_vals) + + res = optimize( + loss_m, + st_vals, + #Newton(), + #BFGS(; linesearch=LineSearches.BackTracking(order=3)), + #LBFGS(),#; linesearch=LineSearches.BackTracking(order=3)), + #lower, upper, + #init_x, + #Fminbox(inner_optimizer), + Optim.Options(store_trace = true, extended_trace = true, iterations=5000), + autodiff = :forward + ) + + # return the estimated parameters + return true_vals, Optim.minimizer(res), res +end + +poly_test(sz=64) \ No newline at end of file diff --git a/src/polynomials.jl b/src/polynomials.jl index 82a02c9..c99429b 100644 --- a/src/polynomials.jl +++ b/src/polynomials.jl @@ -19,7 +19,7 @@ function get_polynomial(::Val{numvars}, ::Val{N}) where {numvars, N} end return s end - function p3(t, c)::Float32 + function p3(t, c) # println("N: $(N), c: $(c) $(length(c))"); p1(t, c[1:length(c)÷(numvars+1)]) + p2(t,c) # int devision needed for type stability! end @@ -37,7 +37,7 @@ function get_multi_poly(::Val{numvars}, ::Val{N}) where {numvars, N} @info "Creating polynomials with $(numvars) variables of order , $(N). Required constants: $(numvars*((numvars+1)^N))" p = get_polynomial(Val(numvars), Val(N)) # return p - function mpol(t,c)::NTuple{numvars, Float32} + function mpol(t,c)#::NTuple{numvars, T} where T return ntuple(n->p(t, split_tuple(c,Val(numvars))[n]), Val(numvars)) # println("N: $(N), c: $(c) $(length(c))"); diff --git a/src/transformators.jl b/src/transformators.jl index 3c7c99f..820d91f 100644 --- a/src/transformators.jl +++ b/src/transformators.jl @@ -19,9 +19,7 @@ This is useful for fitting with a function which is itself defined by measured d By default the value 0.0 is used. Other options are `Flat()`, or `Line()`, See the package `Interpolation` for details. `interp_type`: The type of interpolation to use. See the package `Interpolation` for details. -# Example -```jldoctest -``` + """ function get_function(data::AbstractArray; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) new_size = super_sampling.*size(data) @@ -56,85 +54,165 @@ function get_function(data::AbstractArray; super_sampling=2, extrapolation_bc=ze end +""" + add_dim(cind) + +adds a dimension to a CartesianIndex + +`cind`: A CartesianIndex +""" function add_dim(cind) return SVector.((Tuple(cind))..., 1) end +""" + red_dim(svec::SVector{S,T}) + +removes the last dimension of a SVector to convert it from a homogeneous to a Cartesian coordinates + +`svec::SVector{S,T}`: A SVector +""" @inline function red_dim(svec::SVector{S,T})::SVector{S-1,T} where {S,T} return @view svec[1:S-1] end +""" + red_dim_apply(fct, svec::SVector{S,T}) + +applies a function to a SVector by removing the last dimension to convert it from a homogeneous to a Cartesian coordinates + +`fct`: The function to apply +`svec::SVector{S,T}`: A SVector +""" @inline function red_dim_apply(fct, svec::SVector{S,T}) where {S,T} return fct((@view svec[1:S-1])...) end +""" + red_dim_apply(fct, tup::NTuple{S,T}) + +applies a function to a Tuple by removing the last dimension to convert it from a homogeneous to a Cartesian coordinates + +`fct`: The function to apply +`tup::NTuple{S,T}`: A Tuple +""" @inline function red_dim_apply(fct, tup::NTuple{S,T}) where {S,T} return fct(tup[1:S-1]...) end +""" + idx_apply(fct, svec::SVector{S,T}) where {S,T} + +applies a function to a SVector + +`fct`: The function to apply +`svec::SVector{S,T}`: A SVector +""" @inline function idx_apply(fct, svec::SVector{S,T}) where {S,T} return fct(svec...) end +""" + idx_apply(fct, tup::NTuple{S,T}) where {S,T} + +applies a function to a Tuple + +`fct`: The function to apply +`tup::NTuple{S,T}`: A Tuple +""" @inline function idx_apply(fct, tup::NTuple{S,T}) where {S,T} return fct(tup...) end -# multiplying the transformation matrix -@inline function mat_mul(t::SVector{N, Int64}, matrix_c::SMatrix{N,N,T})::SVector{N,T} where {N,T} +""" + mat_mul(t::SVector{N, T2}, matrix_c::SMatrix{N,N,T}) + +multiplies a SVector with a SMatrix + +`t::SVector{N, T2}`: The SVector to multiply +`matrix_c::SMatrix{N,N,T}`: The SMatrix to multiply with +""" +@inline function mat_mul(t::SVector{N, T2}, matrix_c::SMatrix{N,N,T})::SVector{N,T} where {N,T, T2} return matrix_c * t end -# Applying the coordinate transformation function +""" + func_transform(t, coord_transform_func::Function)::SVector + +applies a coordinate transformation function to an array or `CartesianIndex` and returns the transformed array + +`t`: The array or `CartesianIndex` to transform +`coord_transform_func::Function`: The function to apply the transformation +""" @inline function func_transform(t, coord_transform_func::Function)::SVector return coord_transform_func(Tuple(t)) end +""" + func_transform_tup(t, coord_transform_func::Function) + +applies a coordinate transformation function to a Tuple + +`t`: The array or `CartesianIndex` to transform +`coord_transform_func::Function`: The function to apply the transformation +""" @inline function func_transform_tup(t, coord_transform_func::Function) return coord_transform_func(Tuple(t)) end -# How to run this function: -# f_general((t) -> (t[1]*1.01, t[2]*1.01), out2) - """ - apply_transform!(coord_transf_func::Function, data, itp, out) -applies a homogeneous coordinate-based coordinate transformation function to the indices of an array and returns the transformed array + apply_transform(coord_transf_func::Function, data::AbstractArray{T}, itp) where {T} -coord_transf_func: A function that takes a N-+1 dimensional CartesianIndex and returns a new N+1 dimensional SVector or Tuple -data: The data to transform -itp: The interpolation object to use +applies a general coordinate transformation function to the indices of an array and returns the transformed array +`coord_transf_func:Function`: A function that takes a N-+1 dimensional CartesianIndex and returns a new N+1 dimensional SVector or Tuple +`data::AbstractArray{T}`: The data to transform +`itp`: The interpolation object to use """ -function apply_transform!(coord_transf_func::Function, data, itp, out) - @info "Applying tuple transformation" - out .= idx_apply.(Ref(itp), coord_transf_func.(CartesianIndices(data))); +function apply_transform(coord_transf_func::Function, data::AbstractArray{T}, itp) where {T} #, out::AbstractArray{T}) where {T} + # @info "Applying tuple transformation" + """ + for it in CartesianIndices(data) + out[it] = idx_apply(itp, coord_transf_func(it)) + end + """ + return map((it) -> idx_apply(itp, coord_transf_func(it)), (CartesianIndices(data))) + # out .= idx_apply.(Ref(itp), coord_transf_func.(CartesianIndices(data))); + # return idx_apply.(Ref(itp), coord_transf_func.(CartesianIndices(data))); end """ - apply_transform_homogen!(coord_transf_func::Function, data, itp, out) + apply_transform_homogen(coord_transf_func::Function, data, itp) applies a homogeneous coordinate-based coordinate transformation function to the indices of an array and returns the transformed array -coord_transf_func: A function that takes a N-+1 dimensional homogeneous SVector returns a new N+1 dimensional SVector -data: The data to transform -itp: The interpolation object to use - +`coord_transf_func::Function`: A function that takes a N-+1 dimensional homogeneous SVector returns a new N+1 dimensional SVector +`data`: The data to transform +`itp`: The interpolation object to use """ -function apply_transform_homogen!(coord_transf_func::Function, data, itp, out) +function apply_transform_homogen(coord_transf_func::Function, data, itp)#, out) h_coord_transf_func = (c) -> red_dim(coord_transf_func(add_dim(c))) - apply_transform!(h_coord_transf_func, data, itp, out); + #@info "Applying homogeneous transformation" + return apply_transform(h_coord_transf_func, data, itp)#, out); # out .= itp.(red_dim.(coord_transf_func.(add_dim.(CartesianIndices(data))))); # out .= red_dim_apply.(Ref(itp), coord_transf_func.(add_dim.(CartesianIndices(data)))); end -function apply_transform_affine!(mymat::SMatrix{T}, data, itp, out) where {T} # The SMatrix spec is important to avoid allocations +""" + apply_transform_affine(mymat::SMatrix{T}, data, itp) where T + +applies an affine transformation matrix to the indices of an array and returns the transformed array + +`mymat::SMatrix{T}` The affine transformation matrix to apply +`data`: The data to transform +`itp`: The interpolation function (object) to use +""" +function apply_transform_affine(mymat::SMatrix{T}, data, itp) where T #, out) where {T} # The SMatrix spec is important to avoid allocations # red_dim_apply.(Ref(itp), func_transform.(CartesianIndices(data), Ref(coord_transf_func))); # return red_dim_apply.(Ref(itp), mat_mul.(add_dim.(CartesianIndices(data)), Ref(mymat))); # @info "Applying affine transformation" - homogenous_transform = (c) -> mat_mul(c, mymat) - apply_transform_homogen!(homogenous_transform, data, itp, out); + return apply_transform_homogen(homogenous_transform, data, itp)#, out); #return out end @@ -153,26 +231,38 @@ This is useful for fitting with a function which is itself defined by measured d `interp_type`: The type of interpolation to use. See the package `Interpolation` for details. # Example -```jldoctest -``` + """ function get_function_tuple(data::AbstractArray{T}, fct_tup::Function; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where T # building the extraplation + interpolation object itp = extrapolate(interpolate(data, interp_type), extrapolation_bc); - function interpolated(params, out = similar(data)) + function interpolated(params)#, out = similar(data)) fct_tup_noparams(ci) = fct_tup(ci, params) - return apply_transform!(fct_tup_noparams, data, itp, out); + return apply_transform(fct_tup_noparams, data, itp); end return interpolated end +""" + get_function_svec(data::AbstractArray, fct_hom::Function; super_sampling=1, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) + +returns a function `interpolated(params)` which generates a transformed version of the original data parameterized by transform parameters. + +# Arguments +`data`: The data to represent by the function `dat` +`fct_hom`: The function to apply to the data +`super_sampling`: The factor by which the data is internally represented as a supersampled version (Fourier-based upsampling, see `FourierTools.resample`) +`extrapolation_bc`: The extrapolation boundary condition to select for values outside the range. + By default the value 0.0 is used. Other options are `Flat()`, or `Line()`, See the package `Interpolation` for details. +`interp_type`: The type of interpolation to use. See the package `Interpolation` for details. +""" function get_function_svec(data::AbstractArray{T}, fct_hom::Function; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where T # building the extraplation + interpolation object itp = extrapolate(interpolate(data, interp_type), extrapolation_bc); - function interpolated(params::SVector, out = similar(data)) + function interpolated(params::SVector) #, out = similar(data)) fct_hom_noparams(c) = fct_hom(c, params) - apply_transform_homogen!(fct_hom_noparams, data, itp, out); - return out; + return apply_transform_homogen!(fct_hom_noparams, data, itp)#, out); + # return out; end return interpolated end @@ -180,18 +270,17 @@ end """ get_function_affine(data::AbstractArray; super_sampling=1, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) -returns a function `interpolated(p, [out])` which generates a transformed version of the original data parameterized by transform parameters. +returns a function `interpolated()` which generates a transformed version of the original data parameterized by transform parameters or by transformation matrix. This is useful for fitting with a function which is itself defined by measured data. -The returned function supports two ways to be used, with an affine transform matrix `p` as in input or with a vector `p` of parameters. -The optional argument `out` can be used to store the result of the transformation. +The returned function supports two ways to be used, with an affine transform matrix `matrix_c` as in input or with a vector `p` of parameters. # Arguments `data`: The data to represent by the function `dat` +`super_sampling`: The factor by which the data is internally represented as a supersampled version (Fourier-based upsampling, see `FourierTools.resample`) `extrapolation_bc`: The extrapolation boundary condition to select for values outside the range. By default the value 0.0 is used. Other options are `Flat()`, or `Line()`, See the package `Interpolation` for details. `interp_type`: The type of interpolation to use. See the package `Interpolation` for details. - """ function get_function_affine(data::AbstractArray{T}; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where T #new_size = super_sampling.*size(data) @@ -200,29 +289,29 @@ function get_function_affine(data::AbstractArray{T}; super_sampling=2, extrapola # building the extraplation + interpolation object itp = extrapolate(interpolate(data, interp_type), extrapolation_bc); - function interpolated(matrix_c::SMatrix{T}, out = similar(data)) where T - apply_transform_affine!(matrix_c, data, itp, out); - return out; + function interpolated(matrix_c::SMatrix{T}) where T #, out = similar(data)) where T1 + return apply_transform_affine(matrix_c, data, itp)# , out); + # return out; end - function interpolated(p::AbstractVector{T}, out = similar(data)) where T + function interpolated(p::AbstractVector{T}) where {T} #, out = similar(data)) where T1 x_cen, y_cen = (size(data) .÷ 2.0 .+1) # x_cen_up, y_cen_up = (size(upsampled) .÷ 2.0 .+ 1.0) # creating the matrices of rotation, shear, scale, and shift - rot_mat = @SMatrix [cos(p[7]) -1.0*sin(p[7]) 0.0; sin(p[7]) cos(p[7]) 0.0; 0.0 0.0 1.0]; - shear_mat = @SMatrix [1.0 p[5] 0.0; p[6] 1.0 0.0; 0.0 0.0 1.0]; - scale_mat = @SMatrix [1/p[3] 0.0 0.0; 0.0 1/p[4] 0.0; 0.0 0.0 1.0]; - shift_mat = @SMatrix [1.0 0.0 -1*p[1]; 0.0 1.0 -1*p[2]; 0.0 0.0 1.0]; - t_to_origin = @SMatrix [1.0 0.0 1*x_cen; 0.0 1.0 y_cen; 0.0 0.0 1.0]; - t_to_center = @SMatrix [1.0 0.0 -1.0*x_cen; 0.0 1.0 -1.0*y_cen; 0.0 0.0 1.0]; - # t_orig_upsampled = @SMatrix [1.0 0.0 -1.0*x_cen_up; 0.0 1.0 -1.0*y_cen_up; 0.0 0.0 1.0] + rot_mat = @SMatrix T[cos(p[7]) -1.0*sin(p[7]) 0.0; sin(p[7]) cos(p[7]) 0.0; 0.0 0.0 1.0]; + shear_mat = @SMatrix T[1.0 p[5] 0.0; p[6] 1.0 0.0; 0.0 0.0 1.0]; + scale_mat = @SMatrix T[1/p[3] 0.0 0.0; 0.0 1/p[4] 0.0; 0.0 0.0 1.0]; + shift_mat = @SMatrix T[1.0 0.0 -1*p[1]; 0.0 1.0 -1*p[2]; 0.0 0.0 1.0]; + t_to_origin = @SMatrix T[1.0 0.0 1*x_cen; 0.0 1.0 y_cen; 0.0 0.0 1.0]; + t_to_center = @SMatrix T[1.0 0.0 -1.0*x_cen; 0.0 1.0 -1.0*y_cen; 0.0 0.0 1.0]; + # t_orig_upsampled = SMatrix{3, 3}(T[1.0 0.0 -1.0*x_cen_up; 0.0 1.0 -1.0*y_cen_up; 0.0 0.0 1.0]); # building the overall transformation matrix - matrix_c = t_to_origin * scale_mat * rot_mat *shift_mat * t_to_center + matrix_c = t_to_origin * scale_mat * rot_mat * shear_mat *shift_mat * t_to_center - apply_transform_affine!(matrix_c, data, itp, out); # do not call interolated here for type stability reasons - return out; + return apply_transform_affine(matrix_c, data, itp) #, out); # do not call interolated here for type stability reasons + #return out; end return interpolated @@ -240,10 +329,11 @@ The optional argument `out` can be used to store the result of the transformatio # Arguments `data`: The data to represent by the function `dat` +`order`: The order of the polynomial to use for the transformation +`super_sampling`: The factor by which the data is internally represented as a supersampled version (Fourier-based upsampling, see `FourierTools.resample`) `extrapolation_bc`: The extrapolation boundary condition to select for values outside the range. By default the value 0.0 is used. Other options are `Flat()`, or `Line()`, See the package `Interpolation` for details. `interp_type`: The type of interpolation to use. See the package `Interpolation` for details. - """ function get_function_poly(data::AbstractArray{T}, order; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where T pm = get_multi_poly(Val(ndims(data)), Val(order)) diff --git a/test/Aqua.jl b/test/Aqua.jl new file mode 100644 index 0000000..e7b0cce --- /dev/null +++ b/test/Aqua.jl @@ -0,0 +1,6 @@ +using Aqua + +Aqua.test_all( + DataToFunctions, + unbound_args=false, + ) \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index ddd940c..1735829 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,32 +1,36 @@ using Test -# using Zygote +using Zygote using DataToFunctions -@testset "get_function" begin +include("Aqua.jl") + +@testset "get_function_affine" begin data = rand(40,41) - for supersamp = 1:5 - f = get_function(data; super_sampling=supersamp); - @test f((0.0,0.0),(1.0,1.0)) ≈ data - end + f = get_function_affine(data); + @test f([0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0]) ≈ data + end -@testset "gradient" begin +@testset "loss" begin data = rand(11,10) - f = get_function(data; super_sampling=2); - loss(p,z) = sum(abs2.(f(p, z) .- data)) - @test loss((0.0,0.0),(1.0,1.0)) < 1e-20 - @test loss((0.0,0.001),(1.0,1.0)) > 1e-20 - @test loss((0.0,0.0),(1.0001,1.0)) > 1e-20 + f = get_function_affine(data; super_sampling=2); + loss(p) = sum(abs2.(f(p) .- data)) + @test loss([0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0]) < 1e-20 + @test loss([0.001, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0]) > 1e-20 + @test loss([0.0, 0.0, 1.001, 1.0, 0.0, 0.0, 0.0]) > 1e-20 +end +@testset "gradient" begin + data = rand(11,10) + f = get_function_affine(data; super_sampling=2); + loss(p) = sum(abs2.(f(p) .- data)) + st_vals = [0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0] # throws an error... - # Zygote.gradient(loss, (0.0,0.0), (1.0,1.0)) + @test Zygote.gradient(loss, st_vals)[1] ≈ zeros(7) end @testset "keep center" begin data = ones(5,4); data[3,3] = 5.0; - f = get_function(data; super_sampling=5); - @test f((0.0,0.0),(2.0,2.0))[3,3] ≈ 5.0 - - # throws an error... - # Zygote.gradient(loss, (0.0,0.0), (1.0,1.0)) + f = get_function(data); + @test f([0.0, 0.0, 2.0, 2.0, 0.0, 0.0, 0.0])[3,3] ≈ 5.0 end From 87f43254f16588659db47e5b9ccdade5f68a5705 Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Wed, 10 Jul 2024 12:40:11 +0200 Subject: [PATCH 21/44] added Github workflow --- .github/workflows/ci.yaml | 56 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/ci.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..456b8e6 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,56 @@ +name: CI +on: + pull_request: + branches: + - master + push: + branches: + - master + tags: '*' +concurrency: + # Skip intermediate builds: always. + # Cancel intermediate builds: only if it is a pull request build. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} +jobs: + test: + name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + version: + - '1' + os: + - [ubuntu-latest] + arch: + - x64 + steps: + - uses: actions/checkout@v3 + - uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: julia-actions/cache@v1 + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + file: lcov.info + + + docs: + name: Documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: "1.9" + - uses: julia-actions/julia-docdeploy@releases/v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} \ No newline at end of file From 378797875c78a4841e0ab4f3f55afbcd17a85675 Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Wed, 10 Jul 2024 12:41:41 +0200 Subject: [PATCH 22/44] renaming ci.yml --- .github/workflows/{ci.yaml => ci.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{ci.yaml => ci.yml} (100%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yml similarity index 100% rename from .github/workflows/ci.yaml rename to .github/workflows/ci.yml From 2a1b76406c65d2ea74c42079cf117fdf6d7f4298 Mon Sep 17 00:00:00 2001 From: RainerHeintzmann Date: Sun, 14 Jul 2024 19:33:31 +0200 Subject: [PATCH 23/44] fixed bug and without creator --- Project.toml | 1 + src/polynomials.jl | 94 ++++++++++++++++++++++++++++++---------------- 2 files changed, 62 insertions(+), 33 deletions(-) diff --git a/Project.toml b/Project.toml index 9f7abd6..ff6cd88 100644 --- a/Project.toml +++ b/Project.toml @@ -7,6 +7,7 @@ version = "0.1.0" FourierTools = "b18b359b-aebc-45ac-a139-9c0ccbb2871e" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +Unrolled = "9602ed7d-8fef-5bc8-8597-8f21381861e8" [compat] Aqua = "0.8" diff --git a/src/polynomials.jl b/src/polynomials.jl index c99429b..2bf4ea6 100644 --- a/src/polynomials.jl +++ b/src/polynomials.jl @@ -1,36 +1,61 @@ export get_polynomial, get_multi_poly, get_num_poly_vars, get_num_multipoly_vars +export polynomial -function get_polynomial(::Val{numvars}, ::Val{0}) where {numvars} - # @info "Creating polynomials of order 0" - return (t, c) -> begin - # println("c: $(c) $(length(c))"); - return c[1] - end #, (t,c) -> ntuple(n->c[1], Val(numvars)) -end +using Unrolled -function get_polynomial(::Val{numvars}, ::Val{N}) where {numvars, N} - # @info "Creating polynomials with $(numvars) variables of order , $(N). Required constants: $((numvars+1)^N)" - p1 = get_polynomial(Val(numvars), Val(N-1)); # is reused multiple times - p2(t, c) = begin - s = p1(t, c[1+length(c)÷(numvars+1):(2)*length(c)÷(numvars+1)]) * t[1] # int devision needed for type stability! - # s = 0 - for n in 2:numvars - s += p1(t, c[1+n*length(c)÷(numvars+1):(n+1)*length(c)÷(numvars+1)]) * t[n] # int devision needed for type stability! - end - return s - end - function p3(t, c) - # println("N: $(N), c: $(c) $(length(c))"); - p1(t, c[1:length(c)÷(numvars+1)]) + p2(t,c) # int devision needed for type stability! - end +""" + get_polynomial(::Val{numvars}, ::Val{0}) where {numvars, N} + +Create a polynomial of order 0 with numvars variables. - # function p3m(t, c)::NTuple{numvars, Float32} - # # println("N: $(N), c: $(c) $(length(c))"); - # ntuple(n -> p1(t, c[1+(n-1)*((numvars+1)^N):length(c)÷(numvars+1) + (n-1)*((numvars+1)^N)]) + p2(t,c), Val(numvars)) # int devision needed for type stability! - # end +returned is a function that takes a tuple of variables and a tuple of coefficients and returns the value of the polynomial + and the number of coefficients required (here 1). +""" +# function get_polynomial(::Val{numvars}, ::Val{0}) where {numvars} +# # @info "Creating polynomials of order 0" +function polynomial(::Val{0}, ::T1, c::T2) where {T1 <: NTuple, T2 <: NTuple} + # println("c: $(c) $(length(c))"); + return c[1] +end #, (t,c) -> ntuple(n->c[1], Val(numvars)) +# return myconst +# end - return p3 # , p3m +""" + get_polynomial(::Val{numvars}, ::Val{N}) where {numvars, N} + +Create a polynomial of order N with numvars variables. +returned is a function that takes a tuple of variables and a tuple of coefficients and returns the value of the polynomial. + +E.g. to represent a polynomial of order 1 with 2 variables, the coefficients are ordered as follows: +c = (c0, c1, c2) where the polynomial is: c0 + c1*x + c2*y +or for a polynomial of order 2 with 2 variables: +c = (c0, c1, c2, c3, c4, c5) where the polynomial is c0 + c1*x + c2*x^2 + c3*y + c4*x*y + c5*y^2 +Note that the coefficients are ordered not by the multiples in which they appear in the polynomial, but by the order of the variables. +""" +# function get_polynomial(::Val{numvars}, ::Val{N}) where {numvars, N} +# # @info "Creating polynomials with $(numvars) variables of order , $(N). Required constants: $((numvars+1)^N)" + +function polynomial(::Val{N}, t::T1, c::T2)::Float32 where {N, T1<:NTuple, T2<:NTuple} # :: NTuple{NV, Float32}, NTuple{M, Float32} + c_start = 1 + res = c[c_start] + c_start += 1 + # iterate through the polynomial variables + for n in eachindex(t) # 1:length(t) # eachindex(t) # 1:length(t) + # subpoly = get_polynomial(Val(n), Val(N-1)); # calulate polynomial with only n variable + c_end = c_start + get_num_poly_vars(Val(n), Val(N-1)) - 1 + # @show N + # @show n + # @show c_start + # @show t + # @show c + res += t[n] * polynomial(Val(N-1), t[1:n], c[c_start:c_end]) + c_start = c_end + 1 + end + return res end +# return mypoly +# end + function get_multi_poly(::Val{numvars}, ::Val{N}) where {numvars, N} # cs_per_comp = ((numvars+1)^N) @@ -51,7 +76,7 @@ function get_multi_poly(::Val{numvars}, ::Val{N}) where {numvars, N} end function get_num_poly_vars(::Val{numvars}, ::Val{N}) where {numvars, N} - return (numvars+1)^N + return binomial(numvars+N, N) end function get_num_multipoly_vars(::Val{numvars}, ::Val{N}) where {numvars, N} @@ -59,15 +84,18 @@ function get_num_multipoly_vars(::Val{numvars}, ::Val{N}) where {numvars, N} end function test_poly_allocations() - get_num_poly_vars(Val(2), Val(3)) # (2+1)^3 - p = get_polynomial(Val(2), Val(3)) # 27 indices required + get_num_poly_vars(Val(2), Val(3)) # 10 indices + p = get_polynomial(Val(2), Val(3)) @time p.(Tuple.(CartesianIndices((200,200))),Ref((1.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))); - # does allocate 9 Mb ! + cids = Tuple.(CartesianIndices((200,200))) + cs = Tuple(Float32.([1.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])) + @time polynomial.(Ref(Val(3)), cids, Ref(cs)); + # does allocate 95 Mb ! - p = get_polynomial(Val(3), Val(2)) # 9 indices required + p = get_polynomial(Val(3), Val(2)) # 10 indices required get_num_poly_vars(Val(3), Val(2)) @time p.(Tuple.(CartesianIndices((100,100,10))),Ref((1.1,2.1,3.1,4,5,6,7,8,9,10,11,12,13,14,15,16))); - # does allocate 160 Mb ! + # does allocate 256 Mb ! p = get_polynomial(Val(2), Val(2)) # 9 indices required @time p.(Tuple.(CartesianIndices((200,200))),Ref((1.1,2.1,3.1,4,5,6,7,8,9))); From 9c2c7f75e818293ff5b607e1f923b295a222ef6c Mon Sep 17 00:00:00 2001 From: RainerHeintzmann Date: Sun, 14 Jul 2024 21:01:55 +0200 Subject: [PATCH 24/44] finally got it working fast --- src/polynomials.jl | 105 ++++++++++++++++++++++++++++++++------------- 1 file changed, 75 insertions(+), 30 deletions(-) diff --git a/src/polynomials.jl b/src/polynomials.jl index 2bf4ea6..19fad7c 100644 --- a/src/polynomials.jl +++ b/src/polynomials.jl @@ -11,49 +11,86 @@ Create a polynomial of order 0 with numvars variables. returned is a function that takes a tuple of variables and a tuple of coefficients and returns the value of the polynomial and the number of coefficients required (here 1). """ -# function get_polynomial(::Val{numvars}, ::Val{0}) where {numvars} -# # @info "Creating polynomials of order 0" -function polynomial(::Val{0}, ::T1, c::T2) where {T1 <: NTuple, T2 <: NTuple} +function polynomial(::Val{0}, ::T1, c::T2, ::Val{cstart}=Val(1), ::Val{numvars}=Val(1)) where {NV, TS, T1 <: NTuple{NV, Integer}, T2 <: NTuple{TS, Float32}, cstart, numvars} # println("c: $(c) $(length(c))"); - return c[1] + return c[cstart] end #, (t,c) -> ntuple(n->c[1], Val(numvars)) -# return myconst -# end """ - get_polynomial(::Val{numvars}, ::Val{N}) where {numvars, N} + polynomial(::Val{N}, t::T1, c::T2, ::Val{cstart}=Val(1), ::Val{numvars}=Val(length(t)))::Float32 where {N, NV, TS, T1 <: NTuple{NV, Integer}, T2 <: NTuple{TS, Float32}, cstart, numvars} -Create a polynomial of order N with numvars variables. -returned is a function that takes a tuple of variables and a tuple of coefficients and returns the value of the polynomial. +Represents a polynomial of order N with numvars variables (also implicitely defined via the length of the NTuple `t`). +Note that `numvars` is needed for the internal workings of the polynomial generator, but notmally not by the user. E.g. to represent a polynomial of order 1 with 2 variables, the coefficients are ordered as follows: c = (c0, c1, c2) where the polynomial is: c0 + c1*x + c2*y or for a polynomial of order 2 with 2 variables: c = (c0, c1, c2, c3, c4, c5) where the polynomial is c0 + c1*x + c2*x^2 + c3*y + c4*x*y + c5*y^2 Note that the coefficients are ordered not by the multiples in which they appear in the polynomial, but by the order of the variables. -""" -# function get_polynomial(::Val{numvars}, ::Val{N}) where {numvars, N} -# # @info "Creating polynomials with $(numvars) variables of order , $(N). Required constants: $((numvars+1)^N)" -function polynomial(::Val{N}, t::T1, c::T2)::Float32 where {N, T1<:NTuple, T2<:NTuple} # :: NTuple{NV, Float32}, NTuple{M, Float32} - c_start = 1 +Example: +```jldoctest +>julia polynomial(Val(2), (2, 20), (1f0, 1f0, 1f0, 1f0, 1f0, 1f0)) +467.0f0 +>julia cs = Tuple(Float32.(collect(1:27))) + (1.0f0, 2.0f0, 3.0f0, 4.0f0, 5.0f0, 6.0f0, 7.0f0, 8.0f0, 9.0f0, 10.0f0, 11.0f0, 12.0f0, 13.0f0, 14.0f0, 15.0f0, 16.0f0, 17.0f0, 18.0f0, 19.0f0, 20.0f0, 21.0f0, 22.0f0, 23.0f0, 24.0f0, 25.0f0, 26.0f0, 27.0f0) +>julia res = zeros(Float32, 200,200) +>julia @time res .= polynomial.(Ref(Val(2)), Tuple.(CartesianIndices((200,200))), Ref(cs)); + 0.054017 seconds (147.45 k allocations: 10.168 MiB, 99.75% compilation time) +>julia @time res .= polynomial.(Ref(Val(2)), Tuple.(CartesianIndices((200,200))), Ref(cs)); + 0.000089 seconds (3 allocations: 184 bytes) +``` +""" +function polynomial(::Val{N}, t::T1, c::T2, ::Val{cstart}=Val(1), ::Val{numvars}=Val(length(t)))::Float32 where {N, NV, TS, T1 <: NTuple{NV, Integer}, T2 <: NTuple{TS, Float32}, cstart, numvars} + c_start = cstart res = c[c_start] c_start += 1 - # iterate through the polynomial variables - for n in eachindex(t) # 1:length(t) # eachindex(t) # 1:length(t) - # subpoly = get_polynomial(Val(n), Val(N-1)); # calulate polynomial with only n variable - c_end = c_start + get_num_poly_vars(Val(n), Val(N-1)) - 1 - # @show N - # @show n - # @show c_start - # @show t - # @show c - res += t[n] * polynomial(Val(N-1), t[1:n], c[c_start:c_end]) - c_start = c_end + 1 + # iterate through the polynomial variables: (but this leads to dynamic memory allocation!) + # for n = 1:numvars # eachindex(t) # 1:length(t) # eachindex(t) # 1:length(t) + # # c_end = c_start + get_num_poly_vars(Val(n), Val(N-1)) - 1 + # res += t[n] * polynomial(Val(N-1), t, c, Val(c_start), Val(n)) + # c_start += get_num_poly_vars(Val(n), Val(N-1)) # c_end + 1 + # end + # # does not work: + # @macroexpand Base.Cartesian.@nexprs 4 n -> begin + # if (numvars >= n) + # res += t[n] * polynomial(Val(N-1), t, c, Val(c_start), Val(n)) + # c_start += get_num_poly_vars(Val(n), Val(N-1)) # c_end + 1 + # end + # end + + # this simply unrolls the loop by hand (up to 4D input variables): + if numvars >= 1 + res += t[1] * polynomial(Val(N-1), t, c, Val(c_start), Val(1)) + c_start += get_num_poly_vars(Val(1), Val(N-1)) # c_end + 1 + end + if numvars >= 2 + res += t[2] * polynomial(Val(N-1), t, c, Val(c_start), Val(2)) + c_start += get_num_poly_vars(Val(2), Val(N-1)) # c_end + 1 + end + if numvars >= 3 + res += t[3] * polynomial(Val(N-1), t, c, Val(c_start), Val(3)) + c_start += get_num_poly_vars(Val(3), Val(N-1)) # c_end + 1 + end + if numvars >= 4 + res += t[4] * polynomial(Val(N-1), t, c, Val(c_start), Val(4)) + c_start += get_num_poly_vars(Val(4), Val(N-1)) # c_end + 1 + end + if numvars >= 5 + error("Only up to 4 dimensions are currently supported for polynomials") end return res end -# return mypoly + + +# function unroll_loop(res::Float32, ::Var{0}, ::Var{numvars}) where{n} +# return 0f0; +# end + +# function unroll_loop(res::Float32, t, ::Var{c_start},::Var({n}, ::Var{numvars}) where{c_start, n, numvars} +# res += t[n] * polynomial(Val(N-1), t, c, Val(c_start), Val(n)) +# c_start += get_num_poly_vars(Val(n), Val(N-1)) # c_end + 1 +# return unroll_loop(res::Float32, ::Var({n}, ::Var{numvars}) + # end @@ -84,14 +121,22 @@ function get_num_multipoly_vars(::Val{numvars}, ::Val{N}) where {numvars, N} end function test_poly_allocations() - get_num_poly_vars(Val(2), Val(3)) # 10 indices - p = get_polynomial(Val(2), Val(3)) - @time p.(Tuple.(CartesianIndices((200,200))),Ref((1.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))); + # get_num_poly_vars(Val(2), Val(3)) # 10 indices + # p = get_polynomial(Val(2), Val(3)) + # @time p.(Tuple.(CartesianIndices((200,200))),Ref((1.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))); + + polynomial(Val(2), (2f0, 20f0), (1f0, 1f0, 1f0, 1f0, 1f0, 1f0)) == 467 + cids = Tuple.(CartesianIndices((200,200))) + # cfds = map((t)->Tuple(Float32.([t...])), cids) cs = Tuple(Float32.([1.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])) - @time polynomial.(Ref(Val(3)), cids, Ref(cs)); + res = zeros(Float32, 200,200) + @time res .= polynomial.(Ref(Val(2)), cids, Ref(cs)); + # 0.184371 seconds (2.52 M allocations: 75.226 MiB, 2.59% gc time) # does allocate 95 Mb ! + @time polynomial.(Ref(Val(0)), cfds, Ref(cs)); + p = get_polynomial(Val(3), Val(2)) # 10 indices required get_num_poly_vars(Val(3), Val(2)) @time p.(Tuple.(CartesianIndices((100,100,10))),Ref((1.1,2.1,3.1,4,5,6,7,8,9,10,11,12,13,14,15,16))); From 22f6789cff4ef49bb0bfc41aa99e0e54a4d6065f Mon Sep 17 00:00:00 2001 From: RainerHeintzmann Date: Sun, 14 Jul 2024 21:08:54 +0200 Subject: [PATCH 25/44] a bit of cleanup --- src/polynomials.jl | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/src/polynomials.jl b/src/polynomials.jl index 19fad7c..cf6db67 100644 --- a/src/polynomials.jl +++ b/src/polynomials.jl @@ -129,30 +129,25 @@ function test_poly_allocations() cids = Tuple.(CartesianIndices((200,200))) # cfds = map((t)->Tuple(Float32.([t...])), cids) - cs = Tuple(Float32.([1.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])) + cs = Tuple(Float32.(collect(1:27))) res = zeros(Float32, 200,200) - @time res .= polynomial.(Ref(Val(2)), cids, Ref(cs)); - # 0.184371 seconds (2.52 M allocations: 75.226 MiB, 2.59% gc time) - # does allocate 95 Mb ! + get_num_poly_vars(Val(2), Val(2)) # 6 indices required + @time res .= polynomial.(Ref(Val(2)), cids, Ref(cs)); # 2 orders, two variables + # 0.000122 seconds (3 allocations: 168 bytes) - @time polynomial.(Ref(Val(0)), cfds, Ref(cs)); + get_num_poly_vars(Val(3), Val(2)) # 10 indices required + @time res .= polynomial.(Ref(Val(3)), cids, Ref(cs)); # 2 orders, two variables + # 0.003710 seconds (240.00 k allocations: 13.428 MiB) - p = get_polynomial(Val(3), Val(2)) # 10 indices required - get_num_poly_vars(Val(3), Val(2)) - @time p.(Tuple.(CartesianIndices((100,100,10))),Ref((1.1,2.1,3.1,4,5,6,7,8,9,10,11,12,13,14,15,16))); - # does allocate 256 Mb ! + get_num_poly_vars(Val(4), Val(2)) # 15 indices required + @time res .= polynomial.(Ref(Val(4)), cids, Ref(cs)); # 2 orders, two variables + # 0.008149 seconds (480.00 k allocations: 26.856 MiB) - p = get_polynomial(Val(2), Val(2)) # 9 indices required - @time p.(Tuple.(CartesianIndices((200,200))),Ref((1.1,2.1,3.1,4,5,6,7,8,9))); - # essentially allocation-free + get_num_poly_vars(Val(5), Val(2)) # 21 indices required + @time res .= polynomial.(Ref(Val(5)), cids, Ref(cs)); # 2 orders, two variables + #0.014299 seconds (1.08 M allocations: 60.425 MiB, 23.18% gc time) - p = get_polynomial(Val(2), Val(1)) # 3 indices required - @time p.(Tuple.(CartesianIndices((200,200))),Ref((1.1,2.1,3.1))); - # essentially allocation-free - # p((100,100),((1.1,2.2, 3.3))) - - p = get_polynomial(Val(1), Val(0)) # 27 indices required - @time p.(Tuple.(CartesianIndices((100,100))),Ref((1.1))); - # essentially allocation-free + @time polynomial.(Ref(Val(0)), cids, Ref(cs)); + # 0.000076 seconds (5 allocations: 156.461 KiB) end From 8c7ddc75005468de5f3f2fed15bde7afd04cf604 Mon Sep 17 00:00:00 2001 From: RainerHeintzmann Date: Mon, 15 Jul 2024 07:19:13 +0200 Subject: [PATCH 26/44] removed Unrolled --- Project.toml | 1 - src/polynomials.jl | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index ff6cd88..9f7abd6 100644 --- a/Project.toml +++ b/Project.toml @@ -7,7 +7,6 @@ version = "0.1.0" FourierTools = "b18b359b-aebc-45ac-a139-9c0ccbb2871e" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" -Unrolled = "9602ed7d-8fef-5bc8-8597-8f21381861e8" [compat] Aqua = "0.8" diff --git a/src/polynomials.jl b/src/polynomials.jl index cf6db67..00a419b 100644 --- a/src/polynomials.jl +++ b/src/polynomials.jl @@ -1,7 +1,7 @@ export get_polynomial, get_multi_poly, get_num_poly_vars, get_num_multipoly_vars export polynomial -using Unrolled +# using Unrolled """ get_polynomial(::Val{numvars}, ::Val{0}) where {numvars, N} From 38cbb839f10bb774a9dcc9c0b12008ff6980b616 Mon Sep 17 00:00:00 2001 From: RainerHeintzmann Date: Mon, 15 Jul 2024 08:38:52 +0200 Subject: [PATCH 27/44] added a Pluto example notebook + bugfixes --- .gitignore | 2 +- examples/Project.toml | 3 + examples/deform_testimage.jl | 120 +++++++++++++++++++++++++ examples/deform_testimgage backup 1.jl | 50 +++++++++++ src/polynomials.jl | 6 +- 5 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 examples/deform_testimage.jl create mode 100644 examples/deform_testimgage backup 1.jl diff --git a/.gitignore b/.gitignore index 1cc978c..ad46f5a 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,7 @@ docs/site/ # committed for packages, but should be committed for applications that require a static # environment. Manifest.toml - +examples/Manifest.toml *.mp4 *.png diff --git a/examples/Project.toml b/examples/Project.toml index 630815d..9193d7d 100644 --- a/examples/Project.toml +++ b/examples/Project.toml @@ -7,6 +7,7 @@ Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" FourierTools = "b18b359b-aebc-45ac-a139-9c0ccbb2871e" GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" +ImageShow = "4e3cecfd-b093-5904-9786-8bbb286a6a31" Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" InverseModeling = "ce844058-9528-415d-a63d-06f3dd08b29f" @@ -17,6 +18,8 @@ Noise = "81d43f40-5267-43b7-ae1c-8b967f377efa" OhMyREPL = "5fb14364-9ced-5910-84b2-373655c76a03" Optim = "429524aa-4258-5aef-a3af-852621145aeb" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +Pluto = "c3e4b0f8-55cb-11ea-2926-15256bba5781" +PlutoUI = "7f904dfe-b85e-4ff6-b463-dae2292396a8" PointSpreadFunctions = "e8810a93-244e-46c5-8da3-35c5dd956001" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568" diff --git a/examples/deform_testimage.jl b/examples/deform_testimage.jl new file mode 100644 index 0000000..2b46205 --- /dev/null +++ b/examples/deform_testimage.jl @@ -0,0 +1,120 @@ +### A Pluto.jl notebook ### +# v0.19.43 + +using Markdown +using InteractiveUtils + +# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error). +macro bind(def, element) + quote + local iv = try Base.loaded_modules[Base.PkgId(Base.UUID("6e696c72-6542-2067-7265-42206c756150"), "AbstractPlutoDingetjes")].Bonds.initial_value catch; b -> missing; end + local el = $(esc(element)) + global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : iv(el) + el + end +end + +# ╔═╡ 28975586-853e-4e19-b9eb-65c41fa61a43 +using Pkg + +# ╔═╡ 0ae2da4f-3f75-47bb-a899-9e89c5c3f17c +Pkg.activate(".") + +# ╔═╡ a2d75cfb-feab-4130-8439-30c543618d04 +using DataToFunctions, ImageShow, TestImages, PlutoUI, Images + +# ╔═╡ 4af0c13d-fc42-4fe7-97e6-2248e36b63e2 +# Pkg.add("PlutoUI") + +# ╔═╡ 5ac1123d-5df3-4c9d-aff1-ffe91d931497 +data = testimage("resolution_test_512") + +# ╔═╡ b0c8d15e-1bd3-4e66-b2b9-57885636eb48 + + +# ╔═╡ 2fce0208-732f-4259-a47d-7f78921bfd87 +f = get_function(data, super_sampling=1) + +# ╔═╡ dd535378-3f2a-4429-914c-8b7608b99706 +@bind shift_x Slider(-100:0.02:100, default=0) + +# ╔═╡ 3e26d4e8-b5d4-4414-8936-379ec63cb4d2 +@bind shift_y Slider(-100:0.02:100, default=0) + +# ╔═╡ bc3f3659-aa70-4e7f-bef6-4d05229a03c4 +@bind zoom_x Slider(0.2:0.02:4, default=1) + +# ╔═╡ 39f11dc5-351e-4c6d-8fc4-468222e99976 +@bind zoom_y Slider(0.2:0.02:4, default=1) + +# ╔═╡ 64b64d3b-092f-4553-916d-f7db1fdfa428 +f((shift_x, shift_y), (1/zoom_x, 1/zoom_y)) + +# ╔═╡ c3fe6b25-f4c0-4a80-9d15-9ee30136d43b +typeof(f((shift_x, shift_y), (1/zoom_x, 1/zoom_y))) + +# ╔═╡ ff143f0d-b070-4220-8fa6-0b5a93a56303 +typeof(data) + +# ╔═╡ 1bed34fb-b29a-4042-a493-4835fdb69a9d +g =DataToFunctions.get_function_affine(data, super_sampling=1) + +# ╔═╡ c287fa80-426b-11ef-125e-5fda207e605c +# ╠═╡ disabled = true +#=╠═╡ +g() + ╠═╡ =# + +# ╔═╡ 87be45c0-8b2e-4d49-abd1-a274b3c1815e +h = get_function_poly(data, 1) + +# ╔═╡ 473d635e-d06d-4cdb-991f-22c82a06b491 +@bind c1 Slider(-1f0:0.05f0:1f0, default=0) + +# ╔═╡ 7fb3ef17-d0a0-4919-b4d5-8e66b4a0fe60 +@bind c2 Slider(-2f0:0.05f0:2f0, default=1) + +# ╔═╡ 200bab4d-b444-47c6-b4d8-d5d3c450e5f9 +@bind c3 Slider(-2f0:0.05f0:2f0, default=1) + +# ╔═╡ f17402a6-ba63-44e9-8e22-da027b07ffc3 +@bind c4 Slider(-2f0:0.05f0:2f0, default=1) + +# ╔═╡ d913278e-1658-4bee-9e12-ad778e530c1b +@bind c5 Slider(-2f0:0.05f0:2f0, default=1) + +# ╔═╡ cee301da-2b3e-432f-9551-0fe83bf6c8ec +@bind c6 Slider(-2f0:0.05f0:2f0, default=1) + +# ╔═╡ 74228a9d-6cc2-4aaf-97da-67f32670341e +Gray.(h((c1,c2,c3,c4,c5,c6,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0))) + +# ╔═╡ 687a198b-9020-4959-8e27-fd0896d4b1fc +maximum(h((c1,c2,c3,c4,c5,c6,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0))) + +# ╔═╡ Cell order: +# ╠═28975586-853e-4e19-b9eb-65c41fa61a43 +# ╠═0ae2da4f-3f75-47bb-a899-9e89c5c3f17c +# ╠═4af0c13d-fc42-4fe7-97e6-2248e36b63e2 +# ╠═a2d75cfb-feab-4130-8439-30c543618d04 +# ╠═5ac1123d-5df3-4c9d-aff1-ffe91d931497 +# ╠═b0c8d15e-1bd3-4e66-b2b9-57885636eb48 +# ╠═2fce0208-732f-4259-a47d-7f78921bfd87 +# ╠═dd535378-3f2a-4429-914c-8b7608b99706 +# ╠═3e26d4e8-b5d4-4414-8936-379ec63cb4d2 +# ╠═bc3f3659-aa70-4e7f-bef6-4d05229a03c4 +# ╠═39f11dc5-351e-4c6d-8fc4-468222e99976 +# ╠═64b64d3b-092f-4553-916d-f7db1fdfa428 +# ╠═c3fe6b25-f4c0-4a80-9d15-9ee30136d43b +# ╠═ff143f0d-b070-4220-8fa6-0b5a93a56303 +# ╠═1bed34fb-b29a-4042-a493-4835fdb69a9d +# ╠═c287fa80-426b-11ef-125e-5fda207e605c +# ╠═87be45c0-8b2e-4d49-abd1-a274b3c1815e +# ╠═473d635e-d06d-4cdb-991f-22c82a06b491 +# ╠═7fb3ef17-d0a0-4919-b4d5-8e66b4a0fe60 +# ╠═200bab4d-b444-47c6-b4d8-d5d3c450e5f9 +# ╠═f17402a6-ba63-44e9-8e22-da027b07ffc3 +# ╠═d913278e-1658-4bee-9e12-ad778e530c1b +# ╠═cee301da-2b3e-432f-9551-0fe83bf6c8ec +# ╠═74228a9d-6cc2-4aaf-97da-67f32670341e +# ╠═687a198b-9020-4959-8e27-fd0896d4b1fc diff --git a/examples/deform_testimgage backup 1.jl b/examples/deform_testimgage backup 1.jl new file mode 100644 index 0000000..2a37851 --- /dev/null +++ b/examples/deform_testimgage backup 1.jl @@ -0,0 +1,50 @@ +### A Pluto.jl notebook ### +# v0.19.43 + +using Markdown +using InteractiveUtils + +# ╔═╡ 28975586-853e-4e19-b9eb-65c41fa61a43 +using Pkg + +# ╔═╡ a2d75cfb-feab-4130-8439-30c543618d04 +using DataToFunctions, ImageShow, TestImages + +# ╔═╡ 0ae2da4f-3f75-47bb-a899-9e89c5c3f17c +# Pkg.activate(".") + +# ╔═╡ 4af0c13d-fc42-4fe7-97e6-2248e36b63e2 +# Pkg.add("ImageShow") + +# ╔═╡ 5ac1123d-5df3-4c9d-aff1-ffe91d931497 +data = testimage("resolution_test_512", super_sampling=1) + +# ╔═╡ b0c8d15e-1bd3-4e66-b2b9-57885636eb48 + + +# ╔═╡ 2fce0208-732f-4259-a47d-7f78921bfd87 +f = get_function(data) + +# ╔═╡ 1bed34fb-b29a-4042-a493-4835fdb69a9d + + +# ╔═╡ c287fa80-426b-11ef-125e-5fda207e605c +# ╠═╡ disabled = true +#=╠═╡ +using DataToFunctions + ╠═╡ =# + +# ╔═╡ 87be45c0-8b2e-4d49-abd1-a274b3c1815e + + +# ╔═╡ Cell order: +# ╠═28975586-853e-4e19-b9eb-65c41fa61a43 +# ╠═0ae2da4f-3f75-47bb-a899-9e89c5c3f17c +# ╠═4af0c13d-fc42-4fe7-97e6-2248e36b63e2 +# ╠═a2d75cfb-feab-4130-8439-30c543618d04 +# ╠═5ac1123d-5df3-4c9d-aff1-ffe91d931497 +# ╠═b0c8d15e-1bd3-4e66-b2b9-57885636eb48 +# ╠═2fce0208-732f-4259-a47d-7f78921bfd87 +# ╠═1bed34fb-b29a-4042-a493-4835fdb69a9d +# ╠═c287fa80-426b-11ef-125e-5fda207e605c +# ╠═87be45c0-8b2e-4d49-abd1-a274b3c1815e diff --git a/src/polynomials.jl b/src/polynomials.jl index 00a419b..4b6e989 100644 --- a/src/polynomials.jl +++ b/src/polynomials.jl @@ -4,7 +4,7 @@ export polynomial # using Unrolled """ - get_polynomial(::Val{numvars}, ::Val{0}) where {numvars, N} + polynomial(::Val{0}, ::T1, c::T2, ::Val{cstart}=Val(1), ::Val{numvars}=Val(1)) where {NV, TS, T1 <: NTuple{NV, Integer}, T2 <: NTuple{TS, Float32}, cstart, numvars} Create a polynomial of order 0 with numvars variables. @@ -97,10 +97,10 @@ end function get_multi_poly(::Val{numvars}, ::Val{N}) where {numvars, N} # cs_per_comp = ((numvars+1)^N) @info "Creating polynomials with $(numvars) variables of order , $(N). Required constants: $(numvars*((numvars+1)^N))" - p = get_polynomial(Val(numvars), Val(N)) + p = (t,c) -> polynomial(Val(N), Tuple.(t), c) # return p function mpol(t,c)#::NTuple{numvars, T} where T - return ntuple(n->p(t, split_tuple(c,Val(numvars))[n]), Val(numvars)) + return ntuple(n->p(t, split_tuple(c, Val(numvars))[n]), Val(numvars)) # println("N: $(N), c: $(c) $(length(c))"); # return Tuple(p(t, c[1+(n-1)*((numvars+1)^N):n*((numvars+1)^N)]) for n=1:numvars) From e73820417d897541e80ae77e38b7fadde74e9209 Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Mon, 15 Jul 2024 10:50:09 +0200 Subject: [PATCH 28/44] example of polynomial IM --- examples/Project.toml | 2 ++ examples/polynomial_apply.jl | 55 +++++++++++++++++++++++++++--------- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/examples/Project.toml b/examples/Project.toml index 630815d..b7be6d7 100644 --- a/examples/Project.toml +++ b/examples/Project.toml @@ -4,10 +4,12 @@ CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" CoordinateTransformations = "150eb455-5306-5404-9cee-2592286d6298" DataToFunctions = "64cfdffa-4d02-49ee-ae8b-a805370874f5" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +FindShift = "643ec891-bf64-479f-8088-26ff5ce1b396" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" FourierTools = "b18b359b-aebc-45ac-a139-9c0ccbb2871e" GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" +IndexFunArrays = "613c443e-d742-454e-bfc6-1d7f8dd76566" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" InverseModeling = "ce844058-9528-415d-a63d-06f3dd08b29f" LineSearches = "d3d80556-e9d4-5f37-9878-2ab0fcc64255" diff --git a/examples/polynomial_apply.jl b/examples/polynomial_apply.jl index 93ae825..39b565d 100644 --- a/examples/polynomial_apply.jl +++ b/examples/polynomial_apply.jl @@ -11,10 +11,12 @@ using BenchmarkTools #using InverseModeling import Random using Noise +using IndexFunArrays function poly_test(;sz=64, dtype=Float64, n_photons=1000) + sz_psf = (sz, sz, 100) sampling = (0.040, 0.040, 0.050) # simulate a confocal PSF @@ -28,42 +30,67 @@ function poly_test(;sz=64, dtype=Float64, n_photons=1000) # normalizing the sample data sample_data = p_psf ./ maximum(p_psf) - - f_affine = get_function_affine(sample_data); - true_vals = dtype[10.0, 2.0, 1.1, 1.7, 0.0, 0.0, 0.0] + + #sample_data = make_grid(); + f_1 = get_function_poly(Float64.(sample_data), 1); # get_function_affine(sample_data); + true_vals = dtype[2.1, 1.05, 0.02, 1.5, 0.05, 1.02] # dtype[2.0, 1.0, 1.01, 1.0, 0.0, 0.0, 0.0] # true_vals = dtype[rand(-4.0:0.001:4.0), rand(-4.0:0.001:4.0), rand(0.5:0.001:1.5),rand(0.5:0.001:1.5), 0.0, 0.0, rand(0.001:0.001:pi/2.001)] - dat2 = f_affine(true_vals) + dat2 = f_1(Tuple(true_vals)) - p_img = dat2 # n_photons .* (dat2 ./ maximum(dat2)) - n_img = p_img#dtype.(poisson(Float64.(p_img))) + p_img = n_photons .* (dat2 ./ maximum(dat2)) + n_img = dtype.(poisson(Float64.(p_img))) - s_data = sample_data#poisson(Float64.(sample_data .* n_photons)) + s_data = Float64.(sample_data .* n_photons) #sample_data = dtype.(TestImages.shepp_logan(sz)) f = get_function_poly(Float64.(s_data), 1); loss_m(p1::AbstractVector) = sum(abs2.(f(Tuple(p1)) .- n_img)) - st_vals = [-5.0, 1.0, 0.0, 0.0, 0.0, 1.0] #ones(Float64, 6)./10 - # st_vals = Float64[2.0, 0, 0, 0, 0, 0, 1, 0, 0, 1.0, 0, 0, 1, 0, 0, 0, 0, 0] + st_vals = dtype[2.1, 1.01, 0.01, 1.5, 0.01, 0.9] #ones(Float64, 6)./10 + #st_vals = Float64[1.0, 0, 0, 0, 0, 0, 1.0, 0, 0, 1.0, 0, 0, 1.0, 0, 0, 0, 0, 0] + # Float64[9.0, 0, 0, 0, 0, 0, 1, 0, 0, 5.0, 0, 0, 1, 0, 0, 0, 0, 0] # @vv f(Tuple(st_vals)) # loss_m(st_vals) + + function g!(G, x) # (G, x) + G .= gradient(loss_m, x)[1] + end + od = OnceDifferentiable(loss_m, g!, st_vals) res = optimize( - loss_m, + od, st_vals, #Newton(), - #BFGS(; linesearch=LineSearches.BackTracking(order=3)), + BFGS(; initial_stepnorm = 1e-2),#; linesearch=LineSearches.BackTracking(order=2)), #LBFGS(),#; linesearch=LineSearches.BackTracking(order=3)), #lower, upper, #init_x, #Fminbox(inner_optimizer), - Optim.Options(store_trace = true, extended_trace = true, iterations=5000), + Optim.Options(store_trace = true, extended_trace = true, iterations=5000, g_tol=1e-3), autodiff = :forward ) # return the estimated parameters - return true_vals, Optim.minimizer(res), res + return true_vals, Optim.minimizer(res), res, f, f_1 end -poly_test(sz=64) \ No newline at end of file +#a, b, c = poly_test() +#@vt f(Tuple(b)) f_affine(a) + + +function make_grid!(arr::AbstractArray) + arr[isinteger.(xx(size(arr))./10) .|| isinteger.(yy(size(arr))./10)] .= 1.0 + return arr + +end +function make_grid(sz::NTuple{N, Int}=(64, 64)) where {N} + arr = zeros(Float64, sz) + make_grid!(arr) + return arr +end +function make_grid(::Type{T}, sz::NTuple{N, Int}=(64, 64)) where {N, T} + arr = zeros(T, sz) + make_grid!(arr) + return arr +end \ No newline at end of file From bd2c5e9552ea066e228ead389efed8bc0d4067ee Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Mon, 15 Jul 2024 12:53:51 +0200 Subject: [PATCH 29/44] updated ci.yml --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 456b8e6..ab740f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,9 +3,11 @@ on: pull_request: branches: - master + - develop push: branches: - master + - develop tags: '*' concurrency: # Skip intermediate builds: always. From 133ca00efb32aab9be0ee1313f1ee2636bf2bb41 Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Mon, 15 Jul 2024 13:04:14 +0200 Subject: [PATCH 30/44] removed undefined export --- src/polynomials.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/polynomials.jl b/src/polynomials.jl index 4b6e989..2a65819 100644 --- a/src/polynomials.jl +++ b/src/polynomials.jl @@ -1,4 +1,4 @@ -export get_polynomial, get_multi_poly, get_num_poly_vars, get_num_multipoly_vars +export get_multi_poly, get_num_poly_vars, get_num_multipoly_vars export polynomial # using Unrolled From 76b7d14b44e56d4317aca2c253cab7b062eba57f Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Mon, 15 Jul 2024 13:32:28 +0200 Subject: [PATCH 31/44] modified tutorial to the new commits --- docs/Project.toml | 4 ++++ docs/src/tutorial.jl | 12 +++++++++--- docs/src/tutorial.md | 14 +++++++++++--- examples/deform_testimage.jl | 4 ++-- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index 4401354..5e93c02 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -2,3 +2,7 @@ DataToFunctions = "64cfdffa-4d02-49ee-ae8b-a805370874f5" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +SyntheticObjects = "e7028c27-0967-45e9-8fdb-dbc10ccb2b0a" diff --git a/docs/src/tutorial.jl b/docs/src/tutorial.jl index e63f312..1188d74 100644 --- a/docs/src/tutorial.jl +++ b/docs/src/tutorial.jl @@ -1,5 +1,11 @@ # Tutorial for the DataToFunctions.jl package +import Pkg +Pkg.add("SyntheticObjects") +Pkg.add("StaticArrays") +Pkg.add("Plots") +Pkg.add("Random") + # Load the packages using DataToFunctions using SyntheticObjects @@ -42,13 +48,13 @@ heatmap(data_transformed_m, aspect_ratio=1 # Now we try to do the transformation using a polynomial function # we first define the interpolation object using the DataToFunctions package with a polynomial of order 1 -f_polynomial = get_function_polynomial(data, 1) +f_polynomial = get_function_poly(data, 1) # define the polynomial coefficients -params = [1.0, 0.0, 1.0, 0.0, 1.0, 0.0] +params = (1.0, 0.0, 1.0, 0.0, 1.0, 0.0) # apply the transformation -data_transformed_polynomial = f_polynomial(params) +data_transformed_polynomial = f_polynomial((params)) heatmap(data_transformed_polynomial, aspect_ratio=1 , title="Transformed data using a polynomial function of order 1" diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index 924e00b..bc78b45 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -4,6 +4,14 @@ EditURL = "tutorial.jl" Tutorial for the DataToFunctions.jl package +````@example tutorial +import Pkg +Pkg.add("SyntheticObjects") +Pkg.add("StaticArrays") +Pkg.add("Plots") +Pkg.add("Random") +```` + Load the packages ````@example tutorial @@ -65,19 +73,19 @@ Now we try to do the transformation using a polynomial function we first define the interpolation object using the DataToFunctions package with a polynomial of order 1 ````@example tutorial -f_polynomial = get_function_polynomial(data, 1) +f_polynomial = get_function_poly(data, 1) ```` define the polynomial coefficients ````@example tutorial -params = [1.0, 0.0, 1.0, 0.0, 1.0, 0.0] +params = (1.0, 0.0, 1.0, 0.0, 1.0, 0.0) ```` apply the transformation ````@example tutorial -data_transformed_polynomial = f_polynomial(params) +data_transformed_polynomial = f_polynomial((params)) heatmap(data_transformed_polynomial, aspect_ratio=1 , title="Transformed data using a polynomial function of order 1" diff --git a/examples/deform_testimage.jl b/examples/deform_testimage.jl index 2b46205..8408536 100644 --- a/examples/deform_testimage.jl +++ b/examples/deform_testimage.jl @@ -1,5 +1,5 @@ ### A Pluto.jl notebook ### -# v0.19.43 +# v0.19.42 using Markdown using InteractiveUtils @@ -87,7 +87,7 @@ h = get_function_poly(data, 1) @bind c6 Slider(-2f0:0.05f0:2f0, default=1) # ╔═╡ 74228a9d-6cc2-4aaf-97da-67f32670341e -Gray.(h((c1,c2,c3,c4,c5,c6,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0))) +Gray.(h((0f0, 0f0, 1.0f0, 0f0, 1.00f0, 0f0)))#,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0))) # ╔═╡ 687a198b-9020-4959-8e27-fd0896d4b1fc maximum(h((c1,c2,c3,c4,c5,c6,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0))) From 85efbbcc5d612be9fb629543dc3bfd0f6dd0cae7 Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Mon, 15 Jul 2024 13:40:54 +0200 Subject: [PATCH 32/44] updated tutorial --- docs/src/tutorial.jl | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/docs/src/tutorial.jl b/docs/src/tutorial.jl index 1188d74..2dac15b 100644 --- a/docs/src/tutorial.jl +++ b/docs/src/tutorial.jl @@ -48,16 +48,3 @@ heatmap(data_transformed_m, aspect_ratio=1 # Now we try to do the transformation using a polynomial function # we first define the interpolation object using the DataToFunctions package with a polynomial of order 1 -f_polynomial = get_function_poly(data, 1) - -# define the polynomial coefficients -params = (1.0, 0.0, 1.0, 0.0, 1.0, 0.0) - -# apply the transformation -data_transformed_polynomial = f_polynomial((params)) - -heatmap(data_transformed_polynomial, aspect_ratio=1 - , title="Transformed data using a polynomial function of order 1" - , titlefontsize=10, size=(500, 500) - , xlabel="X", ylabel="Y") - \ No newline at end of file From b8d6c7df7724604d0f06ccf6a3fa37ef684d0264 Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Mon, 15 Jul 2024 13:55:19 +0200 Subject: [PATCH 33/44] modded ci.yml --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab740f3..59c7b92 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,11 +2,11 @@ name: CI on: pull_request: branches: - - master + - main - develop push: branches: - - master + - main - develop tags: '*' concurrency: From fc8a3f974bd212d6af1c4e5aae3125baf630d2f7 Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Mon, 15 Jul 2024 14:03:39 +0200 Subject: [PATCH 34/44] added eevbranch --- docs/make.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/make.jl b/docs/make.jl index 4fd18e0..f3f5377 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -20,4 +20,4 @@ pages = Any[ # compile to HTML: makedocs(; sitename="DataToFunctions.jl", pages, modules = [DataToFunctions], warnonly = true) -deploydocs(repo = "github.com/RainerHeintzmann/DataToFunctions.jl.git") \ No newline at end of file +deploydocs(repo = "github.com/RainerHeintzmann/DataToFunctions.jl.git", devbranch = "develop") \ No newline at end of file From a6e361562f08c37ef8f3ebfed9d67d60d1e1608e Mon Sep 17 00:00:00 2001 From: RainerHeintzmann Date: Mon, 15 Jul 2024 16:18:10 +0200 Subject: [PATCH 35/44] better Pluto sliders --- examples/deform_testimage.jl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/deform_testimage.jl b/examples/deform_testimage.jl index 8408536..d313c8b 100644 --- a/examples/deform_testimage.jl +++ b/examples/deform_testimage.jl @@ -1,5 +1,5 @@ ### A Pluto.jl notebook ### -# v0.19.42 +# v0.19.43 using Markdown using InteractiveUtils @@ -72,25 +72,25 @@ h = get_function_poly(data, 1) @bind c1 Slider(-1f0:0.05f0:1f0, default=0) # ╔═╡ 7fb3ef17-d0a0-4919-b4d5-8e66b4a0fe60 -@bind c2 Slider(-2f0:0.05f0:2f0, default=1) +@bind c2 Slider(0.2f0:0.05f0:2f0, default=1) # ╔═╡ 200bab4d-b444-47c6-b4d8-d5d3c450e5f9 -@bind c3 Slider(-2f0:0.05f0:2f0, default=1) +@bind c3 Slider(-2f0:0.05f0:2f0, default=0) # ╔═╡ f17402a6-ba63-44e9-8e22-da027b07ffc3 -@bind c4 Slider(-2f0:0.05f0:2f0, default=1) +@bind c4 Slider(-20f0:0.05f0:20f0, default=0) # ╔═╡ d913278e-1658-4bee-9e12-ad778e530c1b -@bind c5 Slider(-2f0:0.05f0:2f0, default=1) +@bind c5 Slider(-2f0:0.05f0:2f0, default=0) # ╔═╡ cee301da-2b3e-432f-9551-0fe83bf6c8ec -@bind c6 Slider(-2f0:0.05f0:2f0, default=1) +@bind c6 Slider(0.2f0:0.05f0:2f0, default=1) # ╔═╡ 74228a9d-6cc2-4aaf-97da-67f32670341e -Gray.(h((0f0, 0f0, 1.0f0, 0f0, 1.00f0, 0f0)))#,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0))) +Gray.(h((c1, c2, c3, c4, c5, c6)))#,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0))) # ╔═╡ 687a198b-9020-4959-8e27-fd0896d4b1fc -maximum(h((c1,c2,c3,c4,c5,c6,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0))) +maximum(h((c1,c2,c3,c4,c5,c6))) # ╔═╡ Cell order: # ╠═28975586-853e-4e19-b9eb-65c41fa61a43 From 47d9a0bdc8405d03c5ef58114b2071090654b6c9 Mon Sep 17 00:00:00 2001 From: RainerHeintzmann Date: Mon, 15 Jul 2024 19:33:58 +0200 Subject: [PATCH 36/44] better Pluto example --- examples/deform_testimage.jl | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/deform_testimage.jl b/examples/deform_testimage.jl index d313c8b..d2ac90f 100644 --- a/examples/deform_testimage.jl +++ b/examples/deform_testimage.jl @@ -27,10 +27,10 @@ using DataToFunctions, ImageShow, TestImages, PlutoUI, Images # Pkg.add("PlutoUI") # ╔═╡ 5ac1123d-5df3-4c9d-aff1-ffe91d931497 -data = testimage("resolution_test_512") +data = Float32.(testimage("resolution_test_512")) # ╔═╡ b0c8d15e-1bd3-4e66-b2b9-57885636eb48 - +simshow(data) # ╔═╡ 2fce0208-732f-4259-a47d-7f78921bfd87 f = get_function(data, super_sampling=1) @@ -69,7 +69,7 @@ g() h = get_function_poly(data, 1) # ╔═╡ 473d635e-d06d-4cdb-991f-22c82a06b491 -@bind c1 Slider(-1f0:0.05f0:1f0, default=0) +@bind c1 Slider(-20f0:0.05f0:20f0, default=0) # ╔═╡ 7fb3ef17-d0a0-4919-b4d5-8e66b4a0fe60 @bind c2 Slider(0.2f0:0.05f0:2f0, default=1) @@ -87,11 +87,14 @@ h = get_function_poly(data, 1) @bind c6 Slider(0.2f0:0.05f0:2f0, default=1) # ╔═╡ 74228a9d-6cc2-4aaf-97da-67f32670341e -Gray.(h((c1, c2, c3, c4, c5, c6)))#,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0))) +simshow(h((c1, c2, c3, c4, c5, c6)), cmap=:turbo)#,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0))) # ╔═╡ 687a198b-9020-4959-8e27-fd0896d4b1fc maximum(h((c1,c2,c3,c4,c5,c6))) +# ╔═╡ 6584aacc-440e-457b-bf52-83f8db40c999 +h((c1,c2,c3,c4,c5,c6)) + # ╔═╡ Cell order: # ╠═28975586-853e-4e19-b9eb-65c41fa61a43 # ╠═0ae2da4f-3f75-47bb-a899-9e89c5c3f17c @@ -118,3 +121,4 @@ maximum(h((c1,c2,c3,c4,c5,c6))) # ╠═cee301da-2b3e-432f-9551-0fe83bf6c8ec # ╠═74228a9d-6cc2-4aaf-97da-67f32670341e # ╠═687a198b-9020-4959-8e27-fd0896d4b1fc +# ╠═6584aacc-440e-457b-bf52-83f8db40c999 From 6acc73e14e710ad776b9f2db94b796a50426e119 Mon Sep 17 00:00:00 2001 From: RainerHeintzmann Date: Tue, 16 Jul 2024 15:12:44 +0200 Subject: [PATCH 37/44] Documenter and DocumenterTools --- docs/Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Project.toml b/docs/Project.toml index 5e93c02..efdfaf2 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,6 +1,7 @@ [deps] DataToFunctions = "64cfdffa-4d02-49ee-ae8b-a805370874f5" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +DocumenterTools = "35a29f4d-8980-5a13-9543-d66fff28ecb8" Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" From bc04932e793a8c35f341910b8b8c19c17812f030 Mon Sep 17 00:00:00 2001 From: RainerHeintzmann Date: Mon, 22 Jul 2024 17:37:18 +0200 Subject: [PATCH 38/44] added the @generated tag to allow arbitrary dimensions --- src/polynomials.jl | 104 +++++++++++++++++++++++++++------------------ 1 file changed, 62 insertions(+), 42 deletions(-) diff --git a/src/polynomials.jl b/src/polynomials.jl index 2a65819..969dc3f 100644 --- a/src/polynomials.jl +++ b/src/polynomials.jl @@ -1,8 +1,6 @@ export get_multi_poly, get_num_poly_vars, get_num_multipoly_vars export polynomial -# using Unrolled - """ polynomial(::Val{0}, ::T1, c::T2, ::Val{cstart}=Val(1), ::Val{numvars}=Val(1)) where {NV, TS, T1 <: NTuple{NV, Integer}, T2 <: NTuple{TS, Float32}, cstart, numvars} @@ -41,45 +39,67 @@ Example: 0.000089 seconds (3 allocations: 184 bytes) ``` """ -function polynomial(::Val{N}, t::T1, c::T2, ::Val{cstart}=Val(1), ::Val{numvars}=Val(length(t)))::Float32 where {N, NV, TS, T1 <: NTuple{NV, Integer}, T2 <: NTuple{TS, Float32}, cstart, numvars} - c_start = cstart - res = c[c_start] - c_start += 1 - # iterate through the polynomial variables: (but this leads to dynamic memory allocation!) - # for n = 1:numvars # eachindex(t) # 1:length(t) # eachindex(t) # 1:length(t) - # # c_end = c_start + get_num_poly_vars(Val(n), Val(N-1)) - 1 - # res += t[n] * polynomial(Val(N-1), t, c, Val(c_start), Val(n)) - # c_start += get_num_poly_vars(Val(n), Val(N-1)) # c_end + 1 - # end - # # does not work: - # @macroexpand Base.Cartesian.@nexprs 4 n -> begin - # if (numvars >= n) - # res += t[n] * polynomial(Val(N-1), t, c, Val(c_start), Val(n)) - # c_start += get_num_poly_vars(Val(n), Val(N-1)) # c_end + 1 - # end - # end - - # this simply unrolls the loop by hand (up to 4D input variables): - if numvars >= 1 - res += t[1] * polynomial(Val(N-1), t, c, Val(c_start), Val(1)) - c_start += get_num_poly_vars(Val(1), Val(N-1)) # c_end + 1 - end - if numvars >= 2 - res += t[2] * polynomial(Val(N-1), t, c, Val(c_start), Val(2)) - c_start += get_num_poly_vars(Val(2), Val(N-1)) # c_end + 1 - end - if numvars >= 3 - res += t[3] * polynomial(Val(N-1), t, c, Val(c_start), Val(3)) - c_start += get_num_poly_vars(Val(3), Val(N-1)) # c_end + 1 - end - if numvars >= 4 - res += t[4] * polynomial(Val(N-1), t, c, Val(c_start), Val(4)) - c_start += get_num_poly_vars(Val(4), Val(N-1)) # c_end + 1 - end - if numvars >= 5 - error("Only up to 4 dimensions are currently supported for polynomials") +@generated function polynomial(::Val{N}, t::T1, c::T2, ::Val{cstart}=Val(1), ::Val{numvars}=Val(length(t)))::Float32 where {N, NV, TS, T1 <: NTuple{NV, Integer}, T2 <: NTuple{TS, Float32}, cstart, numvars} + quote + c_start = cstart + res = c[c_start] + c_start += 1 + # iterate through the polynomial variables: (but this leads to dynamic memory allocation!) + # for n = 1:numvars # eachindex(t) # 1:length(t) # eachindex(t) # 1:length(t) + # # c_end = c_start + get_num_poly_vars(Val(n), Val(N-1)) - 1 + # res += t[n] * polynomial(Val(N-1), t, c, Val(c_start), Val(n)) + # c_start += get_num_poly_vars(Val(n), Val(N-1)) # c_end + 1 + # end + # # does not work: + # @macroexpand Base.Cartesian.@nexprs 4 n -> begin + # if (numvars >= n) + # res += t[n] * polynomial(Val(N-1), t, c, Val(c_start), Val(n)) + # c_start += get_num_poly_vars(Val(n), Val(N-1)) # c_end + 1 + # end + # end + + # @generated function myvarsum(t, c, res, c_start, ::Val{numvars}) where {N} + # quote + # c_start = cstart + # res = c[c_start] + # c_start += 1 + # Base.Cartesian.@nexprs $N i A n -> begin + # res += t[n] * polynomial(Val(N-1), t, c, Val(c_start), Val(n)) + # c_start += get_num_poly_vars(Val(n), Val(N-1)) # c_end + 1 + # end + # res, c_start + # end + # end + + Base.Cartesian.@nexprs $numvars n -> begin + res += t[n] * polynomial(Val(N-1), t, c, Val(c_start), Val(n)) + c_start += get_num_poly_vars(Val(n), Val(N-1)) # c_end + 1 + end + # if numvars > 5 + # error("Only up to 5 dimensions are currently supported for polynomials") + # end + # this simply unrolls the loop by hand (up to 4D input variables): + # if numvars >= 1 + # res += t[1] * polynomial(Val(N-1), t, c, Val(c_start), Val(1)) + # c_start += get_num_poly_vars(Val(1), Val(N-1)) # c_end + 1 + # end + # if numvars >= 2 + # res += t[2] * polynomial(Val(N-1), t, c, Val(c_start), Val(2)) + # c_start += get_num_poly_vars(Val(2), Val(N-1)) # c_end + 1 + # end + # if numvars >= 3 + # res += t[3] * polynomial(Val(N-1), t, c, Val(c_start), Val(3)) + # c_start += get_num_poly_vars(Val(3), Val(N-1)) # c_end + 1 + # end + # if numvars >= 4 + # res += t[4] * polynomial(Val(N-1), t, c, Val(c_start), Val(4)) + # c_start += get_num_poly_vars(Val(4), Val(N-1)) # c_end + 1 + # end + # if numvars >= 5 + # error("Only up to 4 dimensions are currently supported for polynomials") + # end + return res end - return res end @@ -125,7 +145,7 @@ function test_poly_allocations() # p = get_polynomial(Val(2), Val(3)) # @time p.(Tuple.(CartesianIndices((200,200))),Ref((1.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))); - polynomial(Val(2), (2f0, 20f0), (1f0, 1f0, 1f0, 1f0, 1f0, 1f0)) == 467 + polynomial(Val(2), (2, 20), (1f0, 1f0, 1f0, 1f0, 1f0, 1f0)) == 467 cids = Tuple.(CartesianIndices((200,200))) # cfds = map((t)->Tuple(Float32.([t...])), cids) @@ -136,7 +156,7 @@ function test_poly_allocations() # 0.000122 seconds (3 allocations: 168 bytes) get_num_poly_vars(Val(3), Val(2)) # 10 indices required - @time res .= polynomial.(Ref(Val(3)), cids, Ref(cs)); # 2 orders, two variables + @time res .= polynomial.(Ref(Val(3)), cids, Ref(cs)); # 3 orders, two variables # 0.003710 seconds (240.00 k allocations: 13.428 MiB) get_num_poly_vars(Val(4), Val(2)) # 15 indices required From 4e117fb99beb6c3440a102f5fb8575b71113a167 Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Tue, 23 Jul 2024 13:26:18 +0200 Subject: [PATCH 39/44] modified to use EvalMultiPoly.jl --- Project.toml | 1 + examples/Project.toml | 2 + examples/deform_testimage.jl | 96 ++++++++++--------- examples/polynomial_apply.jl | 25 +++-- src/DataToFunctions.jl | 3 +- src/polynomials.jl | 173 ----------------------------------- src/transformation_types.jl | 4 + src/transformators.jl | 83 ++++++++++++++++- src/utils.jl | 18 ---- 9 files changed, 151 insertions(+), 254 deletions(-) delete mode 100644 src/polynomials.jl create mode 100644 src/transformation_types.jl delete mode 100644 src/utils.jl diff --git a/Project.toml b/Project.toml index 9f7abd6..eb883bc 100644 --- a/Project.toml +++ b/Project.toml @@ -4,6 +4,7 @@ authors = ["RainerHeintzmann "] version = "0.1.0" [deps] +EvalMultiPoly = "c78649ec-f9ed-405e-be6d-0472f43586aa" FourierTools = "b18b359b-aebc-45ac-a139-9c0ccbb2871e" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" diff --git a/examples/Project.toml b/examples/Project.toml index a25d76a..5a2e1dd 100644 --- a/examples/Project.toml +++ b/examples/Project.toml @@ -4,6 +4,8 @@ CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" CoordinateTransformations = "150eb455-5306-5404-9cee-2592286d6298" DataToFunctions = "64cfdffa-4d02-49ee-ae8b-a805370874f5" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" +EvalMultiPoly = "c78649ec-f9ed-405e-be6d-0472f43586aa" FindShift = "643ec891-bf64-479f-8088-26ff5ce1b396" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" FourierTools = "b18b359b-aebc-45ac-a139-9c0ccbb2871e" diff --git a/examples/deform_testimage.jl b/examples/deform_testimage.jl index d2ac90f..274c0ae 100644 --- a/examples/deform_testimage.jl +++ b/examples/deform_testimage.jl @@ -1,5 +1,5 @@ ### A Pluto.jl notebook ### -# v0.19.43 +# v0.19.42 using Markdown using InteractiveUtils @@ -20,11 +20,14 @@ using Pkg # ╔═╡ 0ae2da4f-3f75-47bb-a899-9e89c5c3f17c Pkg.activate(".") +# ╔═╡ 4af0c13d-fc42-4fe7-97e6-2248e36b63e2 +Pkg.add("PlutoUI"); + # ╔═╡ a2d75cfb-feab-4130-8439-30c543618d04 using DataToFunctions, ImageShow, TestImages, PlutoUI, Images -# ╔═╡ 4af0c13d-fc42-4fe7-97e6-2248e36b63e2 -# Pkg.add("PlutoUI") +# ╔═╡ 1c744bec-f085-4812-ab1a-40a32c2ac176 +import PlutoUI: combine # ╔═╡ 5ac1123d-5df3-4c9d-aff1-ffe91d931497 data = Float32.(testimage("resolution_test_512")) @@ -33,7 +36,7 @@ data = Float32.(testimage("resolution_test_512")) simshow(data) # ╔═╡ 2fce0208-732f-4259-a47d-7f78921bfd87 -f = get_function(data, super_sampling=1) +f = get_interpolated_function(data, AffineMode, super_sampling=1) # ╔═╡ dd535378-3f2a-4429-914c-8b7608b99706 @bind shift_x Slider(-100:0.02:100, default=0) @@ -48,57 +51,68 @@ f = get_function(data, super_sampling=1) @bind zoom_y Slider(0.2:0.02:4, default=1) # ╔═╡ 64b64d3b-092f-4553-916d-f7db1fdfa428 -f((shift_x, shift_y), (1/zoom_x, 1/zoom_y)) +simshow(f((shift_x, shift_y, 1/zoom_x, 1/zoom_y, 0.0, 0.0, 0.0))) # ╔═╡ c3fe6b25-f4c0-4a80-9d15-9ee30136d43b -typeof(f((shift_x, shift_y), (1/zoom_x, 1/zoom_y))) +typeof(f((shift_x, shift_y, 1/zoom_x, 1/zoom_y, 0.0, 0.0, 0.0))) # ╔═╡ ff143f0d-b070-4220-8fa6-0b5a93a56303 typeof(data) -# ╔═╡ 1bed34fb-b29a-4042-a493-4835fdb69a9d -g =DataToFunctions.get_function_affine(data, super_sampling=1) +# ╔═╡ 6676ba35-3efd-49f5-9819-411ad8f8a95c +md""" +# Polynomial transformations +""" -# ╔═╡ c287fa80-426b-11ef-125e-5fda207e605c -# ╠═╡ disabled = true -#=╠═╡ -g() - ╠═╡ =# - -# ╔═╡ 87be45c0-8b2e-4d49-abd1-a274b3c1815e -h = get_function_poly(data, 1) +# ╔═╡ 13461a95-95ea-4bad-8673-e94e06776254 +md""" +First order polynomial transformation which is as follows: -# ╔═╡ 473d635e-d06d-4cdb-991f-22c82a06b491 -@bind c1 Slider(-20f0:0.05f0:20f0, default=0) +``x^{\prime} = c_{1} + c_{2}{x}^{1} + c_{3}{y}^{1}`` -# ╔═╡ 7fb3ef17-d0a0-4919-b4d5-8e66b4a0fe60 -@bind c2 Slider(0.2f0:0.05f0:2f0, default=1) +``y^{\prime} = c_{4} + c_{5}{x}^{1} + c_{6}{y}^{1}`` +""" -# ╔═╡ 200bab4d-b444-47c6-b4d8-d5d3c450e5f9 -@bind c3 Slider(-2f0:0.05f0:2f0, default=0) - -# ╔═╡ f17402a6-ba63-44e9-8e22-da027b07ffc3 -@bind c4 Slider(-20f0:0.05f0:20f0, default=0) - -# ╔═╡ d913278e-1658-4bee-9e12-ad778e530c1b -@bind c5 Slider(-2f0:0.05f0:2f0, default=0) - -# ╔═╡ cee301da-2b3e-432f-9551-0fe83bf6c8ec -@bind c6 Slider(0.2f0:0.05f0:2f0, default=1) +# ╔═╡ 87be45c0-8b2e-4d49-abd1-a274b3c1815e +h = get_interpolated_function(data, PolynomialMode, 1); + +# ╔═╡ 68f771aa-2cde-41cd-990c-9ec7dc2146a4 +function coeffs_input(coeffs::Vector) + + return combine() do Child + + inputs = [ + md""" $(name): $( + Child(name, Slider(-2f0:0.05f0:2f0, default=0, show_value=true)) + )""" + + for name in coeffs + ] + + md""" + #### Transfrorm coefficients + $(inputs) + """ + end +end; + +# ╔═╡ 69331d73-75a3-4727-acda-e79779a2bd03 +@bind c coeffs_input(["c1", "c2", "c3", "c4", "c5", "c6"]) # ╔═╡ 74228a9d-6cc2-4aaf-97da-67f32670341e -simshow(h((c1, c2, c3, c4, c5, c6)), cmap=:turbo)#,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0))) +simshow(h((c.c1, c.c2, c.c3, c.c4, c.c5, c.c6)), cmap=:turbo)#,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0))) # ╔═╡ 687a198b-9020-4959-8e27-fd0896d4b1fc -maximum(h((c1,c2,c3,c4,c5,c6))) +maximum(h((c.c1,c.c2,c.c3,c.c4,c.c5,c.c6))) # ╔═╡ 6584aacc-440e-457b-bf52-83f8db40c999 -h((c1,c2,c3,c4,c5,c6)) +h((c.c1,c.c2,c.c3,c.c4,c.c5,c.c6)) # ╔═╡ Cell order: # ╠═28975586-853e-4e19-b9eb-65c41fa61a43 # ╠═0ae2da4f-3f75-47bb-a899-9e89c5c3f17c # ╠═4af0c13d-fc42-4fe7-97e6-2248e36b63e2 +# ╠═1c744bec-f085-4812-ab1a-40a32c2ac176 # ╠═a2d75cfb-feab-4130-8439-30c543618d04 # ╠═5ac1123d-5df3-4c9d-aff1-ffe91d931497 # ╠═b0c8d15e-1bd3-4e66-b2b9-57885636eb48 @@ -110,15 +124,11 @@ h((c1,c2,c3,c4,c5,c6)) # ╠═64b64d3b-092f-4553-916d-f7db1fdfa428 # ╠═c3fe6b25-f4c0-4a80-9d15-9ee30136d43b # ╠═ff143f0d-b070-4220-8fa6-0b5a93a56303 -# ╠═1bed34fb-b29a-4042-a493-4835fdb69a9d -# ╠═c287fa80-426b-11ef-125e-5fda207e605c +# ╟─6676ba35-3efd-49f5-9819-411ad8f8a95c +# ╟─13461a95-95ea-4bad-8673-e94e06776254 # ╠═87be45c0-8b2e-4d49-abd1-a274b3c1815e -# ╠═473d635e-d06d-4cdb-991f-22c82a06b491 -# ╠═7fb3ef17-d0a0-4919-b4d5-8e66b4a0fe60 -# ╠═200bab4d-b444-47c6-b4d8-d5d3c450e5f9 -# ╠═f17402a6-ba63-44e9-8e22-da027b07ffc3 -# ╠═d913278e-1658-4bee-9e12-ad778e530c1b -# ╠═cee301da-2b3e-432f-9551-0fe83bf6c8ec -# ╠═74228a9d-6cc2-4aaf-97da-67f32670341e +# ╟─69331d73-75a3-4727-acda-e79779a2bd03 +# ╟─68f771aa-2cde-41cd-990c-9ec7dc2146a4 +# ╟─74228a9d-6cc2-4aaf-97da-67f32670341e # ╠═687a198b-9020-4959-8e27-fd0896d4b1fc # ╠═6584aacc-440e-457b-bf52-83f8db40c999 diff --git a/examples/polynomial_apply.jl b/examples/polynomial_apply.jl index 39b565d..501502b 100644 --- a/examples/polynomial_apply.jl +++ b/examples/polynomial_apply.jl @@ -29,25 +29,22 @@ function poly_test(;sz=64, dtype=Float64, n_photons=1000) #sample_data = p_psf ./ maximum(p_psf) # normalizing the sample data - sample_data = p_psf ./ maximum(p_psf) + sample_data = p_psf ./ maximum(p_psf) * n_photons #sample_data = make_grid(); - f_1 = get_function_poly(Float64.(sample_data), 1); # get_function_affine(sample_data); - true_vals = dtype[2.1, 1.05, 0.02, 1.5, 0.05, 1.02] # dtype[2.0, 1.0, 1.01, 1.0, 0.0, 0.0, 0.0] + f_1 = get_interpolated_function(Float64.(sample_data), PolynomialMode, 1); # get_function_affine(sample_data); + true_vals = (1.6, 1.05, 0.1, 1.5, 0.01, 1.02) # dtype[2.0, 1.0, 1.01, 1.0, 0.0, 0.0, 0.0] # true_vals = dtype[rand(-4.0:0.001:4.0), rand(-4.0:0.001:4.0), rand(0.5:0.001:1.5),rand(0.5:0.001:1.5), 0.0, 0.0, rand(0.001:0.001:pi/2.001)] - dat2 = f_1(Tuple(true_vals)) - p_img = n_photons .* (dat2 ./ maximum(dat2)) - n_img = dtype.(poisson(Float64.(p_img))) + dat2 = f_1(true_vals) - s_data = Float64.(sample_data .* n_photons) #sample_data = dtype.(TestImages.shepp_logan(sz)) - f = get_function_poly(Float64.(s_data), 1); + f = get_interpolated_function(Float64.(sample_data), PolynomialMode, 1); - loss_m(p1::AbstractVector) = sum(abs2.(f(Tuple(p1)) .- n_img)) - st_vals = dtype[2.1, 1.01, 0.01, 1.5, 0.01, 0.9] #ones(Float64, 6)./10 + loss_p(p1::AbstractArray) = (sum(abs2.(f(Tuple(p1)) .- dat2))) + st_vals = [2.1, 1.00, 0.00, 1.5, 0.00, 1.0] #ones(Float64, 6)./10 #st_vals = Float64[1.0, 0, 0, 0, 0, 0, 1.0, 0, 0, 1.0, 0, 0, 1.0, 0, 0, 0, 0, 0] # Float64[9.0, 0, 0, 0, 0, 0, 1, 0, 0, 5.0, 0, 0, 1, 0, 0, 0, 0, 0] # @vv f(Tuple(st_vals)) @@ -55,11 +52,11 @@ function poly_test(;sz=64, dtype=Float64, n_photons=1000) function g!(G, x) # (G, x) - G .= gradient(loss_m, x)[1] + G .= gradient(loss_p, x)[1] end - od = OnceDifferentiable(loss_m, g!, st_vals) + od = OnceDifferentiable(loss_p, g!, st_vals) res = optimize( - od, + loss_p, st_vals, #Newton(), BFGS(; initial_stepnorm = 1e-2),#; linesearch=LineSearches.BackTracking(order=2)), @@ -67,7 +64,7 @@ function poly_test(;sz=64, dtype=Float64, n_photons=1000) #lower, upper, #init_x, #Fminbox(inner_optimizer), - Optim.Options(store_trace = true, extended_trace = true, iterations=5000, g_tol=1e-3), + #Optim.Options(store_trace = true, extended_trace = true, iterations=5000, g_tol=1e-3), autodiff = :forward ) diff --git a/src/DataToFunctions.jl b/src/DataToFunctions.jl index 93857cd..77019a6 100644 --- a/src/DataToFunctions.jl +++ b/src/DataToFunctions.jl @@ -1,7 +1,6 @@ module DataToFunctions -include("utils.jl") -include("polynomials.jl") +include("transformation_types.jl") include("transformators.jl") end # module DataToFunctions \ No newline at end of file diff --git a/src/polynomials.jl b/src/polynomials.jl deleted file mode 100644 index 969dc3f..0000000 --- a/src/polynomials.jl +++ /dev/null @@ -1,173 +0,0 @@ -export get_multi_poly, get_num_poly_vars, get_num_multipoly_vars -export polynomial - -""" - polynomial(::Val{0}, ::T1, c::T2, ::Val{cstart}=Val(1), ::Val{numvars}=Val(1)) where {NV, TS, T1 <: NTuple{NV, Integer}, T2 <: NTuple{TS, Float32}, cstart, numvars} - -Create a polynomial of order 0 with numvars variables. - -returned is a function that takes a tuple of variables and a tuple of coefficients and returns the value of the polynomial - and the number of coefficients required (here 1). -""" -function polynomial(::Val{0}, ::T1, c::T2, ::Val{cstart}=Val(1), ::Val{numvars}=Val(1)) where {NV, TS, T1 <: NTuple{NV, Integer}, T2 <: NTuple{TS, Float32}, cstart, numvars} - # println("c: $(c) $(length(c))"); - return c[cstart] -end #, (t,c) -> ntuple(n->c[1], Val(numvars)) - -""" - polynomial(::Val{N}, t::T1, c::T2, ::Val{cstart}=Val(1), ::Val{numvars}=Val(length(t)))::Float32 where {N, NV, TS, T1 <: NTuple{NV, Integer}, T2 <: NTuple{TS, Float32}, cstart, numvars} - -Represents a polynomial of order N with numvars variables (also implicitely defined via the length of the NTuple `t`). -Note that `numvars` is needed for the internal workings of the polynomial generator, but notmally not by the user. - -E.g. to represent a polynomial of order 1 with 2 variables, the coefficients are ordered as follows: -c = (c0, c1, c2) where the polynomial is: c0 + c1*x + c2*y -or for a polynomial of order 2 with 2 variables: -c = (c0, c1, c2, c3, c4, c5) where the polynomial is c0 + c1*x + c2*x^2 + c3*y + c4*x*y + c5*y^2 -Note that the coefficients are ordered not by the multiples in which they appear in the polynomial, but by the order of the variables. - -Example: -```jldoctest ->julia polynomial(Val(2), (2, 20), (1f0, 1f0, 1f0, 1f0, 1f0, 1f0)) -467.0f0 ->julia cs = Tuple(Float32.(collect(1:27))) - (1.0f0, 2.0f0, 3.0f0, 4.0f0, 5.0f0, 6.0f0, 7.0f0, 8.0f0, 9.0f0, 10.0f0, 11.0f0, 12.0f0, 13.0f0, 14.0f0, 15.0f0, 16.0f0, 17.0f0, 18.0f0, 19.0f0, 20.0f0, 21.0f0, 22.0f0, 23.0f0, 24.0f0, 25.0f0, 26.0f0, 27.0f0) ->julia res = zeros(Float32, 200,200) ->julia @time res .= polynomial.(Ref(Val(2)), Tuple.(CartesianIndices((200,200))), Ref(cs)); - 0.054017 seconds (147.45 k allocations: 10.168 MiB, 99.75% compilation time) ->julia @time res .= polynomial.(Ref(Val(2)), Tuple.(CartesianIndices((200,200))), Ref(cs)); - 0.000089 seconds (3 allocations: 184 bytes) -``` -""" -@generated function polynomial(::Val{N}, t::T1, c::T2, ::Val{cstart}=Val(1), ::Val{numvars}=Val(length(t)))::Float32 where {N, NV, TS, T1 <: NTuple{NV, Integer}, T2 <: NTuple{TS, Float32}, cstart, numvars} - quote - c_start = cstart - res = c[c_start] - c_start += 1 - # iterate through the polynomial variables: (but this leads to dynamic memory allocation!) - # for n = 1:numvars # eachindex(t) # 1:length(t) # eachindex(t) # 1:length(t) - # # c_end = c_start + get_num_poly_vars(Val(n), Val(N-1)) - 1 - # res += t[n] * polynomial(Val(N-1), t, c, Val(c_start), Val(n)) - # c_start += get_num_poly_vars(Val(n), Val(N-1)) # c_end + 1 - # end - # # does not work: - # @macroexpand Base.Cartesian.@nexprs 4 n -> begin - # if (numvars >= n) - # res += t[n] * polynomial(Val(N-1), t, c, Val(c_start), Val(n)) - # c_start += get_num_poly_vars(Val(n), Val(N-1)) # c_end + 1 - # end - # end - - # @generated function myvarsum(t, c, res, c_start, ::Val{numvars}) where {N} - # quote - # c_start = cstart - # res = c[c_start] - # c_start += 1 - # Base.Cartesian.@nexprs $N i A n -> begin - # res += t[n] * polynomial(Val(N-1), t, c, Val(c_start), Val(n)) - # c_start += get_num_poly_vars(Val(n), Val(N-1)) # c_end + 1 - # end - # res, c_start - # end - # end - - Base.Cartesian.@nexprs $numvars n -> begin - res += t[n] * polynomial(Val(N-1), t, c, Val(c_start), Val(n)) - c_start += get_num_poly_vars(Val(n), Val(N-1)) # c_end + 1 - end - # if numvars > 5 - # error("Only up to 5 dimensions are currently supported for polynomials") - # end - # this simply unrolls the loop by hand (up to 4D input variables): - # if numvars >= 1 - # res += t[1] * polynomial(Val(N-1), t, c, Val(c_start), Val(1)) - # c_start += get_num_poly_vars(Val(1), Val(N-1)) # c_end + 1 - # end - # if numvars >= 2 - # res += t[2] * polynomial(Val(N-1), t, c, Val(c_start), Val(2)) - # c_start += get_num_poly_vars(Val(2), Val(N-1)) # c_end + 1 - # end - # if numvars >= 3 - # res += t[3] * polynomial(Val(N-1), t, c, Val(c_start), Val(3)) - # c_start += get_num_poly_vars(Val(3), Val(N-1)) # c_end + 1 - # end - # if numvars >= 4 - # res += t[4] * polynomial(Val(N-1), t, c, Val(c_start), Val(4)) - # c_start += get_num_poly_vars(Val(4), Val(N-1)) # c_end + 1 - # end - # if numvars >= 5 - # error("Only up to 4 dimensions are currently supported for polynomials") - # end - return res - end -end - - -# function unroll_loop(res::Float32, ::Var{0}, ::Var{numvars}) where{n} -# return 0f0; -# end - -# function unroll_loop(res::Float32, t, ::Var{c_start},::Var({n}, ::Var{numvars}) where{c_start, n, numvars} -# res += t[n] * polynomial(Val(N-1), t, c, Val(c_start), Val(n)) -# c_start += get_num_poly_vars(Val(n), Val(N-1)) # c_end + 1 -# return unroll_loop(res::Float32, ::Var({n}, ::Var{numvars}) + -# end - - -function get_multi_poly(::Val{numvars}, ::Val{N}) where {numvars, N} - # cs_per_comp = ((numvars+1)^N) - @info "Creating polynomials with $(numvars) variables of order , $(N). Required constants: $(numvars*((numvars+1)^N))" - p = (t,c) -> polynomial(Val(N), Tuple.(t), c) - # return p - function mpol(t,c)#::NTuple{numvars, T} where T - return ntuple(n->p(t, split_tuple(c, Val(numvars))[n]), Val(numvars)) - - # println("N: $(N), c: $(c) $(length(c))"); - # return Tuple(p(t, c[1+(n-1)*((numvars+1)^N):n*((numvars+1)^N)]) for n=1:numvars) - # return ntuple(n->p(t, c[1+(n-1)*cs_per_comp:n*cs_per_comp]), Val(numvars)) - # return ntuple(n->p(t, c[1+(n-1)*((numvars+1)^N):n*((numvars+1)^N)]), Val(numvars)) - # return (p(t, c[1+(1-1)*((numvars+1)^N):1*((numvars+1)^N)]), p(t, c[1+(2-1)*((numvars+1)^N):2*((numvars+1)^N)])) - end - return mpol # (t, c)->ntuple(n->p(t, c[1+(n-1)*((numvars+1)^N):n*((numvars+1)^N)]), Val(numvars)) - # return (t, c) -> Tuple(p(t,c[1+(n-1)*((numvars+1)^N):n*((numvars+1)^N)]) for n=1:numvars) -end - -function get_num_poly_vars(::Val{numvars}, ::Val{N}) where {numvars, N} - return binomial(numvars+N, N) -end - -function get_num_multipoly_vars(::Val{numvars}, ::Val{N}) where {numvars, N} - return numvars*get_num_poly_vars(Val(numvars), Val(N)) -end - -function test_poly_allocations() - # get_num_poly_vars(Val(2), Val(3)) # 10 indices - # p = get_polynomial(Val(2), Val(3)) - # @time p.(Tuple.(CartesianIndices((200,200))),Ref((1.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))); - - polynomial(Val(2), (2, 20), (1f0, 1f0, 1f0, 1f0, 1f0, 1f0)) == 467 - - cids = Tuple.(CartesianIndices((200,200))) - # cfds = map((t)->Tuple(Float32.([t...])), cids) - cs = Tuple(Float32.(collect(1:27))) - res = zeros(Float32, 200,200) - get_num_poly_vars(Val(2), Val(2)) # 6 indices required - @time res .= polynomial.(Ref(Val(2)), cids, Ref(cs)); # 2 orders, two variables - # 0.000122 seconds (3 allocations: 168 bytes) - - get_num_poly_vars(Val(3), Val(2)) # 10 indices required - @time res .= polynomial.(Ref(Val(3)), cids, Ref(cs)); # 3 orders, two variables - # 0.003710 seconds (240.00 k allocations: 13.428 MiB) - - get_num_poly_vars(Val(4), Val(2)) # 15 indices required - @time res .= polynomial.(Ref(Val(4)), cids, Ref(cs)); # 2 orders, two variables - # 0.008149 seconds (480.00 k allocations: 26.856 MiB) - - get_num_poly_vars(Val(5), Val(2)) # 21 indices required - @time res .= polynomial.(Ref(Val(5)), cids, Ref(cs)); # 2 orders, two variables - #0.014299 seconds (1.08 M allocations: 60.425 MiB, 23.18% gc time) - - @time polynomial.(Ref(Val(0)), cids, Ref(cs)); - # 0.000076 seconds (5 allocations: 156.461 KiB) - -end diff --git a/src/transformation_types.jl b/src/transformation_types.jl new file mode 100644 index 0000000..119704d --- /dev/null +++ b/src/transformation_types.jl @@ -0,0 +1,4 @@ +abstract type transformation_method end + +struct AffineMode <: transformation_method end +struct PolynomialMode <: transformation_method end \ No newline at end of file diff --git a/src/transformators.jl b/src/transformators.jl index 820d91f..9a72e7b 100644 --- a/src/transformators.jl +++ b/src/transformators.jl @@ -1,11 +1,14 @@ using Interpolations using FourierTools using StaticArrays +using EvalMultiPoly -export get_function, get_function_tuple, get_function_svec, get_function_affine, get_function_poly +export get_interpolated_function, get_function_tuple, get_function_svec, get_function_affine, get_function_poly export add_dim, red_dim_apply, red_dim, mat_mul, func_transform export extrapolate, interpolate +export PolynomialMode, AffineMode + """ get_function(data::AbstractArray; super_sampling=2, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) @@ -21,7 +24,7 @@ This is useful for fitting with a function which is itself defined by measured d """ -function get_function(data::AbstractArray; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) +function get_function_old(data::AbstractArray; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) new_size = super_sampling.*size(data) upsampled = fftshift(resample(ifftshift(data), new_size)) # @show upsampled @@ -177,7 +180,7 @@ function apply_transform(coord_transf_func::Function, data::AbstractArray{T}, it out[it] = idx_apply(itp, coord_transf_func(it)) end """ - return map((it) -> idx_apply(itp, coord_transf_func(it)), (CartesianIndices(data))) + return map((it) -> idx_apply(itp, coord_transf_func(it)), Tuple.(CartesianIndices(data))) # out .= idx_apply.(Ref(itp), coord_transf_func.(CartesianIndices(data))); # return idx_apply.(Ref(itp), coord_transf_func.(CartesianIndices(data))); end @@ -311,7 +314,26 @@ function get_function_affine(data::AbstractArray{T}; super_sampling=2, extrapola matrix_c = t_to_origin * scale_mat * rot_mat * shear_mat *shift_mat * t_to_center return apply_transform_affine(matrix_c, data, itp) #, out); # do not call interolated here for type stability reasons - #return out; + end + + + function interpolated(p::NTuple{N, T}) where {N, T} #, out = similar(data)) where T1 + x_cen, y_cen = (size(data) .÷ 2.0 .+1) + # x_cen_up, y_cen_up = (size(upsampled) .÷ 2.0 .+ 1.0) + + # creating the matrices of rotation, shear, scale, and shift + rot_mat = @SMatrix T[cos(p[7]) -1.0*sin(p[7]) 0.0; sin(p[7]) cos(p[7]) 0.0; 0.0 0.0 1.0]; + shear_mat = @SMatrix T[1.0 p[5] 0.0; p[6] 1.0 0.0; 0.0 0.0 1.0]; + scale_mat = @SMatrix T[1/p[3] 0.0 0.0; 0.0 1/p[4] 0.0; 0.0 0.0 1.0]; + shift_mat = @SMatrix T[1.0 0.0 -1*p[1]; 0.0 1.0 -1*p[2]; 0.0 0.0 1.0]; + t_to_origin = @SMatrix T[1.0 0.0 1*x_cen; 0.0 1.0 y_cen; 0.0 0.0 1.0]; + t_to_center = @SMatrix T[1.0 0.0 -1.0*x_cen; 0.0 1.0 -1.0*y_cen; 0.0 0.0 1.0]; + # t_orig_upsampled = SMatrix{3, 3}(T[1.0 0.0 -1.0*x_cen_up; 0.0 1.0 -1.0*y_cen_up; 0.0 0.0 1.0]); + + # building the overall transformation matrix + matrix_c = t_to_origin * scale_mat * rot_mat * shear_mat *shift_mat * t_to_center + + return apply_transform_affine(matrix_c, data, itp) #, out); # do not call interolated here for type stability reasons end return interpolated @@ -338,4 +360,57 @@ The optional argument `out` can be used to store the result of the transformatio function get_function_poly(data::AbstractArray{T}, order; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where T pm = get_multi_poly(Val(ndims(data)), Val(order)) return get_function_tuple(data, pm; super_sampling= super_sampling, extrapolation_bc=extrapolation_bc, interp_type=interp_type); +end + +""" + get_interpolated_function(data::AbstractArray, ::Type{AffineMode}; super_sampling=2, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) + +returns a function `interpolated(p)` which generates a transformed version of the original data parameterized by transform parameters. +This is useful for fitting with a function which is itself defined by measured data. +The returned function supports two ways to be used, with an affine transform matrix `p` as in input or with a vector or tuple `p` of parameters. + +# Arguments +`data`: The data to represent by the function `dat` +`AffineMode`: The transformation mode to use +`super_sampling`: The factor by which the data is internally represented as a supersampled version (Fourier-based upsampling, see `FourierTools.resample`) +`extrapolation_bc`: The extrapolation boundary condition to select for values outside the range. + By default the value 0.0 is used. Other options are `Flat()`, or `Line()`, See the package `Interpolation` for details. +`interp_type`: The type of interpolation to use. See the package `Interpolation` for details. + +# Returns +A function `interpolated(p)` which generates a transformed version of the original data parameterized by transform parameters +""" +function get_interpolated_function(data::AbstractArray{T, N}, ::Type{AffineMode}; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where {T, N} + return get_function_affine(data; super_sampling=super_sampling, extrapolation_bc=extrapolation_bc, interp_type=interp_type) +end + +function get_interpolated_function(data::AbstractArray{T, N}; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where {T, N} + @warn "No transformation mode provided. `AffineMode` is used as default" + return get_interpolated_function(data, AffineMode; super_sampling=super_sampling, extrapolation_bc=extrapolation_bc, interp_type=interp_type) +end + +""" + get_interpolated_function(data::AbstractArray, ::Type{PolynomialMode}, order=nothing; super_sampling=2, extrapolation_bc=Flat(), interp_type=Interpolations.BSpline(Linear())) + +returns a function `interpolated(p)` which generates a transformed version of the original data parameterized by transform parameters. +This is useful for fitting with a function which is itself defined by measured data. +The returned function supports polynomial transformations of the data. + +# Arguments +`data`: The data to represent by the function `dat` +`PolynomialMode`: The transformation mode to use +`order`: The order of the polynomial to use for the transformation +`super_sampling`: The factor by which the data is internally represented as a supersampled version (Fourier-based upsampling, see `FourierTools.resample`) +`extrapolation_bc`: The extrapolation boundary condition to select for values outside the range. + By default the value 0.0 is used. Other options are `Flat()`, or `Line()`, See the package `Interpolation` for details. +`interp_type`: The type of interpolation to use. See the package `Interpolation` for details. + +# Returns +A function `interpolated(p)` which generates a transformed version of the original data parameterized by polynomial transform parameters +""" +function get_interpolated_function(data::AbstractArray{T, N}, ::Type{PolynomialMode}, order=nothing; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where {T, N} + if isnothing(order) + error("Providing the order of the transformation polynomial is mandatory for the `PolynomialMode`") + end + return get_function_poly(data, order; super_sampling=super_sampling, extrapolation_bc=extrapolation_bc, interp_type=interp_type) end \ No newline at end of file diff --git a/src/utils.jl b/src/utils.jl deleted file mode 100644 index b04e514..0000000 --- a/src/utils.jl +++ /dev/null @@ -1,18 +0,0 @@ -export split_tuple - -""" - split_tuple(t::NTuple{S,T},::Val{numvars}) where {S,T,numvars} - -Split a tuple into `numvars` parts packed into a tuple of tuples. The tuple `t` is assumed to have a length that is a multiple of `numvars`. - -Example: -```juliadoc -julia> t = (1,2,3,4,5,6) -julia> split_tuple(t, Val{2}) -((1,2), (3,4), (5,6)) -``` -""" -function split_tuple(t::NTuple{S,T},::Val{numvars}) where {S,T,numvars} - return ntuple(n->t[1+(n-1)*(S÷numvars):n*(S÷numvars)], Val(numvars)) -end - From 4c041b9ec0bb96475e3207e28c1cb98b69552d8e Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Wed, 30 Oct 2024 15:36:59 +0100 Subject: [PATCH 40/44] added CUDA functionality and some examples --- .gitignore | 7 ++ Project.toml | 1 + examples/Project.toml | 3 + examples/deform_testimage.jl | 38 ++++++-- examples/poly_test_exp.jl | 172 +++++++++++++++++++++++++++++++++ examples/poly_test_exp_new.jl | 173 ++++++++++++++++++++++++++++++++++ src/DataToFunctions.jl | 7 ++ src/requires.jl | 16 ++++ src/transformators.jl | 18 ++-- 9 files changed, 422 insertions(+), 13 deletions(-) create mode 100644 examples/poly_test_exp.jl create mode 100644 examples/poly_test_exp_new.jl create mode 100644 src/requires.jl diff --git a/.gitignore b/.gitignore index ad46f5a..d41e1f3 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,10 @@ examples/Manifest.toml *.mp4 *.png examples/figures/* +examples/*.tif +examples/*.ipynb +*.jpeg +*.gif +*.jpg +*.txt +*.txt \ No newline at end of file diff --git a/Project.toml b/Project.toml index eb883bc..672a8de 100644 --- a/Project.toml +++ b/Project.toml @@ -7,6 +7,7 @@ version = "0.1.0" EvalMultiPoly = "c78649ec-f9ed-405e-be6d-0472f43586aa" FourierTools = "b18b359b-aebc-45ac-a139-9c0ccbb2871e" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" +Requires = "ae029012-a4dd-5104-9daa-d747884805df" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [compat] diff --git a/examples/Project.toml b/examples/Project.toml index 5a2e1dd..1fd1284 100644 --- a/examples/Project.toml +++ b/examples/Project.toml @@ -1,6 +1,8 @@ [deps] +Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" CoordinateTransformations = "150eb455-5306-5404-9cee-2592286d6298" DataToFunctions = "64cfdffa-4d02-49ee-ae8b-a805370874f5" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" @@ -15,6 +17,7 @@ Images = "916415d5-f1e6-5110-898d-aaa5f9f070e0" IndexFunArrays = "613c443e-d742-454e-bfc6-1d7f8dd76566" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" InverseModeling = "ce844058-9528-415d-a63d-06f3dd08b29f" +LBFGSB = "5be7bae1-8223-5378-bac3-9e7378a2f6e6" LineSearches = "d3d80556-e9d4-5f37-9878-2ab0fcc64255" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" diff --git a/examples/deform_testimage.jl b/examples/deform_testimage.jl index 274c0ae..3e96f79 100644 --- a/examples/deform_testimage.jl +++ b/examples/deform_testimage.jl @@ -21,7 +21,7 @@ using Pkg Pkg.activate(".") # ╔═╡ 4af0c13d-fc42-4fe7-97e6-2248e36b63e2 -Pkg.add("PlutoUI"); +Pkg.add("PlutoUI") # ╔═╡ a2d75cfb-feab-4130-8439-30c543618d04 using DataToFunctions, ImageShow, TestImages, PlutoUI, Images @@ -66,6 +66,8 @@ md""" # ╔═╡ 13461a95-95ea-4bad-8673-e94e06776254 md""" +## First order polynomial + First order polynomial transformation which is as follows: ``x^{\prime} = c_{1} + c_{2}{x}^{1} + c_{3}{y}^{1}`` @@ -90,7 +92,7 @@ function coeffs_input(coeffs::Vector) ] md""" - #### Transfrorm coefficients + #### Transform coefficients $(inputs) """ end @@ -108,10 +110,30 @@ maximum(h((c.c1,c.c2,c.c3,c.c4,c.c5,c.c6))) # ╔═╡ 6584aacc-440e-457b-bf52-83f8db40c999 h((c.c1,c.c2,c.c3,c.c4,c.c5,c.c6)) +# ╔═╡ 74cf9f27-f559-4fe2-9a30-20cb7cf6fe81 +md""" +## Second order polynomial + +Second order polynomial transformation is as follows: + +``x^{\prime} = c_{1} + c_{2}{x}^{1} + c_{3}{x}^{2} + c_{4}{y}^{1} + c_{5}{x}{y} + c_{6}{y}^{2}`` + +``y^{\prime} = c_{7} + c_{8}{x}^{1} + c_{9}{x}^{2} + c_{10}{y}^{1} + c_{11}{x}{y} + c_{12}{y}^{2}`` +""" + +# ╔═╡ 1f440592-9bbb-429c-8ba3-d018f5b354b0 +h2 = get_interpolated_function(data, PolynomialMode, 2); + +# ╔═╡ 12f59a6d-4600-4379-b536-f3b4701bdbe4 +@bind c2 coeffs_input(["c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "c10", "c11", "c12"]) + +# ╔═╡ fd60df55-2a80-48a9-95ec-eeeb1cfe7491 +simshow(h2((c2.c1, c2.c2, c2.c3, c2.c4, c2.c5, c2.c6, c2.c7, c2.c8, c2.c9, c2.c10, c2.c11, c2.c12)), cmap=:turbo)#,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0,0f0))) + # ╔═╡ Cell order: # ╠═28975586-853e-4e19-b9eb-65c41fa61a43 # ╠═0ae2da4f-3f75-47bb-a899-9e89c5c3f17c -# ╠═4af0c13d-fc42-4fe7-97e6-2248e36b63e2 +# ╟─4af0c13d-fc42-4fe7-97e6-2248e36b63e2 # ╠═1c744bec-f085-4812-ab1a-40a32c2ac176 # ╠═a2d75cfb-feab-4130-8439-30c543618d04 # ╠═5ac1123d-5df3-4c9d-aff1-ffe91d931497 @@ -127,8 +149,12 @@ h((c.c1,c.c2,c.c3,c.c4,c.c5,c.c6)) # ╟─6676ba35-3efd-49f5-9819-411ad8f8a95c # ╟─13461a95-95ea-4bad-8673-e94e06776254 # ╠═87be45c0-8b2e-4d49-abd1-a274b3c1815e -# ╟─69331d73-75a3-4727-acda-e79779a2bd03 -# ╟─68f771aa-2cde-41cd-990c-9ec7dc2146a4 -# ╟─74228a9d-6cc2-4aaf-97da-67f32670341e +# ╠═69331d73-75a3-4727-acda-e79779a2bd03 +# ╠═68f771aa-2cde-41cd-990c-9ec7dc2146a4 +# ╠═74228a9d-6cc2-4aaf-97da-67f32670341e # ╠═687a198b-9020-4959-8e27-fd0896d4b1fc # ╠═6584aacc-440e-457b-bf52-83f8db40c999 +# ╟─74cf9f27-f559-4fe2-9a30-20cb7cf6fe81 +# ╠═1f440592-9bbb-429c-8ba3-d018f5b354b0 +# ╠═12f59a6d-4600-4379-b536-f3b4701bdbe4 +# ╠═fd60df55-2a80-48a9-95ec-eeeb1cfe7491 diff --git a/examples/poly_test_exp.jl b/examples/poly_test_exp.jl new file mode 100644 index 0000000..202a7b3 --- /dev/null +++ b/examples/poly_test_exp.jl @@ -0,0 +1,172 @@ +using Images +using DataToFunctions +using FindShift +using Optim, CUDA +using FourierTools +using View5D, Plots, Statistics + +CUDA.allowscalar(false) + +file1 = raw"D:\Hossein\Programming\Julia\DataToFunctions.jl\examples\test_polim1.jpeg" +file2 = raw"D:\Hossein\Programming\Julia\DataToFunctions.jl\examples\test_polim2.jpeg" + +c1 = Float32.(Gray.(load(file1))) +c2 = Float32.(Gray.(load(file2))) + +#TODO increase the size of the images +img11 = c1[30:1519+30, 30:779+30] +img12 = c1[30:1519+30, 800:779+800] +img21 = c2[30:1519+30, 30:779+30] +img22 = c2[30:1519+30, 800:779+800] + +img11_c = CuArray(img11) +img12_c = CuArray(img12) +img21_c = CuArray(img21) +img22_c = CuArray(img22) + +function resize_img(data, scale) + new_size = size(data) .÷ scale + imresize(data, new_size...) +end + +resample_size = 2 +img1_resampled = img11[1:resample_size:end, 1:resample_size:end] #resize_img(img1, 1) +img2_resampled = img12[1:resample_size:end, 1:resample_size:end] #resize_img(img2, 1) + +#im11 = imfilter(img1_resampled, Kernel.gaussian(5)); +#im12 = imfilter(img2_resampled, Kernel.gaussian(5)); + +f = get_interpolated_function(img12_c, PolynomialMode, 2); +loss_p(p1::AbstractArray) = (sum(abs2.(f(Tuple(p1)) .- img11_c))); +loss_updated(p1::AbstractArray) = mapreduce(abs2, +, f(Tuple(p1)) .- img11_c); +#st_vals = [0.0, 1.00, 0.00, 0.0, 0.00, 1.0] #ones(Float64, 6)./10 +st_vals = Float32[0.0, 1.0, 0, 0, 0, 0, 0.0, 0, 0, 1.0, 0, 0]; +# Float64[9.0, 0, 0, 0, 0, 0, 1, 0, 0, 5.0, 0, 0, 1, 0, 0, 0, 0, 0] +# @vv f(Tuple(st_vals)) +# loss_m(st_vals) + +#a = f(Tuple(st_vals)) +# @time a = f(Tuple(st_vals)); +""" +function do_registeration_step(resample_size, gaussian_kernel_size, img1, img2, st_vals=[0f0, 1f0, 0f0, 0f0, 0f0, 1f0]) + img1_resampled = img1[1:resample_size:end, 1:resample_size:end] #resize_img(img1, 1) + img2_resampled = img2[1:resample_size:end, 1:resample_size:end] #resize_img(img2, 1) + + im1 = imfilter(img1_resampled, Kernel.gaussian(gaussian_kernel_size)); + im2 = imfilter(img2_resampled, Kernel.gaussian(gaussian_kernel_size)); + + im1_c = CuArray(im1) + im2_c = CuArray(im2) + + f = get_interpolated_function(im2_c, PolynomialMode, 1); + loss_updated(p1::AbstractArray) = mapreduce(abs2, +, f(Tuple(p1)) .- im1_c); + + #st_vals = Float32[0.0, 1.0, 0, 0, 0, 0, 0.0, 0, 0, 1.0, 0, 0]; + CUDA.@time res = optimize( + loss_updated, + st_vals, + BFGS(),#; initial_stepnorm = 1f-1),#; linesearch=LineSearches.BackTracking(order=2)), + autodiff = :forward + ) + #return [res.minimizer[1]*resample_size, 1.0, 0, 0, 0, 0, res.minimizer[7]*resample_size, 0, 0, 1.0, 0, 0] + return [res.minimizer[1]*resample_size, 1.0, 0, res.minimizer[4]*resample_size, 0, 1.0] +end + +res_step1 = do_registeration_step(10, 5, img11, img12) +@vt img11 get_interpolated_function(img12, PolynomialMode, 1)(Tuple(res_step1)) img12 +""" + + +""" +function g!(G, x) # (G, x) + G .= gradient(loss_updated, x)[1] +end +od = OnceDifferentiable(loss_updated, g!, st_vals) +""" +aligned_imgs = Array(copy(img11_c)) + + +CUDA.@time res = optimize( + loss_updated, + st_vals, + #Newton(), + BFGS(; initial_stepnorm = 1f-2),#; linesearch=LineSearches.BackTracking(order=2)), + #LBFGS(; linesearch=LineSearches.BackTracking(order=3)), + #lower, upper, + #init_x, + #Fminbox(inner_optimizer), + #Optim.Options(store_trace = true, extended_trace = true, iterations=5000), + autodiff = :forward +) +""" +10.271416 seconds (8.78 M CPU allocations: 589.752 MiB, 1.06% gc time) (2.25 k GPU allocations: 52.251 GiB, 0.30% memmgmt time) +* Status: success + +* Candidate solution +Final objective value: 2.966678e+03 + +* Found with +Algorithm: BFGS + +* Convergence measures +|x - x'| = 1.76e-05 ≰ 0.0e+00 +|x - x'|/|x'| = 3.62e-06 ≰ 0.0e+00 +|f(x) - f(x')| = 0.00e+00 ≤ 0.0e+00 +|f(x) - f(x')|/|f(x')| = 0.00e+00 ≤ 0.0e+00 +|g(x)| = 1.17e+05 ≰ 1.0e-01 + +* Work counters +Seconds run: 5 (vs limit Inf) +Iterations: 73 +f(x) calls: 450 +∇f(x) calls: 450 +""" +aligned_imgs = cat(aligned_imgs, Array(f(Tuple(Optim.minimizer(res)))), dims=3); + + +for img_t in [img21_c, img22_c] + f = get_interpolated_function(img_t, PolynomialMode, 2); + loss_p(p1::AbstractArray) = (sum(abs2.(f(Tuple(p1)) .- img11_c))); + loss_updated(p1::AbstractArray) = mapreduce(abs2, +, f(Tuple(p1)) .- img11_c); + #st_vals = Float64[0.0, 1.00, 0.00, size(img11)[2], 0.00, -1.0] #ones(Float64, 6)./10 + st_vals = Float32[0.0, 1.0, 0, 0, 0, 0, size(img11)[2], 0, 0, -1.0, 0, 0]; + # Float64[9.0, 0, 0, 0, 0, 0, 1, 0, 0, 5.0, 0, 0, 1, 0, 0, 0, 0, 0] + # @vv f(Tuple(st_vals)) + # loss_m(st_vals) + + """ + function g!(G, x) # (G, x) + G .= gradient(loss_p, x)[1] + end + od = OnceDifferentiable(loss_p, g!, st_vals) + """ + CUDA.@time res = optimize( + loss_updated, + st_vals, + #Newton(), + BFGS(; initial_stepnorm = 1f-2),#; linesearch=LineSearches.BackTracking(order=2)), + #LBFGS(),#; linesearch=LineSearches.BackTracking(order=3)), + #lower, upper, + #init_x, + #Fminbox(inner_optimizer), + #Optim.Options(store_trace = true, extended_trace = true, iterations=5000), + autodiff = :forward + ) + + + aligned_imgs = cat(aligned_imgs, Array(f(Tuple(Optim.minimizer(res)))), dims=3); +end + + +aligned_imgs = clamp.(aligned_imgs, 0, 1) +@vv aligned_imgs + +aligned_imgs = permutedims(aligned_imgs, [2, 1, 3]) +save("aligned_all_cuda_polyorder2.gif", Gray.(aligned_imgs)) + + + +raw_imgs = cat(img11, img12, img21, img22, dims=3) +raw_imgs = clamp.(raw_imgs, 0, 1) +raw_imgs = permutedims(raw_imgs, [2, 1, 3]) +save("raw_all_imgs.gif", Gray.(raw_imgs)) diff --git a/examples/poly_test_exp_new.jl b/examples/poly_test_exp_new.jl new file mode 100644 index 0000000..b5f8233 --- /dev/null +++ b/examples/poly_test_exp_new.jl @@ -0,0 +1,173 @@ +using Images +using DataToFunctions +using FindShift +using Optim, CUDA +using FourierTools, Zygote +using View5D, Plots, Statistics, LineSearches + +CUDA.allowscalar(false) + +file1 = raw"D:\Hossein\Programming\Julia\DataToFunctions.jl\examples\markerpen_C1_00001.tif" +file2 = raw"D:\Hossein\Programming\Julia\DataToFunctions.jl\examples\markerpen_C2_00001.tif" + +c1 = Float32.(Gray.(load(file1))) +c2 = Float32.(Gray.(load(file2))) + +wide=true +if wide + img11 = c1[180:1990+180, 160:2050+160] + img12 = c1[160:1990+160, 2180:2050+2180] + img21 = c2[130:1990+130, 140:2050+140] + img22 = c2[130:1990+130, 2160:2050+2160] +end + +@vt img11 img12 img21 img22 + + + +img11_c = CuArray(img11./maximum(img11)) +img12_c = CuArray(img12./maximum(img12)) +img21_c = CuArray(img21./maximum(img21)) +img22_c = CuArray(img22./maximum(img22)) + + +resample_size = 2 +img1_resampled = img11[1:resample_size:end, 1:resample_size:end] #resize_img(img1, 1) +img2_resampled = img12[1:resample_size:end, 1:resample_size:end] #resize_img(img2, 1) + +#im11 = imfilter(img1_resampled, Kernel.gaussian(5)); +#im12 = imfilter(img2_resampled, Kernel.gaussian(5)); + + +""" +resample_size=50 +img1_resampled = img11[1:resample_size:end, 1:resample_size:end] #resize_img(img1, 1) +img2_resampled = img12[1:resample_size:end, 1:resample_size:end] #resize_img(img2, 1) + +im1 = imfilter(img1_resampled, Kernel.gaussian(3)); +im2 = imfilter(img2_resampled, Kernel.gaussian(3)); + +im1_c = CuArray(im1./maximum(im1)) +im2_c = CuArray(im2./maximum(im2)) + +f1 = get_interpolated_function(im2_c, PolynomialMode, 2); +loss_updated1(p1::AbstractArray) = mapreduce(abs2, +, f(Tuple(p1)) .- im1_c); + +st_vals = Float32[0.0, 1.0, 0, 0, 0, 0, 0.0, 0, 0, 1.0, 0, 0]; +CUDA.@time res1 = optimize( + loss_updated1, + st_vals, + BFGS(; initial_stepnorm = 1f-2),#; linesearch=LineSearches.BackTracking(order=2)), + autodiff = :forward +) + +@vt Array(im1_c) Array(f1(Tuple(res1.minimizer))) Array(im2_c) + +function do_registeration_step(resample_size, gaussian_kernel_size, img1, img2, st_vals=Float32[0.0, 1.0, 0, 0, 0, 0, 0.0, 0, 0, 1.0, 0, 0]) + img1_resampled = img1[1:resample_size:end, 1:resample_size:end] #resize_img(img1, 1) + img2_resampled = img2[1:resample_size:end, 1:resample_size:end] #resize_img(img2, 1) + + im1 = imfilter(img1_resampled, Kernel.gaussian(gaussian_kernel_size)); + im2 = imfilter(img2_resampled, Kernel.gaussian(gaussian_kernel_size)); + + im1_c = CuArray(im1) + im2_c = CuArray(im2) + + f = get_interpolated_function(im2_c, PolynomialMode, 2); + loss_updated(p1::AbstractArray) = mapreduce(abs2, +, f(Tuple(p1)) .- im1_c); + + #st_vals = Float32[0.0, 1.0, 0, 0, 0, 0, 0.0, 0, 0, 1.0, 0, 0]; + CUDA.@time res = optimize( + loss_updated, + st_vals, + BFGS(),#; initial_stepnorm = 1f-1),#; linesearch=LineSearches.BackTracking(order=2)), + autodiff = :forward + ) + return [res.minimizer[1]*resample_size, 1.0, 0, 0, 0, 0, res.minimizer[7]*resample_size, 0, 0, 1.0, 0, 0] + #return [res.minimizer[1]*resample_size, 1.0, 0, res.minimizer[4]*resample_size, 0, 1.0] +end + +res_step1 = do_registeration_step(10, 5, img11, img12) +@vt img11 get_interpolated_function(img12, PolynomialMode, 2)(Tuple(res_step1)) img12 +""" + + +order=2 +f = get_interpolated_function(img12_c, PolynomialMode, order); +loss_p(p1::AbstractArray) = (sum(abs2.(f(Tuple(p1)) .- img11_c))); +loss_updated(p1::AbstractArray) = mapreduce(abs2, +, f(Tuple(p1)) .- img11_c); +if order ==1 + st_vals = Float32[0.0, 1.00, 0.00, 0.0, 0.00, 1.0] #ones(Float64, 6)./10 +elseif order == 2 + st_vals = Float32[0.0, 1.0, 0, 0, 0, 0, 0.0, 0, 0, 1.0, 0, 0]; +end + + +aligned_imgs = Array(copy(img11_c)) + + +CUDA.@time res = optimize( + loss_updated, + st_vals, + #Newton(), + BFGS(; initial_stepnorm = 1f-1),#; linesearch=LineSearches.BackTracking(order=2)), + #LBFGS(; linesearch=LineSearches.BackTracking(order=3)), + #lower, upper, + #init_x, + #Fminbox(inner_optimizer), + Optim.Options(store_trace = true, extended_trace = true, iterations=5000, g_tol=1f-2), + autodiff = :forward +) + +open("markerpen_new_data.txt", "w") do f + write(f, "\norder $(order) params = $(res.minimizer)") +end + +aligned_imgs = cat(aligned_imgs, Array(f(Tuple(res.minimizer))), dims=3); +@vv aligned_imgs + + +for img_t in [img21_c, img22_c] + f = get_interpolated_function(img_t, PolynomialMode, 2); + loss_p(p1::AbstractArray) = (sum(abs2.(f(Tuple(p1)) .- img11_c))); + loss_updated(p1::AbstractArray) = mapreduce(abs2, +, f(Tuple(p1)) .- img11_c); + #st_vals = Float64[0.0, 1.00, 0.00, size(img11)[2], 0.00, -1.0] #ones(Float64, 6)./10 + #st_vals = Float32[0.0, 1.0, 0, 0, 0, 0, size(img11)[1], 0, 0, -1.0, 0, 0]; + if order ==1 + st_vals = Float32[0.0, 1.00, 0.00, 0.0, 0.00, 1.0] #ones(Float64, 6)./10 + elseif order == 2 + st_vals = Float32[0.0, 1.0, 0, 0, 0, 0, 0.0, 0, 0, 1.0, 0, 0]; + end + + CUDA.@time res = optimize( + loss_updated, + st_vals, + #Newton(), + BFGS(; initial_stepnorm = 1f-1),#; linesearch=LineSearches.BackTracking(order=2)), + #LBFGS(),#; linesearch=LineSearches.BackTracking(order=3)), + #lower, upper, + #init_x, + #Fminbox(inner_optimizer), + Optim.Options(store_trace = true, extended_trace = true, iterations=5000, g_tol=1f-2), + autodiff = :forward + ) + println(res) + open("markerpen_new_data.txt", "a") do f + write(f, "\norder $(order) params = $(res.minimizer)") + end + aligned_imgs = cat(aligned_imgs, Array(f(Tuple(Optim.minimizer(res)))), dims=3); +end + + +aligned_imgs = clamp.(aligned_imgs, 0, 1) +@vv aligned_imgs + +aligned_imgs = permutedims(aligned_imgs, [2, 1, 3]) +save("aligned_all_cuda_polyorder2_markerpen_new.gif", Gray.(aligned_imgs)) + + + +raw_imgs = cat(img11, img12, img21, img22, dims=3) +raw_imgs = clamp.(raw_imgs, 0, 1) +raw_imgs = permutedims(raw_imgs, [2, 1, 3]) +save("raw_all_imgs_markerpen_new.gif", Gray.(raw_imgs)) diff --git a/src/DataToFunctions.jl b/src/DataToFunctions.jl index 77019a6..c9ba973 100644 --- a/src/DataToFunctions.jl +++ b/src/DataToFunctions.jl @@ -1,5 +1,12 @@ module DataToFunctions +using Requires + +export gpu_or_cpu + +# to include CUDA +include("requires.jl") + include("transformation_types.jl") include("transformators.jl") diff --git a/src/requires.jl b/src/requires.jl new file mode 100644 index 0000000..f1eb5dd --- /dev/null +++ b/src/requires.jl @@ -0,0 +1,16 @@ +# from https://github.com/RainerHeintzmann/DeconvOptim.jl/blob/362a741224957155fbc046d3297d43339766721d/src/requires.jl + +isgpu(x) = false +gpu_or_cpu(x) = nothing + +function __init__() + @require CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" begin + @info "DataToFunctions.jl: CUDA.jl is loaded." + + gpu_or_cpu(x) = CUDA.CuArray{Float32} + isgpu(x::CUDA.CuArray) = true + # prevent slow scalar indexing on GPU + CUDA.allowscalar(false); + + end +end \ No newline at end of file diff --git a/src/transformators.jl b/src/transformators.jl index 9a72e7b..2778e6b 100644 --- a/src/transformators.jl +++ b/src/transformators.jl @@ -111,7 +111,7 @@ applies a function to a SVector `fct`: The function to apply `svec::SVector{S,T}`: A SVector """ -@inline function idx_apply(fct, svec::SVector{S,T}) where {S,T} +@inline function idx_apply(fct, svec::SVector{S,T})::Number where {S,T} return fct(svec...) end @@ -123,7 +123,7 @@ applies a function to a Tuple `fct`: The function to apply `tup::NTuple{S,T}`: A Tuple """ -@inline function idx_apply(fct, tup::NTuple{S,T}) where {S,T} +@inline function idx_apply(fct, tup::NTuple{S,T})::Number where {S,T} return fct(tup...) end @@ -173,15 +173,17 @@ applies a general coordinate transformation function to the indices of an array `data::AbstractArray{T}`: The data to transform `itp`: The interpolation object to use """ -function apply_transform(coord_transf_func::Function, data::AbstractArray{T}, itp) where {T} #, out::AbstractArray{T}) where {T} +function apply_transform(coord_transf_func::Function, data::AbstractArray{T, N}, itp) where {T, N} #, out::AbstractArray{T}) where {T} # @info "Applying tuple transformation" """ for it in CartesianIndices(data) out[it] = idx_apply(itp, coord_transf_func(it)) end """ - return map((it) -> idx_apply(itp, coord_transf_func(it)), Tuple.(CartesianIndices(data))) - # out .= idx_apply.(Ref(itp), coord_transf_func.(CartesianIndices(data))); + #res = similar(data); + #return map((it) -> idx_apply(itp, coord_transf_func(Tuple(it))), CartesianIndices(data)) + return idx_apply.(Ref(Interpolations.adapt(gpu_or_cpu(nothing), itp)), coord_transf_func.(Tuple.(CartesianIndices(data)))); + #return idx_apply.(Ref(itp), coord_transf_func.(Tuple.(CartesianIndices(data)))); # return idx_apply.(Ref(itp), coord_transf_func.(CartesianIndices(data))); end @@ -236,11 +238,12 @@ This is useful for fitting with a function which is itself defined by measured d # Example """ -function get_function_tuple(data::AbstractArray{T}, fct_tup::Function; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where T +function get_function_tuple(data::AbstractArray{T, N}, fct_tup::Function; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where {T, N} # building the extraplation + interpolation object itp = extrapolate(interpolate(data, interp_type), extrapolation_bc); function interpolated(params)#, out = similar(data)) fct_tup_noparams(ci) = fct_tup(ci, params) + #@show Interpolations.adapt(gpu_or_cpu(1), itp) return apply_transform(fct_tup_noparams, data, itp); end return interpolated @@ -357,7 +360,7 @@ The optional argument `out` can be used to store the result of the transformatio By default the value 0.0 is used. Other options are `Flat()`, or `Line()`, See the package `Interpolation` for details. `interp_type`: The type of interpolation to use. See the package `Interpolation` for details. """ -function get_function_poly(data::AbstractArray{T}, order; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where T +function get_function_poly(data::AbstractArray{T, N}, order; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where {T, N} pm = get_multi_poly(Val(ndims(data)), Val(order)) return get_function_tuple(data, pm; super_sampling= super_sampling, extrapolation_bc=extrapolation_bc, interp_type=interp_type); end @@ -409,6 +412,7 @@ The returned function supports polynomial transformations of the data. A function `interpolated(p)` which generates a transformed version of the original data parameterized by polynomial transform parameters """ function get_interpolated_function(data::AbstractArray{T, N}, ::Type{PolynomialMode}, order=nothing; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where {T, N} + #TODO from CuArray to CuArray if isnothing(order) error("Providing the order of the transformation polynomial is mandatory for the `PolynomialMode`") end From 0f7f69c5355e230f4877e7dfee18647d653e8f57 Mon Sep 17 00:00:00 2001 From: RainerHeintzmann Date: Tue, 10 Jun 2025 15:27:12 +0200 Subject: [PATCH 41/44] updated Project.toml --- Project.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index 672a8de..d7cc818 100644 --- a/Project.toml +++ b/Project.toml @@ -11,10 +11,10 @@ Requires = "ae029012-a4dd-5104-9daa-d747884805df" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [compat] +FourierTools = "0.4" +Interpolations = "0.15.1, 0.16" +StaticArrays = "1.9.6, 1.10" Aqua = "0.8" -FourierTools = "0.4.4" -Interpolations = "0.15.1" -StaticArrays = "1.9.6" Test = "1.9.3" Zygote = "0.6.70" julia = "1" From 4885978710bfc4acaa5ff312521e09cd50988933 Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Thu, 28 Aug 2025 17:50:35 +0200 Subject: [PATCH 42/44] updated Project.toml, compatible with new ver of Interpolations.jl. --- .gitignore | 6 +++++- Project.toml | 2 +- examples/PSF_fitting_new | 0 examples/Project.toml | 3 ++- src/transformators.jl | 13 ++++--------- 5 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 examples/PSF_fitting_new diff --git a/.gitignore b/.gitignore index d41e1f3..32f88c7 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,8 @@ docs/site/ # environment. Manifest.toml examples/Manifest.toml +examples/*.tiff +examples/image_registration_2dpolim.jl *.mp4 *.png @@ -33,4 +35,6 @@ examples/*.ipynb *.gif *.jpg *.txt -*.txt \ No newline at end of file +*.txt +.*tiff +examples/6-4-Rigis.jl diff --git a/Project.toml b/Project.toml index 672a8de..85b929a 100644 --- a/Project.toml +++ b/Project.toml @@ -13,7 +13,7 @@ StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [compat] Aqua = "0.8" FourierTools = "0.4.4" -Interpolations = "0.15.1" +Interpolations = "0.15.1, 0.16.2" StaticArrays = "1.9.6" Test = "1.9.3" Zygote = "0.6.70" diff --git a/examples/PSF_fitting_new b/examples/PSF_fitting_new deleted file mode 100644 index e69de29..0000000 diff --git a/examples/Project.toml b/examples/Project.toml index 1fd1284..5393e21 100644 --- a/examples/Project.toml +++ b/examples/Project.toml @@ -8,7 +8,6 @@ DataToFunctions = "64cfdffa-4d02-49ee-ae8b-a805370874f5" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" EvalMultiPoly = "c78649ec-f9ed-405e-be6d-0472f43586aa" -FindShift = "643ec891-bf64-479f-8088-26ff5ce1b396" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" FourierTools = "b18b359b-aebc-45ac-a139-9c0ccbb2871e" GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a" @@ -21,9 +20,11 @@ LBFGSB = "5be7bae1-8223-5378-bac3-9e7378a2f6e6" LineSearches = "d3d80556-e9d4-5f37-9878-2ab0fcc64255" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" +NDTools = "98581153-e998-4eef-8d0d-5ec2c052313d" Noise = "81d43f40-5267-43b7-ae1c-8b967f377efa" OhMyREPL = "5fb14364-9ced-5910-84b2-373655c76a03" Optim = "429524aa-4258-5aef-a3af-852621145aeb" +PProf = "e4faabce-9ead-11e9-39d9-4379958e3056" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" Pluto = "c3e4b0f8-55cb-11ea-2926-15256bba5781" PlutoUI = "7f904dfe-b85e-4ff6-b463-dae2292396a8" diff --git a/src/transformators.jl b/src/transformators.jl index 2778e6b..a240376 100644 --- a/src/transformators.jl +++ b/src/transformators.jl @@ -175,13 +175,8 @@ applies a general coordinate transformation function to the indices of an array """ function apply_transform(coord_transf_func::Function, data::AbstractArray{T, N}, itp) where {T, N} #, out::AbstractArray{T}) where {T} # @info "Applying tuple transformation" - """ - for it in CartesianIndices(data) - out[it] = idx_apply(itp, coord_transf_func(it)) - end - """ - #res = similar(data); - #return map((it) -> idx_apply(itp, coord_transf_func(Tuple(it))), CartesianIndices(data)) + + # return map((it) -> idx_apply(Interpolations.adapt(gpu_or_cpu(nothing), itp), coord_transf_func(Tuple(it))), CartesianIndices(data)) return idx_apply.(Ref(Interpolations.adapt(gpu_or_cpu(nothing), itp)), coord_transf_func.(Tuple.(CartesianIndices(data)))); #return idx_apply.(Ref(itp), coord_transf_func.(Tuple.(CartesianIndices(data)))); # return idx_apply.(Ref(itp), coord_transf_func.(CartesianIndices(data))); @@ -267,7 +262,7 @@ function get_function_svec(data::AbstractArray{T}, fct_hom::Function; super_samp itp = extrapolate(interpolate(data, interp_type), extrapolation_bc); function interpolated(params::SVector) #, out = similar(data)) fct_hom_noparams(c) = fct_hom(c, params) - return apply_transform_homogen!(fct_hom_noparams, data, itp)#, out); + return apply_transform_homogen(fct_hom_noparams, data, itp)#, out); # return out; end return interpolated @@ -300,7 +295,7 @@ function get_function_affine(data::AbstractArray{T}; super_sampling=2, extrapola # return out; end - function interpolated(p::AbstractVector{T}) where {T} #, out = similar(data)) where T1 + function interpolated(p::AbstractVector{T1}) where {T1} #, out = similar(data)) where T1 x_cen, y_cen = (size(data) .÷ 2.0 .+1) # x_cen_up, y_cen_up = (size(upsampled) .÷ 2.0 .+ 1.0) From fe8ddee9be2003e30e8936b8b1813a5cf7b5fe4b Mon Sep 17 00:00:00 2001 From: RainerHeintzmann Date: Wed, 29 Apr 2026 15:35:38 +0200 Subject: [PATCH 43/44] added example --- Project.toml | 6 ------ src/transformators.jl | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/Project.toml b/Project.toml index 3947f73..d7cc818 100644 --- a/Project.toml +++ b/Project.toml @@ -15,12 +15,6 @@ FourierTools = "0.4" Interpolations = "0.15.1, 0.16" StaticArrays = "1.9.6, 1.10" Aqua = "0.8" -<<<<<<< HEAD -FourierTools = "0.4.4" -Interpolations = "0.15.1, 0.16.2" -StaticArrays = "1.9.6" -======= ->>>>>>> 0f7f69c5355e230f4877e7dfee18647d653e8f57 Test = "1.9.3" Zygote = "0.6.70" julia = "1" diff --git a/src/transformators.jl b/src/transformators.jl index a240376..1aced99 100644 --- a/src/transformators.jl +++ b/src/transformators.jl @@ -282,6 +282,32 @@ The returned function supports two ways to be used, with an affine transform mat `extrapolation_bc`: The extrapolation boundary condition to select for values outside the range. By default the value 0.0 is used. Other options are `Flat()`, or `Line()`, See the package `Interpolation` for details. `interp_type`: The type of interpolation to use. See the package `Interpolation` for details. + +# Example + +```julia +julia> dat1 = reshape(1:16,(4,4)) +4×4 reshape(::UnitRange{Int64}, 4, 4) with eltype Int64: + 1 5 9 13 + 2 6 10 14 + 3 7 11 15 + 4 8 12 16 + +julia> affine_func = get_function_affine(Float32.(dat1)); + +julia> homogeneous_transform = [1 0 -1; 0 1 1; 0 0 1] # translates by [1,1] +3×3 Matrix{Int64}: + 1 0 -1 + 0 1 1 + 0 0 1 + +julia> affine_func(SMatrix{3,3}(homogeneous_transform)) +4×4 Matrix{Float32}: + 0.0 0.0 0.0 0.0 + 5.0 9.0 13.0 0.0 + 6.0 10.0 14.0 0.0 + 7.0 11.0 15.0 0.0 +``` """ function get_function_affine(data::AbstractArray{T}; super_sampling=2, extrapolation_bc=zero(eltype(data)), interp_type=Interpolations.BSpline(Linear())) where T #new_size = super_sampling.*size(data) From 9d2e868e7e5227be2257aee25692f8b4f20e6599 Mon Sep 17 00:00:00 2001 From: hzarei4 Date: Wed, 29 Apr 2026 16:33:04 +0200 Subject: [PATCH 44/44] updated the CI to pass the tests --- .github/workflows/ci.yml | 8 +++++++- docs/Project.toml | 2 ++ docs/src/tutorial.md | 21 --------------------- src/DataToFunctions.jl | 1 + src/datafunction.jl | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 44 insertions(+), 22 deletions(-) create mode 100644 src/datafunction.jl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59c7b92..3a67587 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: fail-fast: false matrix: version: - - '1' + - '1.9' os: - [ubuntu-latest] arch: @@ -34,6 +34,9 @@ jobs: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - uses: julia-actions/cache@v1 + - name: Add unregistered dependencies + run: julia -e 'using Pkg; Pkg.develop(PackageSpec(url="https://github.com/rainerheintzmann/EvalMultiPoly.jl"))' + - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 @@ -52,6 +55,9 @@ jobs: - uses: julia-actions/setup-julia@v1 with: version: "1.9" + - name: Add unregistered dependencies + run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(url="https://github.com/rainerheintzmann/EvalMultiPoly.jl")); Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' + - uses: julia-actions/julia-docdeploy@releases/v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/docs/Project.toml b/docs/Project.toml index efdfaf2..d2cdc05 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -2,6 +2,8 @@ DataToFunctions = "64cfdffa-4d02-49ee-ae8b-a805370874f5" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DocumenterTools = "35a29f4d-8980-5a13-9543-d66fff28ecb8" +EvalMultiPoly = "c78649ec-f9ed-405e-be6d-0472f43586aa" +GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71" Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index bc78b45..f1d87d8 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -72,27 +72,6 @@ heatmap(data_transformed_m, aspect_ratio=1 Now we try to do the transformation using a polynomial function we first define the interpolation object using the DataToFunctions package with a polynomial of order 1 -````@example tutorial -f_polynomial = get_function_poly(data, 1) -```` - -define the polynomial coefficients - -````@example tutorial -params = (1.0, 0.0, 1.0, 0.0, 1.0, 0.0) -```` - -apply the transformation - -````@example tutorial -data_transformed_polynomial = f_polynomial((params)) - -heatmap(data_transformed_polynomial, aspect_ratio=1 - , title="Transformed data using a polynomial function of order 1" - , titlefontsize=10, size=(500, 500) - , xlabel="X", ylabel="Y") -```` - --- *This page was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).* diff --git a/src/DataToFunctions.jl b/src/DataToFunctions.jl index c9ba973..af2b99b 100644 --- a/src/DataToFunctions.jl +++ b/src/DataToFunctions.jl @@ -7,6 +7,7 @@ export gpu_or_cpu # to include CUDA include("requires.jl") +#include("datafunction.jl") include("transformation_types.jl") include("transformators.jl") diff --git a/src/datafunction.jl b/src/datafunction.jl new file mode 100644 index 0000000..d0063c1 --- /dev/null +++ b/src/datafunction.jl @@ -0,0 +1,34 @@ +import Base: getindex, setindex!, size, axes, eltype, copy, similar, ndims, iterate, length + + +struct DataFunctionAffine{affineparams{NTuple{N, Number}}, itp, S<:AbstractArray} <: AbstractArray + params::affineparams + interpolation::itp + data::S +end + +function DataFunctionAffine(params::affineparams, interpolation::itp) where {affineparams<:NTuple{N, Number}, itp} + return DataFunctionAffine{affineparams, itp}(params, interpolation) +end + +function getindex(dfa::DataFunctionAffine, I::Vararg{Number, N}) where {N} + return dfa.interpolation(Tuple(I)...) +end + +function copy(s::DataFunctionAffine) + res = similar(s) + res .= s +end + + +function similar(dfa::DataFunctionAffine, ::Type{T}=eltype(dfa)) where {T} + return DataFunctionAffine(dfa.params, similar(dfa.interpolation, T)) +end + +size(dfa::DataFunctionAffine) = size(dfa.data) +axes(dfa::DataFunctionAffine) = axes(dfa.data) +eltype(dfa::DataFunctionAffine) = eltype(dfa.data) +ndims(dfa::DataFunctionAffine) = ndims(dfa.data) +length(dfa::DataFunctionAffine) = length(dfa.data) + +