From 2b491fff4f322c31c165a15ba29a4d0d852b9557 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 11 Jul 2024 13:04:17 +0200 Subject: [PATCH 01/91] Initial commit --- KomaMRIBase/src/KomaMRIBase.jl | 12 +- KomaMRIBase/src/datatypes/Phantom.jl | 69 +++++--- KomaMRIBase/src/datatypes/phantom/Motion.jl | 120 +++++++++++++ KomaMRIBase/src/datatypes/phantom/NoMotion.jl | 31 ++++ .../phantom/motion/ArbitraryMotion.jl | 164 +++++++----------- .../src/datatypes/phantom/motion/NoMotion.jl | 29 ---- .../datatypes/phantom/motion/SimpleMotion.jl | 94 +--------- .../motion/arbitrarymotion/FlowTrajectory.jl | 7 + .../motion/arbitrarymotion/Trajectory.jl | 6 + .../phantom/motion/simplemotion/HeartBeat.jl | 43 ++--- .../motion/simplemotion/PeriodicHeartBeat.jl | 86 --------- .../motion/simplemotion/PeriodicRotation.jl | 84 --------- .../simplemotion/PeriodicTranslation.jl | 67 ------- .../phantom/motion/simplemotion/Rotation.jl | 49 +++--- .../motion/simplemotion/Translation.jl | 34 ++-- .../phantom/motion/simplemotion/sorting.jl | 41 ----- .../src/timing/{UnitTime.jl => TimeScale.jl} | 49 ++++-- KomaMRIBase/test/runtests.jl | 38 ++-- KomaMRICore/src/KomaMRICore.jl | 3 + .../Bloch/BlochDictSimulationMethod.jl | 1 + .../simulation/Bloch/BlochSimulationMethod.jl | 5 +- KomaMRICore/src/simulation/Flow.jl | 27 +++ KomaMRICore/src/simulation/Functors.jl | 18 +- KomaMRICore/test/runtests.jl | 6 +- KomaMRICore/test/test_files/utils.jl | 9 +- KomaMRIFiles/src/KomaMRIFiles.jl | 1 - KomaMRIFiles/src/Phantom/Phantom.jl | 126 ++++++++------ KomaMRIFiles/test/runtests.jl | 16 +- KomaMRIPlots/src/ui/DisplayFunctions.jl | 20 +-- 29 files changed, 520 insertions(+), 735 deletions(-) create mode 100644 KomaMRIBase/src/datatypes/phantom/Motion.jl create mode 100644 KomaMRIBase/src/datatypes/phantom/NoMotion.jl delete mode 100644 KomaMRIBase/src/datatypes/phantom/motion/NoMotion.jl create mode 100644 KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/FlowTrajectory.jl create mode 100644 KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/Trajectory.jl delete mode 100644 KomaMRIBase/src/datatypes/phantom/motion/simplemotion/PeriodicHeartBeat.jl delete mode 100644 KomaMRIBase/src/datatypes/phantom/motion/simplemotion/PeriodicRotation.jl delete mode 100644 KomaMRIBase/src/datatypes/phantom/motion/simplemotion/PeriodicTranslation.jl delete mode 100644 KomaMRIBase/src/datatypes/phantom/motion/simplemotion/sorting.jl rename KomaMRIBase/src/timing/{UnitTime.jl => TimeScale.jl} (54%) create mode 100644 KomaMRICore/src/simulation/Flow.jl diff --git a/KomaMRIBase/src/KomaMRIBase.jl b/KomaMRIBase/src/KomaMRIBase.jl index 2e15501e3..21f835930 100644 --- a/KomaMRIBase/src/KomaMRIBase.jl +++ b/KomaMRIBase/src/KomaMRIBase.jl @@ -31,7 +31,6 @@ include("datatypes/Phantom.jl") include("datatypes/simulation/DiscreteSequence.jl") include("timing/TimeStepCalculation.jl") include("timing/TrapezoidalIntegration.jl") -include("timing/UnitTime.jl") # Main export γ # gyro-magnetic ratio [Hz/T] @@ -47,12 +46,11 @@ export kfoldperm, trapz, cumtrapz # Phantom export brain_phantom2D, brain_phantom3D, pelvis_phantom2D, heart_phantom # Motion -export MotionModel -export NoMotion, SimpleMotion, ArbitraryMotion -export SimpleMotionType -export Translation, Rotation, HeartBeat -export PeriodicTranslation, PeriodicRotation, PeriodicHeartBeat -export get_spin_coords +export AbstractMotion, Motion, MotionVector, NoMotion +export SimpleMotion, ArbitraryMotion +export Static, Translation, Rotation, HeartBeat, Trajectory, FlowTrajectory +export TimeScale, TimeRange, Periodic +export sort_motions!, get_spin_coords # Secondary export get_kspace, rotx, roty, rotz # Additionals diff --git a/KomaMRIBase/src/datatypes/Phantom.jl b/KomaMRIBase/src/datatypes/Phantom.jl index 8223084f2..4de25d2df 100644 --- a/KomaMRIBase/src/datatypes/Phantom.jl +++ b/KomaMRIBase/src/datatypes/Phantom.jl @@ -1,9 +1,9 @@ -abstract type MotionModel{T<:Real} end - -#Motion models: -include("phantom/motion/SimpleMotion.jl") -include("phantom/motion/ArbitraryMotion.jl") -include("phantom/motion/NoMotion.jl") +# TimeScale: +include("../timing/TimeScale.jl") +# Motion: +abstract type AbstractMotion{T<:Real} end +include("phantom/Motion.jl") +include("phantom/NoMotion.jl") """ obj = Phantom(name, x, y, z, ρ, T1, T2, T2s, Δw, Dλ1, Dλ2, Dθ, motion) @@ -54,7 +54,7 @@ julia> obj.ρ Dθ::AbstractVector{T} = zeros(eltype(x), size(x)) #Diff::Vector{DiffusionModel} #Diffusion map #Motion - motion::MotionModel{T} = NoMotion{eltype(x)}() + motion::AbstractMotion{T} = NoMotion{eltype(x)}() end """Size and length of a phantom""" @@ -67,18 +67,29 @@ Base.lastindex(x::Phantom) = length(x) Base.getindex(x::Phantom, i::Integer) = x[i:i] """Compare two phantoms""" -Base.:(==)(obj1::Phantom, obj2::Phantom) = reduce( - &, - [getfield(obj1, field) == getfield(obj2, field) for field in Iterators.filter(x -> !(x == :name), fieldnames(Phantom))], -) -Base.:(≈)(obj1::Phantom, obj2::Phantom) = reduce(&, [getfield(obj1, field) ≈ getfield(obj2, field) for field in Iterators.filter(x -> !(x == :name), fieldnames(Phantom))]) -Base.:(==)(m1::MotionModel, m2::MotionModel) = false -Base.:(≈)(m1::MotionModel, m2::MotionModel) = false +function Base.:(==)(obj1::Phantom, obj2::Phantom) + return reduce( + &, + [ + getfield(obj1, field) == getfield(obj2, field) for + field in Iterators.filter(x -> !(x == :name), fieldnames(Phantom)) + ], + ) +end +function Base.:(≈)(obj1::Phantom, obj2::Phantom) + return reduce( + &, + [ + getfield(obj1, field) ≈ getfield(obj2, field) for + field in Iterators.filter(x -> !(x == :name), fieldnames(Phantom)) + ], + ) +end """Separate object spins in a sub-group""" Base.getindex(obj::Phantom, p::Union{AbstractRange,AbstractVector,Colon}) = begin fields = [] - for field in Iterators.filter(x -> !(x == :name), fieldnames(Phantom)) + for field in Iterators.filter(x -> x != :name, fieldnames(Phantom)) push!(fields, (field, getfield(obj, field)[p])) end return Phantom(; name=obj.name, fields...) @@ -87,7 +98,7 @@ end """Separate object spins in a sub-group (lightweigth).""" Base.view(obj::Phantom, p::Union{AbstractRange,AbstractVector,Colon}) = begin fields = [] - for field in Iterators.filter(x -> !(x == :name), fieldnames(Phantom)) + for field in Iterators.filter(x -> x != :name, fieldnames(Phantom)) push!(fields, (field, @view(getfield(obj, field)[p]))) end return Phantom(; name=obj.name, fields...) @@ -95,13 +106,17 @@ end """Addition of phantoms""" +(obj1::Phantom, obj2::Phantom) = begin + Nmaxchars = 50 + name = first(obj1.name * "+" * obj2.name, Nmaxchars) fields = [] - for field in Iterators.filter(x -> !(x == :name), fieldnames(Phantom)) + for field in Iterators.filter(x -> x != :name, fieldnames(Phantom)) push!(fields, (field, [getfield(obj1, field); getfield(obj2, field)])) end - Nmaxchars = 50 - name = first(obj1.name * "+" * obj2.name, Nmaxchars) - return Phantom(; name=name, fields...) + return Phantom(; + name=name, + fields..., + motion=vcat(obj1.motion, obj2.motion, length(obj1), length(obj2)) + ) end """Scalar multiplication of a phantom""" @@ -177,16 +192,18 @@ function heart_phantom( Dλ1=Dλ1[ρ .!= 0], Dλ2=Dλ2[ρ .!= 0], Dθ=Dθ[ρ .!= 0], - motion=SimpleMotion( - PeriodicHeartBeat(; - period=period, - asymmetry=asymmetry, + motion=MotionVector( + HeartBeat(; + times=Periodic(; period=period, asymmetry=asymmetry), circumferential_strain=circumferential_strain, radial_strain=radial_strain, longitudinal_strain=0.0, ), - PeriodicRotation(; - period=period, asymmetry=asymmetry, yaw=rotation_angle, pitch=0.0, roll=0.0 + Rotation(; + times=Periodic(; period=period, asymmetry=asymmetry), + yaw=rotation_angle, + pitch=0.0, + roll=0.0, ), ), ) diff --git a/KomaMRIBase/src/datatypes/phantom/Motion.jl b/KomaMRIBase/src/datatypes/phantom/Motion.jl new file mode 100644 index 000000000..eafde6d02 --- /dev/null +++ b/KomaMRIBase/src/datatypes/phantom/Motion.jl @@ -0,0 +1,120 @@ +abstract type Motion{T<:Real} end + +is_composable(m::Motion) = false + +struct MotionVector{T<:Real} <: AbstractMotion{T} + motions::Vector{<:Motion{T}} +end + +MotionVector(motions...) = length([motions]) > 0 ? MotionVector([motions...]) : @error "You must provide at least one motion as input argument. If you do not want to define motion, use `NoMotion{T}()`" + +include("motion/SimpleMotion.jl") +include("motion/ArbitraryMotion.jl") + +Base.getindex(mv::MotionVector, p::Union{AbstractRange, AbstractVector, Colon, Integer}) = MotionVector(getindex.(mv.motions, Ref(p))) +Base.view(mv::MotionVector, p::Union{AbstractRange, AbstractVector, Colon, Integer}) = MotionVector(view.(mv.motions, Ref(p))) + +""" Addition of MotionVectors """ +function Base.vcat(m1::MotionVector{T}, m2::MotionVector{T}, Ns1::Int, Ns2::Int) where {T<:Real} + mv1 = m1.motions + mv1_aux = Motion{T}[] + for i in 1:length(mv1) + if typeof(mv1[i]) <: ArbitraryMotion + zeros1 = similar(mv1[i].dx, Ns2, size(mv1[i].dx, 2)) + zeros1 .= zero(T) + push!(mv1_aux, typeof(mv1[i])(mv1[i].times, [[getfield(mv1[i], d); zeros1] for d in filter(x -> x != :times, fieldnames(typeof(mv1[i])))]...)) + else + push!(mv1_aux, mv1[i]) + end + end + mv2 = m2.motions + mv2_aux = Motion{T}[] + for i in 1:length(mv2) + if typeof(mv2[i]) <: ArbitraryMotion + zeros2 = similar(mv2[i].dx, Ns1, size(mv2[i].dx, 2)) + zeros2 .= zero(T) + push!(mv2_aux, typeof(mv2[i])(mv2[i].times, [[zeros2; getfield(mv2[i], d)] for d in filter(x -> x != :times, fieldnames(typeof(mv2[i])))]...)) + else + push!(mv2_aux, mv2[i]) + end + end + return MotionVector([mv1_aux; mv2_aux]) +end + +""" Compare two motion vectors """ +function Base.:(==)(mv1::MotionVector{T}, mv2::MotionVector{T}) where {T<:Real} + sort_motions!(mv1) + sort_motions!(mv2) + return reduce(&, mv1.motions .== mv2.motions) +end +function Base.:(≈)(mv1::MotionVector{T}, mv2::MotionVector{T}) where {T<:Real} + sort_motions!(mv1) + sort_motions!(mv2) + return reduce(&, mv1.motions .≈ mv2.motions) +end + +""" + x, y, z = get_spin_coords(motion, x, y, z, t) + +Calculates the position of each spin at a set of arbitrary time instants, i.e. the time steps of the simulation. +For each dimension (x, y, z), the output matrix has ``N_{\text{spins}}`` rows and `length(t)` columns. + +# Arguments +- `motion`: (`::Vector{<:Motion{T<:Real}}`) phantom motion +- `x`: (`::AbstractVector{T<:Real}`, `[m]`) spin x-position vector +- `y`: (`::AbstractVector{T<:Real}`, `[m]`) spin y-position vector +- `z`: (`::AbstractVector{T<:Real}`, `[m]`) spin z-position vector +- `t`: (`::AbstractArray{T<:Real}`) horizontal array of time instants + +# Returns +- `x, y, z`: (`::Tuple{AbstractArray, AbstractArray, AbstractArray}`) spin positions over time +""" +function get_spin_coords( + mv::MotionVector{T}, + x::AbstractVector{T}, + y::AbstractVector{T}, + z::AbstractVector{T}, + t::AbstractArray{T} +) where {T<:Real} + # Buffers for positions: + xt, yt, zt = x .+ 0*t, y .+ 0*t, z .+ 0*t + # Buffers for displacements: + ux, uy, uz = similar(xt), similar(yt), similar(zt) + + # Composable motions: they need to be run sequentially + for m in Iterators.filter(is_composable, mv.motions) + displacement_x!(ux, m, xt, yt, zt, t) + displacement_y!(uy, m, xt, yt, zt, t) + displacement_z!(uz, m, xt, yt, zt, t) + xt .+= ux + yt .+= uy + zt .+= uz + end + # Additive motions: these motions can be run in parallel + for m in Iterators.filter(!is_composable, mv.motions) + displacement_x!(ux, m, x, y, z, t) + displacement_y!(uy, m, x, y, z, t) + displacement_z!(uz, m, x, y, z, t) + xt .+= ux + yt .+= uy + zt .+= uz + end + return xt, yt, zt +end + +""" + times = times(motion) +""" +times(m::Motion) = times(m.times) +times(mv::MotionVector{T}) where {T<:Real} = begin + nodes = reduce(vcat, [times(m) for m in mv.motions]; init=[zero(T)]) + return unique(sort(nodes)) +end + +""" + sort_motions! +""" +function sort_motions!(mv::MotionVector{T}) where {T<:Real} + sort!(mv.motions; by=m -> times(m)[1]) + return nothing +end \ No newline at end of file diff --git a/KomaMRIBase/src/datatypes/phantom/NoMotion.jl b/KomaMRIBase/src/datatypes/phantom/NoMotion.jl new file mode 100644 index 000000000..09f7774c8 --- /dev/null +++ b/KomaMRIBase/src/datatypes/phantom/NoMotion.jl @@ -0,0 +1,31 @@ +struct NoMotion{T<:Real} <: AbstractMotion{T} end + +Base.getindex(mv::NoMotion, p::Union{AbstractRange, AbstractVector, Colon, Integer}) = mv +Base.view(mv::NoMotion, p::Union{AbstractRange, AbstractVector, Colon, Integer}) = mv + +""" Addition of NoMotions """ +Base.vcat(m1::NoMotion{T}, m2::AbstractMotion{T}, Ns1::Int, Ns2::Int) where {T<:Real} = m2 +Base.vcat(m1::AbstractMotion{T}, m2::NoMotion{T}, Ns1::Int, Ns2::Int) where {T<:Real} = m1 + +Base.:(==)(m1::NoMotion{T}, m2::NoMotion{T}) where {T<:Real} = true +Base.:(≈)(m1::NoMotion{T}, m2::NoMotion{T}) where {T<:Real} = true + +function get_spin_coords( + mv::NoMotion{T}, + x::AbstractVector{T}, + y::AbstractVector{T}, + z::AbstractVector{T}, + t::AbstractArray{T} +) where {T<:Real} + return x, y, z +end + +""" + times = times(motion) +""" +times(mv::NoMotion{T}) where {T<:Real} = [zero(T)] + +""" + sort_motions! +""" +sort_motions!(mv::NoMotion) = nothing \ No newline at end of file diff --git a/KomaMRIBase/src/datatypes/phantom/motion/ArbitraryMotion.jl b/KomaMRIBase/src/datatypes/phantom/motion/ArbitraryMotion.jl index 1fa752185..8b6a2bb53 100644 --- a/KomaMRIBase/src/datatypes/phantom/motion/ArbitraryMotion.jl +++ b/KomaMRIBase/src/datatypes/phantom/motion/ArbitraryMotion.jl @@ -1,14 +1,9 @@ -# TODO: Consider different Extrapolations apart from periodic LinerInterpolator{T,ETPType} -# Interpolator{T,Degree,ETPType}, -# Degree = Linear,Cubic.... -# ETPType = Periodic, Flat... - const Interpolator1D = Interpolations.GriddedInterpolation{ T,1,V,Itp,K } where { T<:Real, - V<:AbstractArray{T}, - Itp<:Interpolations.Gridded{Linear{Throw{OnGrid}}}, + V<:AbstractArray{<:Union{T,Bool}}, + Itp<:Interpolations.Gridded, K<:Tuple{AbstractVector{T}}, } @@ -16,121 +11,92 @@ const Interpolator2D = Interpolations.GriddedInterpolation{ T,2,V,Itp,K } where { T<:Real, - V<:AbstractArray{T}, - Itp<:Interpolations.Gridded{Linear{Throw{OnGrid}}}, + V<:AbstractArray{<:Union{T,Bool}}, + Itp<:Interpolations.Gridded, K<:Tuple{AbstractVector{T}, AbstractVector{T}}, } """ - motion = ArbitraryMotion(t_start, t_end, dx, dy, dz) - -ArbitraryMotion model. For this motion model, it is necessary to define -motion for each spin independently, in x (`dx`), y (`dy`) and z (`dz`). -`dx`, `dy` and `dz` are three matrixes, of (``N_{spins}`` x ``N_{discrete\\,times}``) each. -This means that each row corresponds to a spin trajectory over a set of discrete time instants. -The motion duration is determined by `t_start` and `t_end`. -The discrete time instants are evenly spaced, (``dt = \frac{t_{end} - t_{start}}{N_{discrete\\,times}}``). - -This motion model is useful for defining arbitrarly complex motion, specially -for importing the spin trajectories from another source, like XCAT or a CFD. - -# Arguments -- `t_start`: (`::T`, `[s]`) -- `t_end`: (`::T`, `[s]`) -- `dx`: (`::Array{T,2}`, `[m]`) matrix for displacements in x -- `dy`: (`::Array{T,2}`, `[m]`) matrix for displacements in y -- `dz`: (`::Array{T,2}`, `[m]`) matrix for displacements in z - -# Returns -- `motion`: (`::ArbitraryMotion`) ArbitraryMotion struct - -# Examples -```julia-repl -julia> motion = ArbitraryMotion( - 0.0, - 1.0, - 0.01.*rand(1000, 10), - 0.01.*rand(1000, 10), - 0.01.*rand(1000, 10) - ) -``` + ArbitraryMotion """ -struct ArbitraryMotion{T} <: MotionModel{T} - t_start::T - t_end::T - dx::AbstractArray{T} - dy::AbstractArray{T} - dz::AbstractArray{T} -end +abstract type ArbitraryMotion{T<:Real} <: Motion{T} end -function Base.getindex( - motion::ArbitraryMotion, p::Union{AbstractRange,AbstractVector,Colon} -) - return ArbitraryMotion(motion.t_start, motion.t_end, motion.dx[p,:], motion.dy[p,:], motion.dz[p,:]) +function Base.getindex(motion::ArbitraryMotion, p::Union{AbstractRange, AbstractVector, Colon, Integer}) + return typeof(motion)(motion.times, [getfield(motion, d)[p,:] for d in filter(x -> x != :times, fieldnames(typeof(motion)))]...) end -function Base.view( - motion::ArbitraryMotion, p::Union{AbstractRange,AbstractVector,Colon} -) - return ArbitraryMotion(motion.t_start, motion.t_end, @view(motion.dx[p,:]), @view(motion.dy[p,:]), @view(motion.dz[p,:])) +function Base.view(motion::ArbitraryMotion, p::Union{AbstractRange, AbstractVector, Colon, Integer}) + return typeof(motion)(motion.times, [@view(getfield(motion, d)[p,:]) for d in filter(x -> x != :times, fieldnames(typeof(motion)))]...) end -Base.:(==)(m1::ArbitraryMotion, m2::ArbitraryMotion) = reduce(&, [getfield(m1, field) == getfield(m2, field) for field in fieldnames(ArbitraryMotion)]) -Base.:(≈)(m1::ArbitraryMotion, m2::ArbitraryMotion) = reduce(&, [getfield(m1, field) ≈ getfield(m2, field) for field in fieldnames(ArbitraryMotion)]) +Base.:(==)(m1::ArbitraryMotion, m2::ArbitraryMotion) = (typeof(m1) == typeof(m2)) & reduce(&, [getfield(m1, field) == getfield(m2, field) for field in fieldnames(typeof(m1))]) +Base.:(≈)(m1::ArbitraryMotion, m2::ArbitraryMotion) = (typeof(m1) == typeof(m2)) & reduce(&, [getfield(m1, field) ≈ getfield(m2, field) for field in fieldnames(typeof(m1))]) -function Base.vcat(m1::ArbitraryMotion, m2::ArbitraryMotion) - @assert (m1.t_start == m2.t_start) && (m1.t_end == m2.t_end) "Starting and ending times must be the same" - return ArbitraryMotion(m1.t_start, m1.t_end, [m1.dx; m2.dx], [m1.dy; m2.dy], [m1.dz; m2.dz]) +function GriddedInterpolation(nodes, A, ITP) + return Interpolations.GriddedInterpolation{eltype(A), length(nodes), typeof(A), typeof(ITP), typeof(nodes)}(nodes, A, ITP) end -""" - limits = times(obj.motion) -""" -function times(motion::ArbitraryMotion) - return range(motion.t_start, motion.t_end, length=size(motion.dx, 2)) +function interpolate(d::AbstractArray{T}, ITPType, Ns::Val{1}) where {T<:Real} + _, Nt = size(d) + t = similar(d, Nt); copyto!(t, collect(range(zero(T), oneunit(T), Nt))) + return GriddedInterpolation((t, ), d[:], ITPType) end -function GriddedInterpolation(nodes, A, ITP) - return Interpolations.GriddedInterpolation{eltype(A), length(nodes), typeof(A), typeof(ITP), typeof(nodes)}(nodes, A, ITP) +function interpolate(d::AbstractArray{T}, ITPType, Ns::Val) where {T<:Real} + Ns, Nt = size(d) + id = similar(d, Ns); copyto!(id, collect(range(oneunit(T), T(Ns), Ns))) + t = similar(d, Nt); copyto!(t, collect(range(zero(T), oneunit(T), Nt))) + return GriddedInterpolation((id, t), d, ITPType) end -function interpolate(motion::ArbitraryMotion{T}, Ns::Val{1}) where {T<:Real} - _, Nt = size(motion.dx) - t = similar(motion.dx, Nt); copyto!(t, collect(range(zero(T), oneunit(T), Nt))) - itpx = GriddedInterpolation((t, ), motion.dx[:], Gridded(Linear())) - itpy = GriddedInterpolation((t, ), motion.dy[:], Gridded(Linear())) - itpz = GriddedInterpolation((t, ), motion.dz[:], Gridded(Linear())) - return itpx, itpy, itpz +function resample(itp::Interpolator1D{T}, t::AbstractArray{T}) where {T<:Real} + return itp.(t) end -function interpolate(motion::ArbitraryMotion{T}, Ns::Val) where {T<:Real} - Ns, Nt = size(motion.dx) - id = similar(motion.dx, Ns); copyto!(id, collect(range(oneunit(T), T(Ns), Ns))) - t = similar(motion.dx, Nt); copyto!(t, collect(range(zero(T), oneunit(T), Nt))) - itpx = GriddedInterpolation((id, t), motion.dx, Gridded(Linear())) - itpy = GriddedInterpolation((id, t), motion.dy, Gridded(Linear())) - itpz = GriddedInterpolation((id, t), motion.dz, Gridded(Linear())) - return itpx, itpy, itpz +function resample(itp::Interpolator2D{T}, t::AbstractArray{T}) where {T<:Real} + Ns = size(itp.coefs, 1) + id = similar(itp.coefs, Ns) + copyto!(id, collect(range(oneunit(T), T(Ns), Ns))) + return itp.(id, t) end -function resample(itpx::Interpolator1D{T}, itpy::Interpolator1D{T}, itpz::Interpolator1D{T}, t::AbstractArray{T}) where {T<:Real} - return itpx.(t), itpy.(t), itpz.(t) +function displacement_x!( + ux::AbstractArray{T}, + motion::ArbitraryMotion{T}, + x::AbstractArray{T}, + y::AbstractArray{T}, + z::AbstractArray{T}, + t::AbstractArray{T}, +) where {T<:Real} + itp = interpolate(motion.dx, Gridded(Linear()), Val(size(x,1))) + ux .= resample(itp, unit_time(t, motion.times)) + return nothing end -function resample(itpx::Interpolator2D{T}, itpy::Interpolator2D{T}, itpz::Interpolator2D{T}, t::AbstractArray{T}) where {T<:Real} - Ns = size(itpx.coefs, 1) - id = similar(itpx.coefs, Ns) - copyto!(id, collect(range(oneunit(T), T(Ns), Ns))) - return itpx.(id, t), itpy.(id, t), itpz.(id, t) +function displacement_y!( + uy::AbstractArray{T}, + motion::ArbitraryMotion{T}, + x::AbstractArray{T}, + y::AbstractArray{T}, + z::AbstractArray{T}, + t::AbstractArray{T}, +) where {T<:Real} + itp = interpolate(motion.dy, Gridded(Linear()), Val(size(x,1))) + uy .= resample(itp, unit_time(t, motion.times)) + return nothing end -function get_spin_coords( +function displacement_z!( + uz::AbstractArray{T}, motion::ArbitraryMotion{T}, - x::AbstractVector{T}, - y::AbstractVector{T}, - z::AbstractVector{T}, - t::AbstractArray{T} + x::AbstractArray{T}, + y::AbstractArray{T}, + z::AbstractArray{T}, + t::AbstractArray{T}, ) where {T<:Real} - motion_functions = interpolate(motion, Val(size(x,1))) - ux, uy, uz = resample(motion_functions..., unit_time(t, motion.t_start, motion.t_end)) - return x .+ ux, y .+ uy, z .+ uz -end \ No newline at end of file + itp = interpolate(motion.dz, Gridded(Linear()), Val(size(x,1))) + uz .= resample(itp, unit_time(t, motion.times)) + return nothing +end + +include("arbitrarymotion/Trajectory.jl") +include("arbitrarymotion/FlowTrajectory.jl") \ No newline at end of file diff --git a/KomaMRIBase/src/datatypes/phantom/motion/NoMotion.jl b/KomaMRIBase/src/datatypes/phantom/motion/NoMotion.jl deleted file mode 100644 index 4a6895f76..000000000 --- a/KomaMRIBase/src/datatypes/phantom/motion/NoMotion.jl +++ /dev/null @@ -1,29 +0,0 @@ -""" -No Motion - -x = x -""" - -struct NoMotion{T<:Real} <: MotionModel{T} end - -Base.getindex(motion::NoMotion, p::Union{AbstractRange,AbstractVector,Colon}) = motion -Base.view(motion::NoMotion, p::Union{AbstractRange,AbstractVector,Colon}) = motion - -Base.:(==)(m1::NoMotion, m2::NoMotion) = true -Base.:(≈)(m1::NoMotion, m2::NoMotion) = true - -Base.vcat(m1::NoMotion{T}, m2::NoMotion{T}) where {T<:Real} = NoMotion{T}() - -function get_spin_coords( - motion::NoMotion, - x::AbstractVector{T}, - y::AbstractVector{T}, - z::AbstractVector{T}, - t::AbstractArray{T} -) where {T<:Real} - return x, y, z -end - -function times(motion::NoMotion) - return [0.0] -end \ No newline at end of file diff --git a/KomaMRIBase/src/datatypes/phantom/motion/SimpleMotion.jl b/KomaMRIBase/src/datatypes/phantom/motion/SimpleMotion.jl index b1ba5bd30..bebd4ad06 100644 --- a/KomaMRIBase/src/datatypes/phantom/motion/SimpleMotion.jl +++ b/KomaMRIBase/src/datatypes/phantom/motion/SimpleMotion.jl @@ -1,8 +1,3 @@ -# ------ SimpleMotionType -abstract type SimpleMotionType{T<:Real} end - -is_composable(motion_type::SimpleMotionType{T}) where {T<:Real} = false - """ motion = SimpleMotion(types) @@ -26,92 +21,11 @@ julia> motion = SimpleMotion( ) ``` """ -struct SimpleMotion{T<:Real} <: MotionModel{T} - types::Tuple{Vararg{<:SimpleMotionType{T}}} -end - -SimpleMotion(types...) = SimpleMotion(types) - -Base.getindex(motion::SimpleMotion, p::Union{AbstractRange,AbstractVector,Colon}) = motion -Base.view(motion::SimpleMotion, p::Union{AbstractRange,AbstractVector,Colon}) = motion - -Base.vcat(m1::SimpleMotion, m2::SimpleMotion) = SimpleMotion(m1.types..., m2.types...) +abstract type SimpleMotion{T<:Real} <: Motion{T} end -Base.:(==)(m1::SimpleMotion, m2::SimpleMotion) = reduce(&, [m1.types[i] == m2.types[i] for i in 1:length(m1.types)]) -Base.:(≈)(m1::SimpleMotion, m2::SimpleMotion) = reduce(&, [m1.types[i] ≈ m2.types[i] for i in 1:length(m1.types)]) -# When they are the same type -Base.:(==)(t1::T, t2::T) where {T<:SimpleMotionType} = reduce(&, [getfield(t1, field) == getfield(t2, field) for field in fieldnames(T)]) -Base.:(≈)(t1::T, t2::T) where {T<:SimpleMotionType} = reduce(&, [getfield(t1, field) ≈ getfield(t2, field) for field in fieldnames(T)]) -# When they are not (default) -Base.:(==)(t1::SimpleMotionType, t2::SimpleMotionType) = false -Base.:(≈)(t1::SimpleMotionType, t2::SimpleMotionType) = false +Base.getindex(motion::SimpleMotion, p::Union{AbstractRange, AbstractVector, Colon, Integer}) = motion +Base.view(motion::SimpleMotion, p::Union{AbstractRange, AbstractVector, Colon, Integer}) = motion -""" - x, y, z = get_spin_coords(motion, x, y, z, t) - -Calculates the position of each spin at a set of arbitrary time instants, i.e. the time steps of the simulation. -For each dimension (x, y, z), the output matrix has ``N_{\text{spins}}`` rows and `length(t)` columns. - -# Arguments -- `motion`: (`::MotionModel`) phantom motion -- `x`: (`::AbstractVector{T<:Real}`, `[m]`) spin x-position vector -- `y`: (`::AbstractVector{T<:Real}`, `[m]`) spin y-position vector -- `z`: (`::AbstractVector{T<:Real}`, `[m]`) spin z-position vector -- `t`: (`::AbstractArray{T<:Real}`) horizontal array of time instants - -# Returns -- `x, y, z`: (`::Tuple{AbstractArray, AbstractArray, AbstractArray}`) spin positions over time -""" -function get_spin_coords( - motion::SimpleMotion{T}, - x::AbstractVector{T}, - y::AbstractVector{T}, - z::AbstractVector{T}, - t::AbstractArray{T} -) where {T<:Real} - motion = sort_motion(motion) - # Buffers for positions: - xt, yt, zt = x .+ 0*t, y .+ 0*t, z .+ 0*t - # Buffers for displacements: - ux, uy, uz = similar(xt), similar(yt), similar(zt) - - # Composable motions: they need to be run sequentially - for motion in Iterators.filter(is_composable, motion.types) - displacement_x!(ux, motion, xt, yt, zt, t) - displacement_y!(uy, motion, xt, yt, zt, t) - displacement_z!(uz, motion, xt, yt, zt, t) - xt .+= ux - yt .+= uy - zt .+= uz - end - # Additive motions: these motions can be run in parallel - for motion in Iterators.filter(!is_composable, motion.types) - displacement_x!(ux, motion, x, y, z, t) - displacement_y!(uy, motion, x, y, z, t) - displacement_z!(uz, motion, x, y, z, t) - xt .+= ux - yt .+= uy - zt .+= uz - end - return xt, yt, zt -end - -function times(motion::SimpleMotion) - nodes = reduce(vcat, [times(type) for type in motion.types]) - nodes = unique(sort(nodes)) - return nodes -end - -# Sort Motion -include("simplemotion/sorting.jl") - -# Simple Motion Types: -# Non-periodic types: defined by an initial time (t_start), an end time (t_end) and a displacement include("simplemotion/Translation.jl") include("simplemotion/Rotation.jl") -include("simplemotion/HeartBeat.jl") -# Periodic types: defined by the period, the temporal symmetry and a displacement (amplitude) -include("simplemotion/PeriodicTranslation.jl") -include("simplemotion/PeriodicRotation.jl") -include("simplemotion/PeriodicHeartBeat.jl") - +include("simplemotion/HeartBeat.jl") \ No newline at end of file diff --git a/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/FlowTrajectory.jl b/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/FlowTrajectory.jl new file mode 100644 index 000000000..56d08fb3a --- /dev/null +++ b/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/FlowTrajectory.jl @@ -0,0 +1,7 @@ +struct FlowTrajectory{T<:Real, TS<:TimeScale{T}} <: ArbitraryMotion{T} + times::TS + dx::AbstractArray{T} + dy::AbstractArray{T} + dz::AbstractArray{T} + resetmag::AbstractArray{Bool} +end \ No newline at end of file diff --git a/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/Trajectory.jl b/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/Trajectory.jl new file mode 100644 index 000000000..0aae0b094 --- /dev/null +++ b/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/Trajectory.jl @@ -0,0 +1,6 @@ +struct Trajectory{T<:Real, TS<:TimeScale{T}} <: ArbitraryMotion{T} + times::TS + dx::AbstractArray{T} + dy::AbstractArray{T} + dz::AbstractArray{T} +end \ No newline at end of file diff --git a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/HeartBeat.jl b/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/HeartBeat.jl index c2b8d5181..09a8146c4 100644 --- a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/HeartBeat.jl +++ b/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/HeartBeat.jl @@ -1,48 +1,45 @@ @doc raw""" - heartbeat = HeartBeat(circumferential_strain, radial_strain, longitudinal_strain, t_start, t_end) + heartbeat = HeartBeat(times, circumferential_strain, radial_strain, longitudinal_strain) HeartBeat struct. It produces a heartbeat-like motion, characterised by three types of strain: Circumferential, Radial and Longitudinal # Arguments +- `times`: (`::TimeScale{T<:Real}`, `[s]`) time scale - `circumferential_strain`: (`::Real`) contraction parameter - `radial_strain`: (`::Real`) contraction parameter - `longitudinal_strain`: (`::Real`) contraction parameter -- `t_start`: (`::Real`, `[s]`) initial time -- `t_end`: (`::Real`, `[s]`) final time # Returns - `heartbeat`: (`::HeartBeat`) HeartBeat struct # Examples ```julia-repl -julia> hb = HeartBeat(circumferential_strain=-0.3, radial_strain=-0.2, longitudinal_strain=0.0, t_start=0.2, t_end=0.5) +julia> hb = HeartBeat(times=Periodic(period=1.0, asymmetry=0.3), circumferential_strain=-0.3, radial_strain=-0.2, longitudinal_strain=0.0) ``` """ -@with_kw struct HeartBeat{T<:Real} <: SimpleMotionType{T} +@with_kw struct HeartBeat{T<:Real, TS<:TimeScale{T}} <: SimpleMotion{T} + times :: TS circumferential_strain :: T radial_strain :: T - longitudinal_strain::T = typeof(circumferential_strain)(0.0) - t_start::T = typeof(circumferential_strain)(0.0) - t_end::T = typeof(circumferential_strain)(0.0) - @assert t_end >= t_start "t_end must be greater or equal than t_start" + longitudinal_strain :: T = typeof(circumferential_strain)(0.0) end -is_composable(motion_type::HeartBeat) = true +is_composable(motion::HeartBeat) = true function displacement_x!( ux::AbstractArray{T}, - motion_type::HeartBeat{T}, + motion::HeartBeat{T}, x::AbstractArray{T}, y::AbstractArray{T}, z::AbstractArray{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion_type.t_start, motion_type.t_end) + t_unit = unit_time(t, motion.times) r = sqrt.(x .^ 2 + y .^ 2) θ = atan.(y, x) - Δ_circunferential = motion_type.circumferential_strain * maximum(r) - Δ_radial = -motion_type.radial_strain * (maximum(r) .- r) + Δ_circunferential = motion.circumferential_strain * maximum(r) + Δ_radial = -motion.radial_strain * (maximum(r) .- r) Δr = t_unit .* (Δ_circunferential .+ Δ_radial) # Map negative radius to r=0 neg = (r .+ Δr) .< 0 @@ -54,17 +51,17 @@ end function displacement_y!( uy::AbstractArray{T}, - motion_type::HeartBeat{T}, + motion::HeartBeat{T}, x::AbstractArray{T}, y::AbstractArray{T}, z::AbstractArray{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion_type.t_start, motion_type.t_end) + t_unit = unit_time(t, motion.times) r = sqrt.(x .^ 2 + y .^ 2) θ = atan.(y, x) - Δ_circunferential = motion_type.circumferential_strain * maximum(r) - Δ_radial = -motion_type.radial_strain * (maximum(r) .- r) + Δ_circunferential = motion.circumferential_strain * maximum(r) + Δ_radial = -motion.radial_strain * (maximum(r) .- r) Δr = t_unit .* (Δ_circunferential .+ Δ_radial) # Map negative radius to r=0 neg = (r .+ Δr) .< 0 @@ -76,17 +73,13 @@ end function displacement_z!( uz::AbstractArray{T}, - motion_type::HeartBeat{T}, + motion::HeartBeat{T}, x::AbstractArray{T}, y::AbstractArray{T}, z::AbstractArray{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion_type.t_start, motion_type.t_end) - uz .= t_unit .* (z .* motion_type.longitudinal_strain) + t_unit = unit_time(t, motion.times) + uz .= t_unit .* (z .* motion.longitudinal_strain) return nothing -end - -times(motion_type::HeartBeat) = begin - return [motion_type.t_start, motion_type.t_end] end \ No newline at end of file diff --git a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/PeriodicHeartBeat.jl b/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/PeriodicHeartBeat.jl deleted file mode 100644 index 2a9c02537..000000000 --- a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/PeriodicHeartBeat.jl +++ /dev/null @@ -1,86 +0,0 @@ -@doc raw""" - periodic_heartbeat = PeriodicHeartBeat(circumferential_strain, radial_strain, longitudinal_strain, period, asymmetry) - -HeartBeat struct. It produces a heartbeat-like motion, characterised by three types of strain: -Circumferential, Radial and Longitudinal - -# Arguments -- `circumferential_strain`: (`::Real`) contraction parameter -- `radial_strain`: (`::Real`) contraction parameter -- `longitudinal_strain`: (`::Real`) contraction parameter -- `period`: (`::Real`, `[s]`) period -- `asymmetry`: (`::Real`) asymmetry factor, between 0 and 1 - -# Returns -- `periodic_heartbeat`: (`::PeriodicHeartBeat`) PeriodicHeartBeat struct -""" -@with_kw struct PeriodicHeartBeat{T<:Real} <: SimpleMotionType{T} - circumferential_strain :: T - radial_strain :: T - longitudinal_strain::T = typeof(circumferential_strain)(0.0) - period::T = typeof(circumferential_strain)(0.0) - asymmetry::T = typeof(circumferential_strain)(0.5) -end - -is_composable(motion_type::PeriodicHeartBeat) = true - -function displacement_x!( - ux::AbstractArray{T}, - motion_type::PeriodicHeartBeat{T}, - x::AbstractArray{T}, - y::AbstractArray{T}, - z::AbstractArray{T}, - t::AbstractArray{T}, -) where {T<:Real} - t_unit = unit_time_triangular(t, motion_type.period, motion_type.asymmetry) - r = sqrt.(x .^ 2 + y .^ 2) - θ = atan.(y, x) - Δ_circunferential = motion_type.circumferential_strain * maximum(r) - Δ_radial = -motion_type.radial_strain * (maximum(r) .- r) - Δr = t_unit .* (Δ_circunferential .+ Δ_radial) - # Map negative radius to r=0 - neg = (r .+ Δr) .< 0 - Δr = (.!neg) .* Δr - Δr .-= neg .* r - ux .= Δr .* cos.(θ) - return nothing -end - -function displacement_y!( - uy::AbstractArray{T}, - motion_type::PeriodicHeartBeat{T}, - x::AbstractArray{T}, - y::AbstractArray{T}, - z::AbstractArray{T}, - t::AbstractArray{T}, -) where {T<:Real} - t_unit = unit_time_triangular(t, motion_type.period, motion_type.asymmetry) - r = sqrt.(x .^ 2 + y .^ 2) - θ = atan.(y, x) - Δ_circunferential = motion_type.circumferential_strain * maximum(r) - Δ_radial = -motion_type.radial_strain * (maximum(r) .- r) - Δr = t_unit .* (Δ_circunferential .+ Δ_radial) - # Map negative radius to r=0 - neg = (r .+ Δr) .< 0 - Δr = (.!neg) .* Δr - Δr .-= neg .* r - uy .= Δr .* sin.(θ) - return nothing -end - -function displacement_z!( - uz::AbstractArray{T}, - motion_type::PeriodicHeartBeat{T}, - x::AbstractArray{T}, - y::AbstractArray{T}, - z::AbstractArray{T}, - t::AbstractArray{T}, -) where {T<:Real} - t_unit = unit_time_triangular(t, motion_type.period, motion_type.asymmetry) - uz .= t_unit .* (z .* motion_type.longitudinal_strain) - return nothing -end - -function times(motion_type::PeriodicHeartBeat) - return [0, motion_type.period * motion_type.asymmetry, motion_type.period] -end \ No newline at end of file diff --git a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/PeriodicRotation.jl b/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/PeriodicRotation.jl deleted file mode 100644 index 8bb8cc5f1..000000000 --- a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/PeriodicRotation.jl +++ /dev/null @@ -1,84 +0,0 @@ -@doc raw""" - periodic_rotation = PeriodicRotation(pitch, roll, yaw, period, asymmetry) - -PeriodicRotation motion struct. It produces a rotation of the phantom in the three axes: -x (pitch), y (roll), and z (yaw) - -# Arguments -- `pitch`: (`::Real`, `[º]`) rotation in x -- `roll`: (`::Real`, `[º]`) rotation in y -- `yaw`: (`::Real`, `[º]`) rotation in z -- `period`: (`::Real`, `[s]`) period -- `asymmetry`: (`::Real`) asymmetry factor, between 0 and 1 - -# Returns -- `periodic_rotation`: (`::PeriodicRotation`) PeriodicRotation struct - -""" -@with_kw struct PeriodicRotation{T<:Real} <: SimpleMotionType{T} - pitch :: T - roll :: T - yaw :: T - period::T = typeof(pitch)(0.0) - asymmetry::T = typeof(pitch)(0.5) -end - -is_composable(motion_type::PeriodicRotation) = true - -function displacement_x!( - ux::AbstractArray{T}, - motion_type::PeriodicRotation{T}, - x::AbstractArray{T}, - y::AbstractArray{T}, - z::AbstractArray{T}, - t::AbstractArray{T}, -) where {T<:Real} - t_unit = unit_time_triangular(t, motion_type.period, motion_type.asymmetry) - α = t_unit .* motion_type.pitch - β = t_unit .* motion_type.roll - γ = t_unit .* motion_type.yaw - ux .= cosd.(γ) .* cosd.(β) .* x + - (cosd.(γ) .* sind.(β) .* sind.(α) .- sind.(γ) .* cosd.(α)) .* y + - (cosd.(γ) .* sind.(β) .* cosd.(α) .+ sind.(γ) .* sind.(α)) .* z .- x - return nothing -end - -function displacement_y!( - uy::AbstractArray{T}, - motion_type::PeriodicRotation{T}, - x::AbstractArray{T}, - y::AbstractArray{T}, - z::AbstractArray{T}, - t::AbstractArray{T}, -) where {T<:Real} - t_unit = unit_time_triangular(t, motion_type.period, motion_type.asymmetry) - α = t_unit .* motion_type.pitch - β = t_unit .* motion_type.roll - γ = t_unit .* motion_type.yaw - uy .= sind.(γ) .* cosd.(β) .* x + - (sind.(γ) .* sind.(β) .* sind.(α) .+ cosd.(γ) .* cosd.(α)) .* y + - (sind.(γ) .* sind.(β) .* cosd.(α) .- cosd.(γ) .* sind.(α)) .* z .- y - return nothing -end - -function displacement_z!( - uz::AbstractArray{T}, - motion_type::PeriodicRotation{T}, - x::AbstractArray{T}, - y::AbstractArray{T}, - z::AbstractArray{T}, - t::AbstractArray{T}, -) where {T<:Real} - t_unit = unit_time_triangular(t, motion_type.period, motion_type.asymmetry) - α = t_unit .* motion_type.pitch - β = t_unit .* motion_type.roll - γ = t_unit .* motion_type.yaw - uz .= -sind.(β) .* x + - cosd.(β) .* sind.(α) .* y + - cosd.(β) .* cosd.(α) .* z .- z - return nothing -end - -function times(motion_type::PeriodicRotation) - return [0, motion_type.period * motion_type.asymmetry, motion_type.period] -end \ No newline at end of file diff --git a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/PeriodicTranslation.jl b/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/PeriodicTranslation.jl deleted file mode 100644 index 7dcebaac8..000000000 --- a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/PeriodicTranslation.jl +++ /dev/null @@ -1,67 +0,0 @@ -@doc raw""" - periodic_translation = PeriodicTranslation(dx, dy, dz, period, asymmetry) - -PeriodicTranslation motion struct. It produces a periodic translation of the phantom in the three directions x, y and z. -The amplitude of the oscillation will be defined by dx, dy and dz - -# Arguments -- `dx`: (`::Real`, `[m]`) translation in x -- `dy`: (`::Real`, `[m]`) translation in y -- `dz`: (`::Real`, `[m]`) translation in z -- `period`: (`::Real`, `[s]`) period -- `asymmetry`: (`::Real`) asymmetry factor, between 0 and 1 - -# Returns -- `periodic_translation`: (`::PeriodicTranslation`) PeriodicTranslation struct - -""" -@with_kw struct PeriodicTranslation{T<:Real} <: SimpleMotionType{T} - dx :: T - dy :: T - dz :: T - period::T = typeof(dx)(0.0) - asymmetry::T = typeof(dx)(0.5) -end - -function displacement_x!( - ux::AbstractArray{T}, - motion_type::PeriodicTranslation{T}, - x::AbstractVector{T}, - y::AbstractVector{T}, - z::AbstractVector{T}, - t::AbstractArray{T}, -) where {T<:Real} - t_unit = unit_time_triangular(t, motion_type.period, motion_type.asymmetry) - ux .= t_unit .* motion_type.dx - return nothing -end - -function displacement_y!( - uy::AbstractArray{T}, - motion_type::PeriodicTranslation{T}, - x::AbstractVector{T}, - y::AbstractVector{T}, - z::AbstractVector{T}, - t::AbstractArray{T}, -) where {T<:Real} - t_unit = unit_time_triangular(t, motion_type.period, motion_type.asymmetry) - uy .= t_unit .* motion_type.dy - return nothing -end - -function displacement_z!( - uz::AbstractArray{T}, - motion_type::PeriodicTranslation{T}, - x::AbstractVector{T}, - y::AbstractVector{T}, - z::AbstractVector{T}, - t::AbstractArray{T}, -) where {T<:Real} - t_unit = unit_time_triangular(t, motion_type.period, motion_type.asymmetry) - uz .= t_unit .* motion_type.dz - return nothing -end - -function times(motion_type::PeriodicTranslation) - return [0, motion_type.period * motion_type.asymmetry, motion_type.period] -end \ No newline at end of file diff --git a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Rotation.jl b/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Rotation.jl index 2216e7d1d..48628b5b8 100644 --- a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Rotation.jl +++ b/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Rotation.jl @@ -1,5 +1,5 @@ @doc raw""" - rotation = Rotation(pitch, roll, yaw, t_start, t_end) + rotation = Rotation(times, pitch, roll, yaw) Rotation motion struct. It produces a rotation of the phantom in the three axes: x (pitch), y (roll), and z (yaw). @@ -38,43 +38,40 @@ R &= R_z(\alpha) R_y(\beta) R_x(\gamma) \\ ``` # Arguments +- `times`: (`::TimeScale{T<:Real}`, `[s]`) time scale - `pitch`: (`::Real`, `[º]`) rotation in x - `roll`: (`::Real`, `[º]`) rotation in y - `yaw`: (`::Real`, `[º]`) rotation in z -- `t_start`: (`::Real`, `[s]`) initial time -- `t_end`: (`::Real`, `[s]`) final time # Returns - `rotation`: (`::Rotation`) Rotation struct # Examples ```julia-repl -julia> rt = Rotation(pitch=15.0, roll=0.0, yaw=20.0, t_start=0.1, t_end=0.5) +julia> rt = Rotation(times=TimeRange(0.0, 0.5), pitch=15.0, roll=0.0, yaw=20.0) ``` """ -@with_kw struct Rotation{T<:Real} <: SimpleMotionType{T} +@with_kw struct Rotation{T<:Real, TS<:TimeScale{T}} <: SimpleMotion{T} + times :: TS pitch :: T roll :: T yaw :: T - t_start::T = typeof(pitch)(0.0) - t_end = typeof(pitch)(0.0) - @assert t_end >= t_start "t_end must be greater or equal than t_start" end -is_composable(motion_type::Rotation) = true +is_composable(motion::Rotation) = true function displacement_x!( ux::AbstractArray{T}, - motion_type::Rotation{T}, + motion::Rotation{T}, x::AbstractArray{T}, y::AbstractArray{T}, z::AbstractArray{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion_type.t_start, motion_type.t_end) - α = t_unit .* (motion_type.yaw) - β = t_unit .* (motion_type.roll) - γ = t_unit .* (motion_type.pitch) + t_unit = unit_time(t, motion.times) + α = t_unit .* (motion.yaw) + β = t_unit .* (motion.roll) + γ = t_unit .* (motion.pitch) ux .= cosd.(α) .* cosd.(β) .* x + (cosd.(α) .* sind.(β) .* sind.(γ) .- sind.(α) .* cosd.(γ)) .* y + (cosd.(α) .* sind.(β) .* cosd.(γ) .+ sind.(α) .* sind.(γ)) .* z .- x @@ -83,16 +80,16 @@ end function displacement_y!( uy::AbstractArray{T}, - motion_type::Rotation{T}, + motion::Rotation{T}, x::AbstractArray{T}, y::AbstractArray{T}, z::AbstractArray{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion_type.t_start, motion_type.t_end) - α = t_unit .* (motion_type.yaw) - β = t_unit .* (motion_type.roll) - γ = t_unit .* (motion_type.pitch) + t_unit = unit_time(t, motion.times) + α = t_unit .* (motion.yaw) + β = t_unit .* (motion.roll) + γ = t_unit .* (motion.pitch) uy .= sind.(α) .* cosd.(β) .* x + (sind.(α) .* sind.(β) .* sind.(γ) .+ cosd.(α) .* cosd.(γ)) .* y + (sind.(α) .* sind.(β) .* cosd.(γ) .- cosd.(α) .* sind.(γ)) .* z .- y @@ -101,22 +98,18 @@ end function displacement_z!( uz::AbstractArray{T}, - motion_type::Rotation{T}, + motion::Rotation{T}, x::AbstractArray{T}, y::AbstractArray{T}, z::AbstractArray{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion_type.t_start, motion_type.t_end) - α = t_unit .* (motion_type.yaw) - β = t_unit .* (motion_type.roll) - γ = t_unit .* (motion_type.pitch) + t_unit = unit_time(t, motion.times) + α = t_unit .* (motion.yaw) + β = t_unit .* (motion.roll) + γ = t_unit .* (motion.pitch) uz .= -sind.(β) .* x + cosd.(β) .* sind.(γ) .* y + cosd.(β) .* cosd.(γ) .* z .- z return nothing -end - -times(motion_type::Rotation) = begin - return [motion_type.t_start, motion_type.t_end] end \ No newline at end of file diff --git a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Translation.jl b/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Translation.jl index bbdca7317..88634f185 100644 --- a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Translation.jl +++ b/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Translation.jl @@ -1,73 +1,67 @@ @doc raw""" - translation = Translation(dx, dy, dz, t_start, t_end) + translation = Translation(times, dx, dy, dz) Translation motion struct. It produces a linear translation of the phantom. Its fields are the final displacements in the three axes (dx, dy, dz) and the start and end times of the translation. # Arguments +- `times`: (`::TimeScale{T<:Real}`, `[s]`) time scale - `dx`: (`::Real`, `[m]`) translation in x - `dy`: (`::Real`, `[m]`) translation in y - `dz`: (`::Real`, `[m]`) translation in z -- `t_start`: (`::Real`, `[s]`) initial time -- `t_end`: (`::Real`, `[s]`) final time # Returns - `translation`: (`::Translation`) Translation struct # Examples ```julia-repl -julia> tr = Translation(dx=0.01, dy=0.02, dz=0.03, t_start=0.0, t_end=0.5) +julia> tr = Translation(times=TimeRange(0.0, 0.5), dx=0.01, dy=0.02, dz=0.03) ``` """ -@with_kw struct Translation{T<:Real} <: SimpleMotionType{T} +@with_kw struct Translation{T<:Real, TS<:TimeScale{T}} <: SimpleMotion{T} + times :: TS dx :: T dy :: T dz :: T - t_start::T = typeof(dx)(0.0) - t_end::T = typeof(dx)(0.0) - @assert t_end >= t_start "t_end must be greater or equal than t_start" end function displacement_x!( ux::AbstractArray{T}, - motion_type::Translation{T}, + motion::Translation{T}, x::AbstractVector{T}, y::AbstractVector{T}, z::AbstractVector{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion_type.t_start, motion_type.t_end) - ux .= t_unit .* motion_type.dx + t_unit = unit_time(t, motion.times) + ux .= t_unit .* motion.dx return nothing end function displacement_y!( uy::AbstractArray{T}, - motion_type::Translation{T}, + motion::Translation{T}, x::AbstractVector{T}, y::AbstractVector{T}, z::AbstractVector{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion_type.t_start, motion_type.t_end) - uy .= t_unit .* motion_type.dy + t_unit = unit_time(t, motion.times) + uy .= t_unit .* motion.dy return nothing end function displacement_z!( uz::AbstractArray{T}, - motion_type::Translation{T}, + motion::Translation{T}, x::AbstractVector{T}, y::AbstractVector{T}, z::AbstractVector{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion_type.t_start, motion_type.t_end) - uz .= t_unit .* motion_type.dz + t_unit = unit_time(t, motion.times) + uz .= t_unit .* motion.dz return nothing end -times(motion_type::Translation) = begin - return [motion_type.t_start, motion_type.t_end] -end diff --git a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/sorting.jl b/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/sorting.jl deleted file mode 100644 index 17a4931a5..000000000 --- a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/sorting.jl +++ /dev/null @@ -1,41 +0,0 @@ -""" - sorted_motion = sort_motion(motion) - -Sorts, with respect to time, the motion types of a `SimpleMotion` instance. -No allocations, since it uses the TupleTools.sort method -""" -function sort_motion(motion::SimpleMotion) - return SimpleMotion(_sort(motion.types, isless, m -> times(m)[1])) -end - - -""" - _sort(t::Tuple; lt=isless, by=identity, rev::Bool=false) -> ::Tuple - -Sorts the tuple `t`. Extracted from TupleTools.jl -""" -@inline function _sort(t::Tuple, lt=isless, by=identity, rev::Bool=false) - t1, t2 = _split(t) - t1s = _sort(t1, lt, by, rev) - t2s = _sort(t2, lt, by, rev) - return _merge(t1s, t2s, lt, by, rev) -end -_sort(t::Tuple{Any}, lt=isless, by=identity, rev::Bool=false) = t -_sort(t::Tuple{}, lt=isless, by=identity, rev::Bool=false) = t - -function _split(t::Tuple) - N = length(t) - M = N >> 1 - return ntuple(i -> t[i], M), ntuple(i -> t[i + M], N - M) -end - -function _merge(t1::Tuple, t2::Tuple, lt, by, rev) - if lt(by(first(t1)), by(first(t2))) != rev - return (first(t1), _merge(tail(t1), t2, lt, by, rev)...) - else - return (first(t2), _merge(t1, tail(t2), lt, by, rev)...) - end -end -_merge(::Tuple{}, t2::Tuple, lt, by, rev) = t2 -_merge(t1::Tuple, ::Tuple{}, lt, by, rev) = t1 -_merge(::Tuple{}, ::Tuple{}, lt, by, rev) = () \ No newline at end of file diff --git a/KomaMRIBase/src/timing/UnitTime.jl b/KomaMRIBase/src/timing/TimeScale.jl similarity index 54% rename from KomaMRIBase/src/timing/UnitTime.jl rename to KomaMRIBase/src/timing/TimeScale.jl index 0dff6bb7e..b5f29a862 100644 --- a/KomaMRIBase/src/timing/UnitTime.jl +++ b/KomaMRIBase/src/timing/TimeScale.jl @@ -1,5 +1,21 @@ +abstract type TimeScale{T<:Real} end + +@with_kw struct TimeRange{T<:Real} <: TimeScale{T} + t_start ::T + t_end ::T = t_start + @assert t_end >= t_start "t_end must be greater or equal than t_start" +end + +@with_kw struct Periodic{T<:Real} <: TimeScale{T} + period::T + asymmetry::T = eltype(period)(0.5) +end + +times(ts::TimeRange) = [ts.t_start, ts.t_end] +times(ts::Periodic{T}) where {T<:Real} = [zero(T), ts.period * ts.asymmetry, ts.period] + """ - t_unit = unit_time(t, t_start, t_end) + t_unit = unit_time(t, time_range) The `unit_time` function normalizes a given array of time values t to a unit interval [0, 1] based on a specified start time t_start and end time t_end. @@ -10,15 +26,14 @@ to fit within the range [0, 1] based on the provided start and end times. # Arguments - `t`: (`::AbstractArray{T<:Real}`, `[s]`) array of time values to be normalized -- `t_start`: (`::T`, `[s]`) start time for normalization -- `t_end`: (`::T`, `[s]`) end time for normalization +- `time_range`: (`::TimeRange{T<:Real}`, `[s]`) time interval (defined by `t_start` and `t_end`) over which we want to normalise # Returns - `t_unit`: (`::AbstractArray{T<:Real}`, `[s]`) array of normalized time values # Examples ```julia-repl -julia> t_unit = KomaMRIBase.unit_time([0.0, 1.0, 2.0, 3.0, 4.0, 5.0], 1.0, 4.0) +julia> t_unit = KomaMRIBase.unit_time([0.0, 1.0, 2.0, 3.0, 4.0, 5.0], TimeRange(1.0, 4.0)) 6-element Vector{Float64}: 0.0 0.0 @@ -27,18 +42,18 @@ julia> t_unit = KomaMRIBase.unit_time([0.0, 1.0, 2.0, 3.0, 4.0, 5.0], 1.0, 4.0) 1.0 1.0 """ -function unit_time(t::AbstractArray{T}, t_start::T, t_end::T) where {T<:Real} - if t_start == t_end - return (t .>= t_start) .* oneunit(T) +function unit_time(t::AbstractArray{T}, ts::TimeRange{T}) where {T<:Real} + if ts.t_start == ts.t_end + return (t .>= ts.t_start) .* oneunit(T) else - return min.(max.((t .- t_start) ./ (t_end - t_start), zero(T)), oneunit(T)) + return min.(max.((t .- ts.t_start) ./ (ts.t_end - ts.t_start), zero(T)), oneunit(T)) end end """ - t_unit = unit_time_triangular(t, period, asymmetry) + t_unit = unit_time(t, periodic) -The `unit_time_triangular` function normalizes a given array +The `unit_time` function normalizes a given array of time values t to a unit interval [0, 1] for periodic motions, based on a specified period and an asymmetry factor. This function is useful for creating triangular waveforms @@ -48,15 +63,13 @@ or normalizing time values in periodic processes. # Arguments - `t`: (`::AbstractArray{T<:Real}`, `[s]`) array of time values to be normalized -- `period`: (`::T`, `[s]`) the period of the triangular waveform -- `asymmetry`: (`::T`) asymmetry factor, a value in the range (0, 1) indicating the fraction of the period in the rising part of the triangular wave - +- `periodic`: (`::Periodic{T<:Real}`, `[s]`) information about the `period` and the temporal `asymmetry` # Returns - `t_unit`: (`::AbstractArray{T<:Real}`, `[s]`) array of normalized time values # Examples ```julia-repl -julia> t_unit = KomaMRIBase.unit_time_triangular([0.0, 1.0, 2.0, 3.0, 4.0, 5.0], 4.0, 0.5) +julia> t_unit = KomaMRIBase.unit_time_triangular([0.0, 1.0, 2.0, 3.0, 4.0, 5.0], Periodic(4.0, 0.5)) 6-element Vector{Float64}: 0.0 0.5 @@ -65,10 +78,10 @@ julia> t_unit = KomaMRIBase.unit_time_triangular([0.0, 1.0, 2.0, 3.0, 4.0, 5.0], 0.0 0.5 """ -function unit_time_triangular(t::AbstractArray{T}, period::T, asymmetry::T) where {T<:Real} - t_rise = period * asymmetry - t_fall = period * (oneunit(T) - asymmetry) - t_relative = mod.(t, period) +function unit_time(t::AbstractArray{T}, ts::Periodic{T}) where {T<:Real} + t_rise = ts.period * ts.asymmetry + t_fall = ts.period * (oneunit(T) - ts.asymmetry) + t_relative = mod.(t, ts.period) t_unit = ifelse.( t_relative .< t_rise, diff --git a/KomaMRIBase/test/runtests.jl b/KomaMRIBase/test/runtests.jl index d57a4e14f..3b9b8aae8 100644 --- a/KomaMRIBase/test/runtests.jl +++ b/KomaMRIBase/test/runtests.jl @@ -398,7 +398,7 @@ end t = collect(range(t_start, t_end, 11)) dx, dy, dz = [1.0, 0.0, 0.0] vx, vy, vz = [dx, dy, dz] ./ (t_end - t_start) - translation = SimpleMotion(Translation(dx, dy, dz, t_start, t_end)) + translation = MotionVector(Translation(TimeRange(t_start, t_end), dx, dy, dz)) xt, yt, zt = get_spin_coords(translation, ph.x, ph.y, ph.z, t') @test xt == ph.x .+ vx.*t' @test yt == ph.y .+ vy.*t' @@ -406,7 +406,7 @@ end # ----- t_start = t_end -------- t_start = t_end = 0.0 t = [-0.5, -0.25, 0.0, 0.25, 0.5] - translation = SimpleMotion(Translation(dx, dy, dz, t_start, t_end)) + translation = MotionVector(Translation(TimeRange(t_start, t_end), dx, dy, dz)) xt, yt, zt = get_spin_coords(translation, ph.x, ph.y, ph.z, t') @test xt == ph.x .+ dx*[0, 0, 1, 1, 1]' @test yt == ph.y .+ dy*[0, 0, 1, 1, 1]' @@ -420,7 +420,7 @@ end asymmetry = 0.5 dx, dy, dz = [1.0, 0.0, 0.0] vx, vy, vz = [dx, dy, dz] ./ (t_end - t_start) - periodictranslation = SimpleMotion(PeriodicTranslation(dx, dy, dz, period, asymmetry)) + periodictranslation = MotionVector(Translation(Periodic(period, asymmetry), dx, dy, dz)) xt, yt, zt = get_spin_coords(periodictranslation, ph.x, ph.y, ph.z, t') @test xt == ph.x .+ vx.*t' @test yt == ph.y .+ vy.*t' @@ -433,7 +433,7 @@ end pitch = 45.0 roll = 0.0 yaw = 45.0 - rotation = SimpleMotion(Rotation(pitch, roll, yaw, t_start, t_end)) + rotation = MotionVector(Rotation(TimeRange(t_start, t_end), pitch, roll, yaw)) xt, yt, zt = get_spin_coords(rotation, ph.x, ph.y, ph.z, t') r = vcat(ph.x, ph.y, ph.z) R = rotz(π*yaw/180) * roty(π*roll/180) * rotx(π*pitch/180) @@ -444,7 +444,7 @@ end # ----- t_start = t_end -------- t_start = t_end = 0.0 t = [-0.5, -0.25, 0.0, 0.25, 0.5] - rotation = SimpleMotion(Rotation(pitch, roll, yaw, t_start, t_end)) + rotation = MotionVector(Rotation(TimeRange(t_start, t_end), pitch, roll, yaw)) xt, yt, zt = get_spin_coords(rotation, ph.x, ph.y, ph.z, t') @test xt ≈ [ph.x ph.x rot_x rot_x rot_x] @test yt ≈ [ph.y ph.y rot_y rot_y rot_y] @@ -459,7 +459,7 @@ end pitch = 45.0 roll = 0.0 yaw = 45.0 - periodicrotation = SimpleMotion(PeriodicRotation(pitch, roll, yaw, period, asymmetry)) + periodicrotation = MotionVector(Rotation(Periodic(period, asymmetry), pitch, roll, yaw)) xt, yt, zt = get_spin_coords(periodicrotation, ph.x, ph.y, ph.z, t') r = vcat(ph.x, ph.y, ph.z) R = rotz(π*yaw/180) * roty(π*roll/180) * rotx(π*pitch/180) @@ -475,7 +475,7 @@ end circumferential_strain = -0.1 radial_strain = 0.0 longitudinal_strain = -0.1 - heartbeat = SimpleMotion(HeartBeat(circumferential_strain, radial_strain, longitudinal_strain, t_start, t_end)) + heartbeat = MotionVector(HeartBeat(TimeRange(t_start, t_end), circumferential_strain, radial_strain, longitudinal_strain)) xt, yt, zt = get_spin_coords(heartbeat, ph.x, ph.y, ph.z, t') r = sqrt.(ph.x .^ 2 + ph.y .^ 2) θ = atan.(ph.y, ph.x) @@ -485,7 +485,7 @@ end # ----- t_start = t_end -------- t_start = t_end = 0.0 t = [-0.5, -0.25, 0.0, 0.25, 0.5] - heartbeat = SimpleMotion(HeartBeat(circumferential_strain, radial_strain, longitudinal_strain, t_start, t_end)) + heartbeat = MotionVector(HeartBeat(TimeRange(t_start, t_end), circumferential_strain, radial_strain, longitudinal_strain)) xt, yt, zt = get_spin_coords(heartbeat, ph.x, ph.y, ph.z, t') r = sqrt.(ph.x .^ 2 + ph.y .^ 2) θ = atan.(ph.y, ph.x) @@ -505,7 +505,7 @@ end circumferential_strain = -0.1 radial_strain = 0.0 longitudinal_strain = -0.1 - periodicheartbeat = SimpleMotion(PeriodicHeartBeat(circumferential_strain, radial_strain, longitudinal_strain, period, asymmetry)) + periodicheartbeat = MotionVector(HeartBeat(Periodic(period, asymmetry), circumferential_strain, radial_strain, longitudinal_strain)) xt, yt, zt = get_spin_coords(periodicheartbeat, ph.x, ph.y, ph.z, t') r = sqrt.(ph.x .^ 2 + ph.y .^ 2) θ = atan.(ph.y, ph.x) @@ -513,7 +513,7 @@ end @test yt[:,end] == ph.y .* (1 .+ circumferential_strain * maximum(r) .* sin.(θ)) @test zt[:,end] == ph.z .* (1 .+ longitudinal_strain) end - @testset "ArbitraryMotion" begin + @testset "Trajectory" begin # 1 spin ph = Phantom(x=[1.0], y=[1.0]) Ns = length(ph) @@ -523,8 +523,8 @@ end dx = rand(Ns, Nt) dy = rand(Ns, Nt) dz = rand(Ns, Nt) - arbitrarymotion = @suppress ArbitraryMotion(t_start, t_end, dx, dy, dz) - t = times(arbitrarymotion) + arbitrarymotion = MotionVector(Trajectory(TimeRange(t_start, t_end), dx, dy, dz)) + t = range(t_start, t_end, Nt) xt, yt, zt = get_spin_coords(arbitrarymotion, ph.x, ph.y, ph.z, t') @test xt == ph.x .+ dx @test yt == ph.y .+ dy @@ -538,24 +538,24 @@ end dx = rand(Ns, Nt) dy = rand(Ns, Nt) dz = rand(Ns, Nt) - arbitrarymotion = @suppress ArbitraryMotion(t_start, t_end, dx, dy, dz) - t = times(arbitrarymotion) + arbitrarymotion = MotionVector(Trajectory(TimeRange(t_start, t_end), dx, dy, dz)) + t = range(t_start, t_end, Nt) xt, yt, zt = get_spin_coords(arbitrarymotion, ph.x, ph.y, ph.z, t') @test xt == ph.x .+ dx @test yt == ph.y .+ dy @test zt == ph.z .+ dz end - simplemotion = SimpleMotion( - PeriodicTranslation(dx=0.05, dy=0.05, dz=0.0, period=0.5, asymmetry=0.5), - Rotation(pitch=0.0, roll=0.0, yaw=π / 2, t_start=0.05, t_end=0.5), + simplemotion = MotionVector( + Translation(times=Periodic(period=0.5, asymmetry=0.5), dx=0.05, dy=0.05, dz=0.0), + Rotation(times=TimeRange(t_start=0.05, t_end=0.5), pitch=0.0, roll=0.0, yaw=π / 2) ) Ns = length(obj1) Nt = 3 t_start = 0.0 t_end = 1.0 - arbitrarymotion = @suppress ArbitraryMotion(t_start, t_end, 0.01 .* rand(Ns, Nt), 0.01 .* rand(Ns, Nt), 0.01 .* rand(Ns, Nt)) + arbitrarymotion = MotionVector(Trajectory(TimeRange(t_start, t_end), 0.01 .* rand(Ns, Nt), 0.01 .* rand(Ns, Nt), 0.01 .* rand(Ns, Nt))) # Test phantom subset obs1 = Phantom( @@ -611,7 +611,7 @@ end [Dλ1; Dλ1[rng]], [Dλ2; Dλ2[rng]], [Dθ; Dθ[rng]], - [obs1.motion; obs2.motion] + vcat(obs1.motion, obs2.motion, length(obs1), length(obs2)) ) @test obs1 + obs2 == oba diff --git a/KomaMRICore/src/KomaMRICore.jl b/KomaMRICore/src/KomaMRICore.jl index d55896fc6..73fd92d79 100644 --- a/KomaMRICore/src/KomaMRICore.jl +++ b/KomaMRICore/src/KomaMRICore.jl @@ -20,6 +20,7 @@ include("other/DiffusionModel.jl") include("simulation/GPUFunctions.jl") include("simulation/Functors.jl") include("simulation/SimulatorCore.jl") +include("simulation/Flow.jl") # ISMRMRD export signal_to_raw_data @@ -28,6 +29,8 @@ export Mag export simulate, simulate_slice_profile # Spinors export Spinor, Rx, Ry, Rz, Q, Un +# Flow +export reset_magnetization! #Package version, KomaMRICore.__VERSION__ using Pkg diff --git a/KomaMRICore/src/simulation/Bloch/BlochDictSimulationMethod.jl b/KomaMRICore/src/simulation/Bloch/BlochDictSimulationMethod.jl index 3cf7abb6c..be526b3fa 100644 --- a/KomaMRICore/src/simulation/Bloch/BlochDictSimulationMethod.jl +++ b/KomaMRICore/src/simulation/Bloch/BlochDictSimulationMethod.jl @@ -52,6 +52,7 @@ function run_spin_precession!( tp = cumsum(seq.Δt) # t' = t - t0 dur = sum(seq.Δt) # Total length, used for signal relaxation Mxy = [M.xy M.xy .* exp.(-tp' ./ p.T2) .* (cos.(ϕ) .+ im * sin.(ϕ))] #This assumes Δw and T2 are constant in time + reset_magnetization!(M, Mxy, p.motion, seq.t') M.xy .= Mxy[:, end] #Acquired signal sig[:, :, 1] .= transpose(Mxy[:, findall(seq.ADC)]) diff --git a/KomaMRICore/src/simulation/Bloch/BlochSimulationMethod.jl b/KomaMRICore/src/simulation/Bloch/BlochSimulationMethod.jl index a6dabbd0a..4081dddaf 100644 --- a/KomaMRICore/src/simulation/Bloch/BlochSimulationMethod.jl +++ b/KomaMRICore/src/simulation/Bloch/BlochSimulationMethod.jl @@ -19,6 +19,7 @@ function initialize_spins_state( Mxy = zeros(T, Nspins) Mz = obj.ρ Xt = Mag{T}(Mxy, Mz) + sort_motions!(obj.motion) return Xt, obj end @@ -60,8 +61,9 @@ function run_spin_precession!( tp = cumsum(seq.Δt) # t' = t - t0 dur = sum(seq.Δt) # Total length, used for signal relaxation Mxy = [M.xy M.xy .* exp.(-tp' ./ p.T2) .* (cos.(ϕ) .+ im * sin.(ϕ))] #This assumes Δw and T2 are constant in time - M.xy .= Mxy[:, end] M.z .= M.z .* exp.(-dur ./ p.T1) .+ p.ρ .* (1 .- exp.(-dur ./ p.T1)) + reset_magnetization!(M, Mxy, p.motion, seq.t') + M.xy .= Mxy[:, end] #Acquired signal sig .= transpose(sum(Mxy[:, findall(seq.ADC)]; dims=1)) #<--- TODO: add coil sensitivities @@ -105,6 +107,7 @@ function run_spin_excitation!( #Relaxation M.xy .= M.xy .* exp.(-s.Δt ./ p.T2) M.z .= M.z .* exp.(-s.Δt ./ p.T1) .+ p.ρ .* (1 .- exp.(-s.Δt ./ p.T1)) + reset_magnetization!(M, M.xy, p.motion, s.t) end #Acquired signal #sig .= -1.4im #<-- This was to test if an ADC point was inside an RF block diff --git a/KomaMRICore/src/simulation/Flow.jl b/KomaMRICore/src/simulation/Flow.jl new file mode 100644 index 000000000..6b74b8bfd --- /dev/null +++ b/KomaMRICore/src/simulation/Flow.jl @@ -0,0 +1,27 @@ +""" + reset_magnetization! +""" +function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion::NoMotion{T}, t::AbstractArray{T}) where {T<:Real} + return nothing +end + +function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion::MotionVector{T}, t::AbstractArray{T}) where {T<:Real} + for m in motion.motions + reset_magnetization!(M, Mxy, m, t) + end + return nothing +end + +function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion::Motion{T}, t::AbstractArray{T}) where {T<:Real} + return nothing +end + +function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion::FlowTrajectory{T}, t::AbstractArray{T}) where {T<:Real} + itp = interpolate(motion.resetmag, Gridded(Constant{Previous}), Val(size(x,1))) + flags = resample(itp, unit_time(t, motion.times)) + reset = any(flags; dims=2) + flags = .!(cumsum(flags; dims=2) .>= 1) + Mxy .*= flags + M.z[reset] = p.ρ[reset] + return nothing +end \ No newline at end of file diff --git a/KomaMRICore/src/simulation/Functors.jl b/KomaMRICore/src/simulation/Functors.jl index 0d698b78f..0354b763e 100644 --- a/KomaMRICore/src/simulation/Functors.jl +++ b/KomaMRICore/src/simulation/Functors.jl @@ -6,7 +6,6 @@ _isleaf(x) = isleaf(x) _isleaf(::AbstractArray{<:Number}) = true _isleaf(::AbstractArray{T}) where T = isbitstype(T) _isleaf(::AbstractRange) = true - """ gpu(x) @@ -52,9 +51,8 @@ See also [`f32`](@ref) and [`f64`](@ref) to change element type only. x = gpu(x, CUDABackend()) ``` """ -function gpu(x, backend::KA.GPU) - return fmap(x -> adapt(backend, x), x; exclude=_isleaf) -end +gpu(x, backend::KA.GPU) = fmap(x -> adapt(backend, x), x; exclude=_isleaf) +adapt_storage(backend::KA.GPU, xs::MotionVector) = MotionVector(gpu.(xs.motions, Ref(backend))) # To CPU """ @@ -78,6 +76,7 @@ adapt_storage(T::Type{<:Real}, xs::AbstractArray{<:Real}) = convert.(T, xs) adapt_storage(T::Type{<:Real}, xs::AbstractArray{<:Complex}) = convert.(Complex{T}, xs) adapt_storage(T::Type{<:Real}, xs::AbstractArray{<:Bool}) = xs adapt_storage(T::Type{<:Real}, xs::NoMotion) = NoMotion{T}() +adapt_storage(T::Type{<:Real}, xs::MotionVector) = MotionVector(paramtype.(T, xs.motions)) """ f32(m) @@ -102,16 +101,13 @@ f64(m) = paramtype(Float64, m) #The functor macro makes it easier to call a function in all the parameters # Phantom @functor Phantom -# SimpleMotion -@functor SimpleMotion @functor Translation @functor Rotation @functor HeartBeat -@functor PeriodicTranslation -@functor PeriodicRotation -@functor PeriodicHeartBeat -# ArbitraryMotion -@functor ArbitraryMotion +@functor Trajectory +@functor FlowTrajectory +@functor TimeRange +@functor Periodic # Spinor @functor Spinor # DiscreteSequence diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index 77246c48e..0352d93bf 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -22,7 +22,7 @@ using TestItems, TestItemRunner #Environment variable set by CI const CI = get(ENV, "CI", nothing) -@run_package_tests filter=ti->(:core in ti.tags)&&(isnothing(CI) || :skipci ∉ ti.tags) #verbose=true +@run_package_tests filter=ti->(:motion in ti.tags)&&(isnothing(CI) || :skipci ∉ ti.tags) #verbose=true @testitem "Spinors×Mag" tags=[:core] begin using KomaMRICore: Rx, Ry, Rz, Q, rotx, roty, rotz, Un, Rφ, Rg @@ -287,7 +287,7 @@ end @test raw1.profiles[1].data ≈ raw2.profiles[1].data end -@testitem "Bloch SimpleMotion" tags=[:important, :core, :skipci] begin +@testitem "Bloch SimpleMotion" tags=[:important, :core, :motion] begin using Suppressor include("initialize.jl") include(joinpath(@__DIR__, "test_files", "utils.jl")) @@ -307,7 +307,7 @@ end @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end -@testitem "Bloch ArbitraryMotion" tags=[:important, :core, :skipci] begin +@testitem "Bloch ArbitraryMotion" tags=[:important, :core, :motion] begin using Suppressor include("initialize.jl") include(joinpath(@__DIR__, "test_files", "utils.jl")) diff --git a/KomaMRICore/test/test_files/utils.jl b/KomaMRICore/test/test_files/utils.jl index 0a5f878a6..7bc9d7caf 100644 --- a/KomaMRICore/test/test_files/utils.jl +++ b/KomaMRICore/test/test_files/utils.jl @@ -18,7 +18,7 @@ end function phantom_brain_simple_motion() obj = phantom_brain() - obj.motion = SimpleMotion(Translation(t_end=10.0, dx=0.0, dy=1.0, dz=0.0)) + obj.motion = MotionVector(Translation(times=TimeRange(0.0, 10.0), dx=0.0, dy=1.0, dz=0.0)) return obj end @@ -30,12 +30,11 @@ function phantom_brain_arbitrary_motion() dx = zeros(Ns, 2) dz = zeros(Ns, 2) dy = [zeros(Ns,1) ones(Ns,1)] - obj.motion = ArbitraryMotion( - t_start, - t_end, + obj.motion = MotionVector(Trajectory( + TimeRange(t_start, t_end), dx, dy, - dz) + dz)) return obj end diff --git a/KomaMRIFiles/src/KomaMRIFiles.jl b/KomaMRIFiles/src/KomaMRIFiles.jl index 259639a62..08ac4cb38 100644 --- a/KomaMRIFiles/src/KomaMRIFiles.jl +++ b/KomaMRIFiles/src/KomaMRIFiles.jl @@ -2,7 +2,6 @@ module KomaMRIFiles using KomaMRIBase using Scanf, FileIO, HDF5, MAT, InteractiveUtils # IO related - using Reexport using MRIFiles import MRIFiles: insertNode diff --git a/KomaMRIFiles/src/Phantom/Phantom.jl b/KomaMRIFiles/src/Phantom/Phantom.jl index 8409d792f..364b8dec7 100644 --- a/KomaMRIFiles/src/Phantom/Phantom.jl +++ b/KomaMRIFiles/src/Phantom/Phantom.jl @@ -25,8 +25,7 @@ function read_phantom(filename::String) end # Motion motion_group = fid["motion"] - model = read_attribute(motion_group, "model") - import_motion!(phantom_fields, Ns, Symbol(model), motion_group) + import_motion!(phantom_fields, motion_group) obj = Phantom(; phantom_fields...) close(fid) @@ -54,44 +53,56 @@ function read_param(param::HDF5.Group) return values end -function import_motion!(phantom_fields::Array, Ns::Int, model::Symbol, motion_group::HDF5.Group) - return import_motion!(phantom_fields, Ns, Val(model), motion_group) -end -function import_motion!( - phantom_fields::Array, Ns::Int, model::Val{:NoMotion}, motion_group::HDF5.Group -) - return nothing -end -function import_motion!( - phantom_fields::Array, Ns::Int, model::Val{:SimpleMotion}, motion_group::HDF5.Group -) - types_group = motion_group["types"] - types = SimpleMotionType[] - for key in keys(types_group) - type_group = types_group[key] - type_str = split(key, "_")[2] - @assert type_str in last.(split.(string.(subtypes(SimpleMotionType)), ".")) "Simple Motion Type: $(type_str) has not been implemented in KomaMRIBase $(KomaMRIBase.__VERSION__)" - for SMT in subtypes(SimpleMotionType) +function import_motion!(phantom_fields::Array, motion_group::HDF5.Group) + T = eltype(phantom_fields[2][2]) + motion_type = read_attribute(motion_group, "type") + if motion_type == "MotionVector" + simple_motion_types = last.(split.(string.(reduce(vcat,(subtypes(subtypes(Motion)[2])))), ".")) + arbitrary_motion_types = last.(split.(string.(reduce(vcat,(subtypes(subtypes(Motion)[1])))), ".")) + motion_array = Motion{T}[] + for key in keys(motion_group) + type_group = motion_group[key] + type_str = split(key, "_")[2] + @assert type_str in vcat(simple_motion_types, arbitrary_motion_types) "Motion Type: $(type_str) has not been implemented in KomaMRIBase $(KomaMRIBase.__VERSION__)" args = [] - if type_str == last(split(string(SMT), ".")) - for key in fieldnames(SMT) - push!(args, read_attribute(type_group, string(key))) + for smtype in subtypes(SimpleMotion) + if type_str == last(split(string(smtype), ".")) + times = import_time_range(type_group["times"]) + type_fields = filter(x -> x != :times, fieldnames(smtype)) + for key in type_fields + push!(args, read_attribute(type_group, string(key))) + end + push!(motion_array, smtype(times, args...)) + end + end + for amtype in subtypes(ArbitraryMotion) + if type_str == last(split(string(amtype), ".")) + times = import_time_range(type_group["times"]) + type_fields = filter(x -> x != :times, fieldnames(amtype)) + for key in type_fields + push!(args, read(type_group[string(key)])) + end + push!(motion_array, amtype(times, args...)) end - push!(types, SMT(args...)) end end + return push!(phantom_fields, (:motion, MotionVector(motion_array))) + elseif motion_type == "NoMotion" + return push!(phantom_fields, (:motion, NoMotion{T}())) end - return push!(phantom_fields, (:motion, SimpleMotion((types...)))) end -function import_motion!( - phantom_fields::Array, Ns::Int, model::Val{:ArbitraryMotion}, motion_group::HDF5.Group -) - t_start = read(motion_group["t_start"]) - t_end = read(motion_group["t_end"]) - dx = read(motion_group["dx"]) - dy = read(motion_group["dy"]) - dz = read(motion_group["dz"]) - return push!(phantom_fields, (:motion, ArbitraryMotion(t_start, t_end, dx, dy, dz))) + +function import_time_range(times_group::HDF5.Group) + time_scale_type = read_attribute(times_group, "type") + for tstype in subtypes(TimeScale) + if time_scale_type == last(split(string(tstype), ".")) + args = [] + for key in filter(x -> x != :type, fieldnames(tstype)) + push!(args, read_attribute(times_group, string(key))) + end + return tstype(args...) + end + end end """ @@ -136,27 +147,34 @@ function write_phantom( return close(fid) end -function export_motion!(motion_group::HDF5.Group, motion::NoMotion) - return HDF5.attributes(motion_group)["model"] = "NoMotion" -end -function export_motion!(motion_group::HDF5.Group, motion::SimpleMotion) - HDF5.attributes(motion_group)["model"] = "SimpleMotion" - types_group = create_group(motion_group, "types") - counter = 1 - for (counter, sm_type) in enumerate(motion.types) - simple_motion_type = typeof(sm_type).name.name - type_group = create_group(types_group, "$(counter)_$simple_motion_type") - phantom_fields = fieldnames(typeof(sm_type)) - for field in phantom_fields - HDF5.attributes(type_group)[string(field)] = getfield(sm_type, field) +function export_motion!(motion_group::HDF5.Group, mv::MotionVector{T}) where {T<:Real} + HDF5.attributes(motion_group)["type"] = "MotionVector" + for (counter, m) in enumerate(mv.motions) + type_name = typeof(m).name.name + type_group = create_group(motion_group, "$(counter)_$type_name") + export_time_range!(type_group, m.times) + type_fields = filter(x -> x != :times, fieldnames(typeof(m))) + for field in type_fields + field_value = getfield(m, field) + if typeof(field_value) <: Number + HDF5.attributes(type_group)[string(field)] = field_value + elseif typeof(field_value) <: AbstractArray + type_group[string(field)] = field_value + end end end end -function export_motion!(motion_group::HDF5.Group, motion::ArbitraryMotion) - HDF5.attributes(motion_group)["model"] = "ArbitraryMotion" - motion_group["t_start"] = motion.t_start - motion_group["t_end"] = motion.t_end - motion_group["dx"] = motion.dx - motion_group["dy"] = motion.dy - motion_group["dz"] = motion.dz + +function export_motion!(motion_group::HDF5.Group, motion::NoMotion{T}) where {T<:Real} + HDF5.attributes(motion_group)["type"] = "NoMotion" +end + +function export_time_range!(type_group::HDF5.Group, times::TimeScale) + times_name = typeof(times).name.name + times_group = create_group(type_group, "times") + HDF5.attributes(times_group)["type"] = string(times_name) + for field in fieldnames(typeof(times)) + field_value = getfield(times, field) + HDF5.attributes(times_group)[string(field)] = field_value + end end \ No newline at end of file diff --git a/KomaMRIFiles/test/runtests.jl b/KomaMRIFiles/test/runtests.jl index cca82be15..dad477288 100644 --- a/KomaMRIFiles/test/runtests.jl +++ b/KomaMRIFiles/test/runtests.jl @@ -64,15 +64,14 @@ using TestItems, TestItemRunner path = @__DIR__ filename = path * "/test_files/brain_simplemotion_w.phantom" obj1 = brain_phantom2D() - obj1.motion = SimpleMotion( - PeriodicRotation( - period=1.0, + obj1.motion = MotionVector( + Rotation( + times=Periodic(period=1.0), yaw=45.0, pitch=0.0, roll=0.0), Translation( - t_start=0.0, - t_end=0.5, + times=TimeRange(t_start=0.0, t_end=0.5), dx=0.0, dy=0.02, dz=0.0 @@ -91,12 +90,11 @@ using TestItems, TestItemRunner K = 10 t_start = 0.0 t_end = 1.0 - obj1.motion = ArbitraryMotion( - t_start, - t_end, + obj1.motion = MotionVector(Trajectory( + TimeRange(t_start, t_end), 0.01.*rand(Ns, K-1), 0.01.*rand(Ns, K-1), - 0.01.*rand(Ns, K-1)) + 0.01.*rand(Ns, K-1))) write_phantom(obj1, filename) obj2 = read_phantom(filename) @test obj1 == obj2 diff --git a/KomaMRIPlots/src/ui/DisplayFunctions.jl b/KomaMRIPlots/src/ui/DisplayFunctions.jl index 4d181c409..5636f386f 100644 --- a/KomaMRIPlots/src/ui/DisplayFunctions.jl +++ b/KomaMRIPlots/src/ui/DisplayFunctions.jl @@ -1033,27 +1033,23 @@ function plot_phantom_map( kwargs..., ) - function interpolate_times(motion::MotionModel) + function interpolate_times(motion::AbstractMotion{T}) where {T<:Real} t = times(motion) - # Interpolate time points (as many as indicated by intermediate_time_samples) - itp = interpolate((1:(intermediate_time_samples + 1):(length(t) + intermediate_time_samples * (length(t) - 1)), ), t, Gridded(Linear())) - t = itp.(1:(length(t) + intermediate_time_samples * (length(t) - 1))) + if length(t)>1 + # Interpolate time points (as many as indicated by intermediate_time_samples) + itp = interpolate((1:(intermediate_time_samples + 1):(length(t) + intermediate_time_samples * (length(t) - 1)), ), t, Gridded(Linear())) + t = itp.(1:(length(t) + intermediate_time_samples * (length(t) - 1))) + end return t end - function process_times(motion::SimpleMotion) - motion = KomaMRIBase.sort_motion(motion) - return interpolate_times(motion) - end - function process_times(motion::ArbitraryMotion) + function process_times(motion::AbstractMotion{T}) where {T<:Real} + sort_motions!(motion) t = interpolate_times(motion) # Decimate time points so their number is smaller than max_time_samples ss = length(t) > max_time_samples ? length(t) ÷ max_time_samples : 1 return t[1:ss:end] end - function process_times(motion::MotionModel) - return times(motion) - end function decimate_uniform_phantom(ph::Phantom, num_points::Int) dimx, dimy, dimz = KomaMRIBase.get_dims(ph) From b948d498dd001e35026ab4652164135d4c9c42a3 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 11 Jul 2024 13:14:00 +0200 Subject: [PATCH 02/91] Delete `Static` --- KomaMRIBase/src/KomaMRIBase.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KomaMRIBase/src/KomaMRIBase.jl b/KomaMRIBase/src/KomaMRIBase.jl index 21f835930..e29d5a950 100644 --- a/KomaMRIBase/src/KomaMRIBase.jl +++ b/KomaMRIBase/src/KomaMRIBase.jl @@ -48,7 +48,7 @@ export brain_phantom2D, brain_phantom3D, pelvis_phantom2D, heart_phantom # Motion export AbstractMotion, Motion, MotionVector, NoMotion export SimpleMotion, ArbitraryMotion -export Static, Translation, Rotation, HeartBeat, Trajectory, FlowTrajectory +export Translation, Rotation, HeartBeat, Trajectory, FlowTrajectory export TimeScale, TimeRange, Periodic export sort_motions!, get_spin_coords # Secondary From 7ad003eb01018d75017f5618d448697ca7ef2b5c Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 1 Aug 2024 11:28:19 +0200 Subject: [PATCH 03/91] Apply suggested change by @rkierulf --- KomaMRIBase/src/timing/TimeScale.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/KomaMRIBase/src/timing/TimeScale.jl b/KomaMRIBase/src/timing/TimeScale.jl index b5f29a862..4291d31db 100644 --- a/KomaMRIBase/src/timing/TimeScale.jl +++ b/KomaMRIBase/src/timing/TimeScale.jl @@ -46,7 +46,8 @@ function unit_time(t::AbstractArray{T}, ts::TimeRange{T}) where {T<:Real} if ts.t_start == ts.t_end return (t .>= ts.t_start) .* oneunit(T) else - return min.(max.((t .- ts.t_start) ./ (ts.t_end - ts.t_start), zero(T)), oneunit(T)) + tmp = max.((t .- t_start) ./ (t_end - t_start), zero(T)) + return min.(tmp, oneunit(T)) end end From 2804413ba8b6e5fe63a4aa4cadf66744a1fdcfc9 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 1 Aug 2024 22:29:37 +0200 Subject: [PATCH 04/91] Solve bug --- KomaMRIBase/src/timing/TimeScale.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KomaMRIBase/src/timing/TimeScale.jl b/KomaMRIBase/src/timing/TimeScale.jl index 4291d31db..73adb1fc7 100644 --- a/KomaMRIBase/src/timing/TimeScale.jl +++ b/KomaMRIBase/src/timing/TimeScale.jl @@ -46,7 +46,7 @@ function unit_time(t::AbstractArray{T}, ts::TimeRange{T}) where {T<:Real} if ts.t_start == ts.t_end return (t .>= ts.t_start) .* oneunit(T) else - tmp = max.((t .- t_start) ./ (t_end - t_start), zero(T)) + tmp = max.((t .- ts.t_start) ./ (ts.t_end - ts.t_start), zero(T)) return min.(tmp, oneunit(T)) end end From 596af23adf735d1a56250a5fac4f2109ced7abcc Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 1 Aug 2024 23:45:19 +0200 Subject: [PATCH 05/91] Enable all testsitems --- KomaMRICore/test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index 03a0b58a1..69333e2c2 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -36,7 +36,7 @@ using TestItems, TestItemRunner #Environment variable set by CI const CI = get(ENV, "CI", nothing) -@run_package_tests filter=ti->(:motion in ti.tags)&&(isnothing(CI) || :skipci ∉ ti.tags) #verbose=true +@run_package_tests filter=ti->(:core in ti.tags)&&(isnothing(CI) || :skipci ∉ ti.tags) #verbose=true @testitem "Spinors×Mag" tags=[:core] begin using KomaMRICore: Rx, Ry, Rz, Q, rotx, roty, rotz, Un, Rφ, Rg From fe9c8c2554f7117cd30c64a293607acb2e2f348b Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 8 Aug 2024 19:58:21 +0200 Subject: [PATCH 06/91] Suggested changes --- KomaMRIBase/src/KomaMRIBase.jl | 8 ++-- KomaMRIBase/src/datatypes/Phantom.jl | 10 ++-- KomaMRIBase/src/datatypes/phantom/Motion.jl | 47 ++++++++++--------- KomaMRIBase/src/datatypes/phantom/NoMotion.jl | 6 +-- .../phantom/motion/ArbitraryMotion.jl | 12 ++--- .../datatypes/phantom/motion/SimpleMotion.jl | 2 +- .../motion/arbitrarymotion/FlowTrajectory.jl | 6 +-- .../motion/arbitrarymotion/Trajectory.jl | 4 +- .../phantom/motion/simplemotion/HeartBeat.jl | 16 +++---- .../phantom/motion/simplemotion/Rotation.jl | 20 ++++---- .../motion/simplemotion/Translation.jl | 22 +++++---- KomaMRIBase/src/timing/TimeScale.jl | 9 ++-- KomaMRIBase/test/runtests.jl | 30 ++++++------ KomaMRICore/src/simulation/Flow.jl | 8 ++-- KomaMRICore/src/simulation/Functors.jl | 4 +- KomaMRICore/test/runtests.jl | 43 +++++++++++++++++ KomaMRICore/test/test_files/utils.jl | 4 +- KomaMRIFiles/src/Phantom/Phantom.jl | 44 ++++++++--------- KomaMRIFiles/test/runtests.jl | 8 ++-- examples/3.tutorials/lit-05-SimpleMotion.jl | 4 +- 20 files changed, 180 insertions(+), 127 deletions(-) diff --git a/KomaMRIBase/src/KomaMRIBase.jl b/KomaMRIBase/src/KomaMRIBase.jl index 370007e1a..ca76a7462 100644 --- a/KomaMRIBase/src/KomaMRIBase.jl +++ b/KomaMRIBase/src/KomaMRIBase.jl @@ -46,10 +46,12 @@ export kfoldperm, trapz, cumtrapz # Phantom export brain_phantom2D, brain_phantom3D, pelvis_phantom2D, heart_phantom # Motion -export AbstractMotion, Motion, MotionVector, NoMotion +export MotionList, NoMotion export SimpleMotion, ArbitraryMotion -export Translation, Rotation, HeartBeat, Trajectory, FlowTrajectory -export TimeScale, TimeRange, Periodic +export Translation, TranslationX, TranslationY, TranslationZ +export Rotation, RotationX, RotationY, RotationZ +export HeartBeat, Trajectory, FlowTrajectory +export AbstractTimeSpan, TimeRange, Periodic export sort_motions!, get_spin_coords # Secondary export get_kspace, rotx, roty, rotz diff --git a/KomaMRIBase/src/datatypes/Phantom.jl b/KomaMRIBase/src/datatypes/Phantom.jl index 4de25d2df..9f5cee974 100644 --- a/KomaMRIBase/src/datatypes/Phantom.jl +++ b/KomaMRIBase/src/datatypes/Phantom.jl @@ -1,7 +1,7 @@ # TimeScale: include("../timing/TimeScale.jl") # Motion: -abstract type AbstractMotion{T<:Real} end +abstract type AbstractMotionList{T<:Real} end include("phantom/Motion.jl") include("phantom/NoMotion.jl") @@ -54,7 +54,7 @@ julia> obj.ρ Dθ::AbstractVector{T} = zeros(eltype(x), size(x)) #Diff::Vector{DiffusionModel} #Diffusion map #Motion - motion::AbstractMotion{T} = NoMotion{eltype(x)}() + motion::AbstractMotionList{T} = NoMotion{eltype(x)}() end """Size and length of a phantom""" @@ -192,15 +192,15 @@ function heart_phantom( Dλ1=Dλ1[ρ .!= 0], Dλ2=Dλ2[ρ .!= 0], Dθ=Dθ[ρ .!= 0], - motion=MotionVector( + motion=MotionList( HeartBeat(; - times=Periodic(; period=period, asymmetry=asymmetry), + time=Periodic(; period=period, asymmetry=asymmetry), circumferential_strain=circumferential_strain, radial_strain=radial_strain, longitudinal_strain=0.0, ), Rotation(; - times=Periodic(; period=period, asymmetry=asymmetry), + time=Periodic(; period=period, asymmetry=asymmetry), yaw=rotation_angle, pitch=0.0, roll=0.0, diff --git a/KomaMRIBase/src/datatypes/phantom/Motion.jl b/KomaMRIBase/src/datatypes/phantom/Motion.jl index eafde6d02..aa5b46a3d 100644 --- a/KomaMRIBase/src/datatypes/phantom/Motion.jl +++ b/KomaMRIBase/src/datatypes/phantom/Motion.jl @@ -1,53 +1,53 @@ -abstract type Motion{T<:Real} end +abstract type AbstractMotion{T<:Real} end -is_composable(m::Motion) = false +is_composable(m::AbstractMotion) = false -struct MotionVector{T<:Real} <: AbstractMotion{T} - motions::Vector{<:Motion{T}} +struct MotionList{T<:Real} <: AbstractMotionList{T} + motions::Vector{<:AbstractMotion{T}} end -MotionVector(motions...) = length([motions]) > 0 ? MotionVector([motions...]) : @error "You must provide at least one motion as input argument. If you do not want to define motion, use `NoMotion{T}()`" +MotionList(motions...) = length([motions]) > 0 ? MotionList([motions...]) : @error "You must provide at least one motion as input argument. If you do not want to define motion, use `NoMotion{T}()`" include("motion/SimpleMotion.jl") include("motion/ArbitraryMotion.jl") -Base.getindex(mv::MotionVector, p::Union{AbstractRange, AbstractVector, Colon, Integer}) = MotionVector(getindex.(mv.motions, Ref(p))) -Base.view(mv::MotionVector, p::Union{AbstractRange, AbstractVector, Colon, Integer}) = MotionVector(view.(mv.motions, Ref(p))) +Base.getindex(mv::MotionList, p::Union{AbstractRange, AbstractVector, Colon, Integer}) = MotionList(getindex.(mv.motions, Ref(p))) +Base.view(mv::MotionList, p::Union{AbstractRange, AbstractVector, Colon, Integer}) = MotionList(view.(mv.motions, Ref(p))) -""" Addition of MotionVectors """ -function Base.vcat(m1::MotionVector{T}, m2::MotionVector{T}, Ns1::Int, Ns2::Int) where {T<:Real} +""" Addition of MotionLists """ +function Base.vcat(m1::MotionList{T}, m2::MotionList{T}, Ns1::Int, Ns2::Int) where {T<:Real} mv1 = m1.motions - mv1_aux = Motion{T}[] + mv1_aux = AbstractMotion{T}[] for i in 1:length(mv1) if typeof(mv1[i]) <: ArbitraryMotion zeros1 = similar(mv1[i].dx, Ns2, size(mv1[i].dx, 2)) zeros1 .= zero(T) - push!(mv1_aux, typeof(mv1[i])(mv1[i].times, [[getfield(mv1[i], d); zeros1] for d in filter(x -> x != :times, fieldnames(typeof(mv1[i])))]...)) + push!(mv1_aux, typeof(mv1[i])(mv1[i].time, [[getfield(mv1[i], d); zeros1] for d in filter(x -> x != :time, fieldnames(typeof(mv1[i])))]...)) else push!(mv1_aux, mv1[i]) end end mv2 = m2.motions - mv2_aux = Motion{T}[] + mv2_aux = AbstractMotion{T}[] for i in 1:length(mv2) if typeof(mv2[i]) <: ArbitraryMotion zeros2 = similar(mv2[i].dx, Ns1, size(mv2[i].dx, 2)) zeros2 .= zero(T) - push!(mv2_aux, typeof(mv2[i])(mv2[i].times, [[zeros2; getfield(mv2[i], d)] for d in filter(x -> x != :times, fieldnames(typeof(mv2[i])))]...)) + push!(mv2_aux, typeof(mv2[i])(mv2[i].time, [[zeros2; getfield(mv2[i], d)] for d in filter(x -> x != :time, fieldnames(typeof(mv2[i])))]...)) else push!(mv2_aux, mv2[i]) end end - return MotionVector([mv1_aux; mv2_aux]) + return MotionList([mv1_aux; mv2_aux]) end """ Compare two motion vectors """ -function Base.:(==)(mv1::MotionVector{T}, mv2::MotionVector{T}) where {T<:Real} +function Base.:(==)(mv1::MotionList{T}, mv2::MotionList{T}) where {T<:Real} sort_motions!(mv1) sort_motions!(mv2) return reduce(&, mv1.motions .== mv2.motions) end -function Base.:(≈)(mv1::MotionVector{T}, mv2::MotionVector{T}) where {T<:Real} +function Base.:(≈)(mv1::MotionList{T}, mv2::MotionList{T}) where {T<:Real} sort_motions!(mv1) sort_motions!(mv2) return reduce(&, mv1.motions .≈ mv2.motions) @@ -60,7 +60,7 @@ Calculates the position of each spin at a set of arbitrary time instants, i.e. t For each dimension (x, y, z), the output matrix has ``N_{\text{spins}}`` rows and `length(t)` columns. # Arguments -- `motion`: (`::Vector{<:Motion{T<:Real}}`) phantom motion +- `motion`: (`::Vector{<:AbstractMotion{T<:Real}}`) phantom motion - `x`: (`::AbstractVector{T<:Real}`, `[m]`) spin x-position vector - `y`: (`::AbstractVector{T<:Real}`, `[m]`) spin y-position vector - `z`: (`::AbstractVector{T<:Real}`, `[m]`) spin z-position vector @@ -70,7 +70,7 @@ For each dimension (x, y, z), the output matrix has ``N_{\text{spins}}`` rows an - `x, y, z`: (`::Tuple{AbstractArray, AbstractArray, AbstractArray}`) spin positions over time """ function get_spin_coords( - mv::MotionVector{T}, + mv::MotionList{T}, x::AbstractVector{T}, y::AbstractVector{T}, z::AbstractVector{T}, @@ -81,7 +81,7 @@ function get_spin_coords( # Buffers for displacements: ux, uy, uz = similar(xt), similar(yt), similar(zt) - # Composable motions: they need to be run sequentially + # Composable motions: they need to be run sequentially. Note that they depend on xt, yt , and zt for m in Iterators.filter(is_composable, mv.motions) displacement_x!(ux, m, xt, yt, zt, t) displacement_y!(uy, m, xt, yt, zt, t) @@ -105,16 +105,17 @@ end """ times = times(motion) """ -times(m::Motion) = times(m.times) -times(mv::MotionVector{T}) where {T<:Real} = begin +times(m::AbstractMotion) = times(m.time) +times(mv::MotionList{T}) where {T<:Real} = begin nodes = reduce(vcat, [times(m) for m in mv.motions]; init=[zero(T)]) return unique(sort(nodes)) end """ - sort_motions! + sort_motions!(motion_list) +sort_motions motions in a list according to their starting time """ -function sort_motions!(mv::MotionVector{T}) where {T<:Real} +function sort_motions!(mv::MotionList{T}) where {T<:Real} sort!(mv.motions; by=m -> times(m)[1]) return nothing end \ No newline at end of file diff --git a/KomaMRIBase/src/datatypes/phantom/NoMotion.jl b/KomaMRIBase/src/datatypes/phantom/NoMotion.jl index 09f7774c8..ed52d186d 100644 --- a/KomaMRIBase/src/datatypes/phantom/NoMotion.jl +++ b/KomaMRIBase/src/datatypes/phantom/NoMotion.jl @@ -1,11 +1,11 @@ -struct NoMotion{T<:Real} <: AbstractMotion{T} end +struct NoMotion{T<:Real} <: AbstractMotionList{T} end Base.getindex(mv::NoMotion, p::Union{AbstractRange, AbstractVector, Colon, Integer}) = mv Base.view(mv::NoMotion, p::Union{AbstractRange, AbstractVector, Colon, Integer}) = mv """ Addition of NoMotions """ -Base.vcat(m1::NoMotion{T}, m2::AbstractMotion{T}, Ns1::Int, Ns2::Int) where {T<:Real} = m2 -Base.vcat(m1::AbstractMotion{T}, m2::NoMotion{T}, Ns1::Int, Ns2::Int) where {T<:Real} = m1 +Base.vcat(m1::NoMotion{T}, m2::AbstractMotionList{T}, Ns1::Int, Ns2::Int) where {T<:Real} = m2 +Base.vcat(m1::AbstractMotionList{T}, m2::NoMotion{T}, Ns1::Int, Ns2::Int) where {T<:Real} = m1 Base.:(==)(m1::NoMotion{T}, m2::NoMotion{T}) where {T<:Real} = true Base.:(≈)(m1::NoMotion{T}, m2::NoMotion{T}) where {T<:Real} = true diff --git a/KomaMRIBase/src/datatypes/phantom/motion/ArbitraryMotion.jl b/KomaMRIBase/src/datatypes/phantom/motion/ArbitraryMotion.jl index 8b6a2bb53..5a41f425e 100644 --- a/KomaMRIBase/src/datatypes/phantom/motion/ArbitraryMotion.jl +++ b/KomaMRIBase/src/datatypes/phantom/motion/ArbitraryMotion.jl @@ -19,13 +19,13 @@ const Interpolator2D = Interpolations.GriddedInterpolation{ """ ArbitraryMotion """ -abstract type ArbitraryMotion{T<:Real} <: Motion{T} end +abstract type ArbitraryMotion{T<:Real} <: AbstractMotion{T} end function Base.getindex(motion::ArbitraryMotion, p::Union{AbstractRange, AbstractVector, Colon, Integer}) - return typeof(motion)(motion.times, [getfield(motion, d)[p,:] for d in filter(x -> x != :times, fieldnames(typeof(motion)))]...) + return typeof(motion)(motion.time, [getfield(motion, d)[p,:] for d in filter(x -> x != :time, fieldnames(typeof(motion)))]...) end function Base.view(motion::ArbitraryMotion, p::Union{AbstractRange, AbstractVector, Colon, Integer}) - return typeof(motion)(motion.times, [@view(getfield(motion, d)[p,:]) for d in filter(x -> x != :times, fieldnames(typeof(motion)))]...) + return typeof(motion)(motion.time, [@view(getfield(motion, d)[p,:]) for d in filter(x -> x != :time, fieldnames(typeof(motion)))]...) end Base.:(==)(m1::ArbitraryMotion, m2::ArbitraryMotion) = (typeof(m1) == typeof(m2)) & reduce(&, [getfield(m1, field) == getfield(m2, field) for field in fieldnames(typeof(m1))]) @@ -68,7 +68,7 @@ function displacement_x!( t::AbstractArray{T}, ) where {T<:Real} itp = interpolate(motion.dx, Gridded(Linear()), Val(size(x,1))) - ux .= resample(itp, unit_time(t, motion.times)) + ux .= resample(itp, unit_time(t, motion.time)) return nothing end @@ -81,7 +81,7 @@ function displacement_y!( t::AbstractArray{T}, ) where {T<:Real} itp = interpolate(motion.dy, Gridded(Linear()), Val(size(x,1))) - uy .= resample(itp, unit_time(t, motion.times)) + uy .= resample(itp, unit_time(t, motion.time)) return nothing end @@ -94,7 +94,7 @@ function displacement_z!( t::AbstractArray{T}, ) where {T<:Real} itp = interpolate(motion.dz, Gridded(Linear()), Val(size(x,1))) - uz .= resample(itp, unit_time(t, motion.times)) + uz .= resample(itp, unit_time(t, motion.time)) return nothing end diff --git a/KomaMRIBase/src/datatypes/phantom/motion/SimpleMotion.jl b/KomaMRIBase/src/datatypes/phantom/motion/SimpleMotion.jl index bebd4ad06..f431d19a0 100644 --- a/KomaMRIBase/src/datatypes/phantom/motion/SimpleMotion.jl +++ b/KomaMRIBase/src/datatypes/phantom/motion/SimpleMotion.jl @@ -21,7 +21,7 @@ julia> motion = SimpleMotion( ) ``` """ -abstract type SimpleMotion{T<:Real} <: Motion{T} end +abstract type SimpleMotion{T<:Real} <: AbstractMotion{T} end Base.getindex(motion::SimpleMotion, p::Union{AbstractRange, AbstractVector, Colon, Integer}) = motion Base.view(motion::SimpleMotion, p::Union{AbstractRange, AbstractVector, Colon, Integer}) = motion diff --git a/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/FlowTrajectory.jl b/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/FlowTrajectory.jl index 56d08fb3a..c459625a4 100644 --- a/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/FlowTrajectory.jl +++ b/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/FlowTrajectory.jl @@ -1,7 +1,7 @@ -struct FlowTrajectory{T<:Real, TS<:TimeScale{T}} <: ArbitraryMotion{T} - times::TS +struct FlowTrajectory{T<:Real, TS<:AbstractTimeSpan{T}} <: ArbitraryMotion{T} + time::TS dx::AbstractArray{T} dy::AbstractArray{T} dz::AbstractArray{T} - resetmag::AbstractArray{Bool} + spin_reset::AbstractArray{Bool} end \ No newline at end of file diff --git a/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/Trajectory.jl b/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/Trajectory.jl index 0aae0b094..a059ac618 100644 --- a/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/Trajectory.jl +++ b/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/Trajectory.jl @@ -1,5 +1,5 @@ -struct Trajectory{T<:Real, TS<:TimeScale{T}} <: ArbitraryMotion{T} - times::TS +struct Trajectory{T<:Real, TS<:AbstractTimeSpan{T}} <: ArbitraryMotion{T} + time::TS dx::AbstractArray{T} dy::AbstractArray{T} dz::AbstractArray{T} diff --git a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/HeartBeat.jl b/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/HeartBeat.jl index 09a8146c4..ea1fdc211 100644 --- a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/HeartBeat.jl +++ b/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/HeartBeat.jl @@ -1,11 +1,11 @@ @doc raw""" - heartbeat = HeartBeat(times, circumferential_strain, radial_strain, longitudinal_strain) + heartbeat = HeartBeat(time, circumferential_strain, radial_strain, longitudinal_strain) HeartBeat struct. It produces a heartbeat-like motion, characterised by three types of strain: Circumferential, Radial and Longitudinal # Arguments -- `times`: (`::TimeScale{T<:Real}`, `[s]`) time scale +- `time`: (`::AbstractTimeSpan{T<:Real}`, `[s]`) time scale - `circumferential_strain`: (`::Real`) contraction parameter - `radial_strain`: (`::Real`) contraction parameter - `longitudinal_strain`: (`::Real`) contraction parameter @@ -15,11 +15,11 @@ Circumferential, Radial and Longitudinal # Examples ```julia-repl -julia> hb = HeartBeat(times=Periodic(period=1.0, asymmetry=0.3), circumferential_strain=-0.3, radial_strain=-0.2, longitudinal_strain=0.0) +julia> hb = HeartBeat(time=Periodic(period=1.0, asymmetry=0.3), circumferential_strain=-0.3, radial_strain=-0.2, longitudinal_strain=0.0) ``` """ -@with_kw struct HeartBeat{T<:Real, TS<:TimeScale{T}} <: SimpleMotion{T} - times :: TS +@with_kw struct HeartBeat{T<:Real, TS<:AbstractTimeSpan{T}} <: SimpleMotion{T} + time :: TS circumferential_strain :: T radial_strain :: T longitudinal_strain :: T = typeof(circumferential_strain)(0.0) @@ -35,7 +35,7 @@ function displacement_x!( z::AbstractArray{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion.times) + t_unit = unit_time(t, motion.time) r = sqrt.(x .^ 2 + y .^ 2) θ = atan.(y, x) Δ_circunferential = motion.circumferential_strain * maximum(r) @@ -57,7 +57,7 @@ function displacement_y!( z::AbstractArray{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion.times) + t_unit = unit_time(t, motion.time) r = sqrt.(x .^ 2 + y .^ 2) θ = atan.(y, x) Δ_circunferential = motion.circumferential_strain * maximum(r) @@ -79,7 +79,7 @@ function displacement_z!( z::AbstractArray{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion.times) + t_unit = unit_time(t, motion.time) uz .= t_unit .* (z .* motion.longitudinal_strain) return nothing end \ No newline at end of file diff --git a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Rotation.jl b/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Rotation.jl index 48628b5b8..e511f78d0 100644 --- a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Rotation.jl +++ b/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Rotation.jl @@ -1,5 +1,5 @@ @doc raw""" - rotation = Rotation(times, pitch, roll, yaw) + rotation = Rotation(time, pitch, roll, yaw) Rotation motion struct. It produces a rotation of the phantom in the three axes: x (pitch), y (roll), and z (yaw). @@ -38,7 +38,7 @@ R &= R_z(\alpha) R_y(\beta) R_x(\gamma) \\ ``` # Arguments -- `times`: (`::TimeScale{T<:Real}`, `[s]`) time scale +- `time`: (`::AbstractTimeSpan{T<:Real}`, `[s]`) time scale - `pitch`: (`::Real`, `[º]`) rotation in x - `roll`: (`::Real`, `[º]`) rotation in y - `yaw`: (`::Real`, `[º]`) rotation in z @@ -48,16 +48,20 @@ R &= R_z(\alpha) R_y(\beta) R_x(\gamma) \\ # Examples ```julia-repl -julia> rt = Rotation(times=TimeRange(0.0, 0.5), pitch=15.0, roll=0.0, yaw=20.0) +julia> rt = Rotation(time=TimeRange(0.0, 0.5), pitch=15.0, roll=0.0, yaw=20.0) ``` """ -@with_kw struct Rotation{T<:Real, TS<:TimeScale{T}} <: SimpleMotion{T} - times :: TS +@with_kw struct Rotation{T<:Real, TS<:AbstractTimeSpan{T}} <: SimpleMotion{T} + time :: TS pitch :: T roll :: T yaw :: T end +RotationX(time::AbstractTimeSpan{T}, pitch::T) where {T<:Real} = Rotation(time, pitch, zero(T), zero(T)) +RotationY(time::AbstractTimeSpan{T}, roll::T) where {T<:Real} = Rotation(time, zero(T), roll, zero(T)) +RotationZ(time::AbstractTimeSpan{T}, yaw::T) where {T<:Real} = Rotation(time, zero(T), zero(T), yaw) + is_composable(motion::Rotation) = true function displacement_x!( @@ -68,7 +72,7 @@ function displacement_x!( z::AbstractArray{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion.times) + t_unit = unit_time(t, motion.time) α = t_unit .* (motion.yaw) β = t_unit .* (motion.roll) γ = t_unit .* (motion.pitch) @@ -86,7 +90,7 @@ function displacement_y!( z::AbstractArray{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion.times) + t_unit = unit_time(t, motion.time) α = t_unit .* (motion.yaw) β = t_unit .* (motion.roll) γ = t_unit .* (motion.pitch) @@ -104,7 +108,7 @@ function displacement_z!( z::AbstractArray{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion.times) + t_unit = unit_time(t, motion.time) α = t_unit .* (motion.yaw) β = t_unit .* (motion.roll) γ = t_unit .* (motion.pitch) diff --git a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Translation.jl b/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Translation.jl index 88634f185..39949dda4 100644 --- a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Translation.jl +++ b/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Translation.jl @@ -1,12 +1,12 @@ @doc raw""" - translation = Translation(times, dx, dy, dz) + translation = Translation(time, dx, dy, dz) Translation motion struct. It produces a linear translation of the phantom. Its fields are the final displacements in the three axes (dx, dy, dz) -and the start and end times of the translation. +and the start and end time of the translation. # Arguments -- `times`: (`::TimeScale{T<:Real}`, `[s]`) time scale +- `time`: (`::AbstractTimeSpan{T<:Real}`, `[s]`) time scale - `dx`: (`::Real`, `[m]`) translation in x - `dy`: (`::Real`, `[m]`) translation in y - `dz`: (`::Real`, `[m]`) translation in z @@ -16,16 +16,20 @@ and the start and end times of the translation. # Examples ```julia-repl -julia> tr = Translation(times=TimeRange(0.0, 0.5), dx=0.01, dy=0.02, dz=0.03) +julia> tr = Translation(time=TimeRange(0.0, 0.5), dx=0.01, dy=0.02, dz=0.03) ``` """ -@with_kw struct Translation{T<:Real, TS<:TimeScale{T}} <: SimpleMotion{T} - times :: TS +@with_kw struct Translation{T<:Real, TS<:AbstractTimeSpan{T}} <: SimpleMotion{T} + time :: TS dx :: T dy :: T dz :: T end +TranslationX(time::AbstractTimeSpan{T}, dx::T) where {T<:Real} = Translation(time, dx, zero(T), zero(T)) +TranslationY(time::AbstractTimeSpan{T}, dy::T) where {T<:Real} = Translation(time, zero(T), dy, zero(T)) +TranslationZ(time::AbstractTimeSpan{T}, dz::T) where {T<:Real} = Translation(time, zero(T), zero(T), dz) + function displacement_x!( ux::AbstractArray{T}, motion::Translation{T}, @@ -34,7 +38,7 @@ function displacement_x!( z::AbstractVector{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion.times) + t_unit = unit_time(t, motion.time) ux .= t_unit .* motion.dx return nothing end @@ -47,7 +51,7 @@ function displacement_y!( z::AbstractVector{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion.times) + t_unit = unit_time(t, motion.time) uy .= t_unit .* motion.dy return nothing end @@ -60,7 +64,7 @@ function displacement_z!( z::AbstractVector{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion.times) + t_unit = unit_time(t, motion.time) uz .= t_unit .* motion.dz return nothing end diff --git a/KomaMRIBase/src/timing/TimeScale.jl b/KomaMRIBase/src/timing/TimeScale.jl index 73adb1fc7..dad8167a0 100644 --- a/KomaMRIBase/src/timing/TimeScale.jl +++ b/KomaMRIBase/src/timing/TimeScale.jl @@ -1,12 +1,12 @@ -abstract type TimeScale{T<:Real} end +abstract type AbstractTimeSpan{T<:Real} end -@with_kw struct TimeRange{T<:Real} <: TimeScale{T} +@with_kw struct TimeRange{T<:Real} <: AbstractTimeSpan{T} t_start ::T t_end ::T = t_start @assert t_end >= t_start "t_end must be greater or equal than t_start" end -@with_kw struct Periodic{T<:Real} <: TimeScale{T} +@with_kw struct Periodic{T<:Real} <: AbstractTimeSpan{T} period::T asymmetry::T = eltype(period)(0.5) end @@ -46,8 +46,7 @@ function unit_time(t::AbstractArray{T}, ts::TimeRange{T}) where {T<:Real} if ts.t_start == ts.t_end return (t .>= ts.t_start) .* oneunit(T) else - tmp = max.((t .- ts.t_start) ./ (ts.t_end - ts.t_start), zero(T)) - return min.(tmp, oneunit(T)) + return min.(max.((t .- ts.t_start) ./ (ts.t_end - ts.t_start), zero(T)), oneunit(T)) end end diff --git a/KomaMRIBase/test/runtests.jl b/KomaMRIBase/test/runtests.jl index 3b9b8aae8..9ec13403a 100644 --- a/KomaMRIBase/test/runtests.jl +++ b/KomaMRIBase/test/runtests.jl @@ -398,7 +398,7 @@ end t = collect(range(t_start, t_end, 11)) dx, dy, dz = [1.0, 0.0, 0.0] vx, vy, vz = [dx, dy, dz] ./ (t_end - t_start) - translation = MotionVector(Translation(TimeRange(t_start, t_end), dx, dy, dz)) + translation = MotionList(Translation(TimeRange(t_start, t_end), dx, dy, dz)) xt, yt, zt = get_spin_coords(translation, ph.x, ph.y, ph.z, t') @test xt == ph.x .+ vx.*t' @test yt == ph.y .+ vy.*t' @@ -406,7 +406,7 @@ end # ----- t_start = t_end -------- t_start = t_end = 0.0 t = [-0.5, -0.25, 0.0, 0.25, 0.5] - translation = MotionVector(Translation(TimeRange(t_start, t_end), dx, dy, dz)) + translation = MotionList(Translation(TimeRange(t_start, t_end), dx, dy, dz)) xt, yt, zt = get_spin_coords(translation, ph.x, ph.y, ph.z, t') @test xt == ph.x .+ dx*[0, 0, 1, 1, 1]' @test yt == ph.y .+ dy*[0, 0, 1, 1, 1]' @@ -420,7 +420,7 @@ end asymmetry = 0.5 dx, dy, dz = [1.0, 0.0, 0.0] vx, vy, vz = [dx, dy, dz] ./ (t_end - t_start) - periodictranslation = MotionVector(Translation(Periodic(period, asymmetry), dx, dy, dz)) + periodictranslation = MotionList(Translation(Periodic(period, asymmetry), dx, dy, dz)) xt, yt, zt = get_spin_coords(periodictranslation, ph.x, ph.y, ph.z, t') @test xt == ph.x .+ vx.*t' @test yt == ph.y .+ vy.*t' @@ -433,7 +433,7 @@ end pitch = 45.0 roll = 0.0 yaw = 45.0 - rotation = MotionVector(Rotation(TimeRange(t_start, t_end), pitch, roll, yaw)) + rotation = MotionList(Rotation(TimeRange(t_start, t_end), pitch, roll, yaw)) xt, yt, zt = get_spin_coords(rotation, ph.x, ph.y, ph.z, t') r = vcat(ph.x, ph.y, ph.z) R = rotz(π*yaw/180) * roty(π*roll/180) * rotx(π*pitch/180) @@ -444,7 +444,7 @@ end # ----- t_start = t_end -------- t_start = t_end = 0.0 t = [-0.5, -0.25, 0.0, 0.25, 0.5] - rotation = MotionVector(Rotation(TimeRange(t_start, t_end), pitch, roll, yaw)) + rotation = MotionList(Rotation(TimeRange(t_start, t_end), pitch, roll, yaw)) xt, yt, zt = get_spin_coords(rotation, ph.x, ph.y, ph.z, t') @test xt ≈ [ph.x ph.x rot_x rot_x rot_x] @test yt ≈ [ph.y ph.y rot_y rot_y rot_y] @@ -459,7 +459,7 @@ end pitch = 45.0 roll = 0.0 yaw = 45.0 - periodicrotation = MotionVector(Rotation(Periodic(period, asymmetry), pitch, roll, yaw)) + periodicrotation = MotionList(Rotation(Periodic(period, asymmetry), pitch, roll, yaw)) xt, yt, zt = get_spin_coords(periodicrotation, ph.x, ph.y, ph.z, t') r = vcat(ph.x, ph.y, ph.z) R = rotz(π*yaw/180) * roty(π*roll/180) * rotx(π*pitch/180) @@ -475,7 +475,7 @@ end circumferential_strain = -0.1 radial_strain = 0.0 longitudinal_strain = -0.1 - heartbeat = MotionVector(HeartBeat(TimeRange(t_start, t_end), circumferential_strain, radial_strain, longitudinal_strain)) + heartbeat = MotionList(HeartBeat(TimeRange(t_start, t_end), circumferential_strain, radial_strain, longitudinal_strain)) xt, yt, zt = get_spin_coords(heartbeat, ph.x, ph.y, ph.z, t') r = sqrt.(ph.x .^ 2 + ph.y .^ 2) θ = atan.(ph.y, ph.x) @@ -485,7 +485,7 @@ end # ----- t_start = t_end -------- t_start = t_end = 0.0 t = [-0.5, -0.25, 0.0, 0.25, 0.5] - heartbeat = MotionVector(HeartBeat(TimeRange(t_start, t_end), circumferential_strain, radial_strain, longitudinal_strain)) + heartbeat = MotionList(HeartBeat(TimeRange(t_start, t_end), circumferential_strain, radial_strain, longitudinal_strain)) xt, yt, zt = get_spin_coords(heartbeat, ph.x, ph.y, ph.z, t') r = sqrt.(ph.x .^ 2 + ph.y .^ 2) θ = atan.(ph.y, ph.x) @@ -505,7 +505,7 @@ end circumferential_strain = -0.1 radial_strain = 0.0 longitudinal_strain = -0.1 - periodicheartbeat = MotionVector(HeartBeat(Periodic(period, asymmetry), circumferential_strain, radial_strain, longitudinal_strain)) + periodicheartbeat = MotionList(HeartBeat(Periodic(period, asymmetry), circumferential_strain, radial_strain, longitudinal_strain)) xt, yt, zt = get_spin_coords(periodicheartbeat, ph.x, ph.y, ph.z, t') r = sqrt.(ph.x .^ 2 + ph.y .^ 2) θ = atan.(ph.y, ph.x) @@ -523,7 +523,7 @@ end dx = rand(Ns, Nt) dy = rand(Ns, Nt) dz = rand(Ns, Nt) - arbitrarymotion = MotionVector(Trajectory(TimeRange(t_start, t_end), dx, dy, dz)) + arbitrarymotion = MotionList(Trajectory(TimeRange(t_start, t_end), dx, dy, dz)) t = range(t_start, t_end, Nt) xt, yt, zt = get_spin_coords(arbitrarymotion, ph.x, ph.y, ph.z, t') @test xt == ph.x .+ dx @@ -538,7 +538,7 @@ end dx = rand(Ns, Nt) dy = rand(Ns, Nt) dz = rand(Ns, Nt) - arbitrarymotion = MotionVector(Trajectory(TimeRange(t_start, t_end), dx, dy, dz)) + arbitrarymotion = MotionList(Trajectory(TimeRange(t_start, t_end), dx, dy, dz)) t = range(t_start, t_end, Nt) xt, yt, zt = get_spin_coords(arbitrarymotion, ph.x, ph.y, ph.z, t') @test xt == ph.x .+ dx @@ -546,16 +546,16 @@ end @test zt == ph.z .+ dz end - simplemotion = MotionVector( - Translation(times=Periodic(period=0.5, asymmetry=0.5), dx=0.05, dy=0.05, dz=0.0), - Rotation(times=TimeRange(t_start=0.05, t_end=0.5), pitch=0.0, roll=0.0, yaw=π / 2) + simplemotion = MotionList( + Translation(time=Periodic(period=0.5, asymmetry=0.5), dx=0.05, dy=0.05, dz=0.0), + Rotation(time=TimeRange(t_start=0.05, t_end=0.5), pitch=0.0, roll=0.0, yaw=π / 2) ) Ns = length(obj1) Nt = 3 t_start = 0.0 t_end = 1.0 - arbitrarymotion = MotionVector(Trajectory(TimeRange(t_start, t_end), 0.01 .* rand(Ns, Nt), 0.01 .* rand(Ns, Nt), 0.01 .* rand(Ns, Nt))) + arbitrarymotion = MotionList(Trajectory(TimeRange(t_start, t_end), 0.01 .* rand(Ns, Nt), 0.01 .* rand(Ns, Nt), 0.01 .* rand(Ns, Nt))) # Test phantom subset obs1 = Phantom( diff --git a/KomaMRICore/src/simulation/Flow.jl b/KomaMRICore/src/simulation/Flow.jl index 6b74b8bfd..78af0d1df 100644 --- a/KomaMRICore/src/simulation/Flow.jl +++ b/KomaMRICore/src/simulation/Flow.jl @@ -5,20 +5,20 @@ function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion: return nothing end -function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion::MotionVector{T}, t::AbstractArray{T}) where {T<:Real} +function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion::MotionList{T}, t::AbstractArray{T}) where {T<:Real} for m in motion.motions reset_magnetization!(M, Mxy, m, t) end return nothing end -function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion::Motion{T}, t::AbstractArray{T}) where {T<:Real} +function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion::AbstractMotion{T}, t::AbstractArray{T}) where {T<:Real} return nothing end function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion::FlowTrajectory{T}, t::AbstractArray{T}) where {T<:Real} - itp = interpolate(motion.resetmag, Gridded(Constant{Previous}), Val(size(x,1))) - flags = resample(itp, unit_time(t, motion.times)) + itp = interpolate(motion.spin_reset, Gridded(Constant{Previous}), Val(size(x,1))) + flags = resample(itp, unit_time(t, motion.time)) reset = any(flags; dims=2) flags = .!(cumsum(flags; dims=2) .>= 1) Mxy .*= flags diff --git a/KomaMRICore/src/simulation/Functors.jl b/KomaMRICore/src/simulation/Functors.jl index 0354b763e..3217ab0a3 100644 --- a/KomaMRICore/src/simulation/Functors.jl +++ b/KomaMRICore/src/simulation/Functors.jl @@ -52,7 +52,7 @@ x = gpu(x, CUDABackend()) ``` """ gpu(x, backend::KA.GPU) = fmap(x -> adapt(backend, x), x; exclude=_isleaf) -adapt_storage(backend::KA.GPU, xs::MotionVector) = MotionVector(gpu.(xs.motions, Ref(backend))) +adapt_storage(backend::KA.GPU, xs::MotionList) = MotionList(gpu.(xs.motions, Ref(backend))) # To CPU """ @@ -76,7 +76,7 @@ adapt_storage(T::Type{<:Real}, xs::AbstractArray{<:Real}) = convert.(T, xs) adapt_storage(T::Type{<:Real}, xs::AbstractArray{<:Complex}) = convert.(Complex{T}, xs) adapt_storage(T::Type{<:Real}, xs::AbstractArray{<:Bool}) = xs adapt_storage(T::Type{<:Real}, xs::NoMotion) = NoMotion{T}() -adapt_storage(T::Type{<:Real}, xs::MotionVector) = MotionVector(paramtype.(T, xs.motions)) +adapt_storage(T::Type{<:Real}, xs::MotionList) = MotionList(paramtype.(T, xs.motions)) """ f32(m) diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index 69333e2c2..554cb6354 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -388,6 +388,49 @@ end @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end +@testitem "BlochSimple SimpleMotion" tags=[:important, :core, :motion] begin + using Suppressor + include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) + + sig_jemris = signal_brain_motion_jemris() + seq = seq_epi_100x100_TE100_FOV230() + sys = Scanner() + obj = phantom_brain_simple_motion() + + sim_params = Dict{String, Any}( + "gpu"=>USE_GPU, + "sim_method"=>KomaMRICore.BlochSimple(), + "return_type"=>"mat" + ) + sig = @suppress simulate(obj, seq, sys; sim_params) + sig = sig / prod(size(obj)) + NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% +end + +@testitem "BlochSimple ArbitraryMotion" tags=[:important, :core, :motion] begin + using Suppressor + include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) + + sig_jemris = signal_brain_motion_jemris() + seq = seq_epi_100x100_TE100_FOV230() + sys = Scanner() + obj = phantom_brain_arbitrary_motion() + + sim_params = Dict{String, Any}( + "gpu"=>USE_GPU, + "sim_method"=>KomaMRICore.BlochSimple(), + "return_type"=>"mat" + ) + sig = @suppress simulate(obj, seq, sys; sim_params) + sig = sig / prod(size(obj)) + NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% +end + + @testitem "simulate_slice_profile" tags=[:core] begin using Suppressor include("initialize_backend.jl") diff --git a/KomaMRICore/test/test_files/utils.jl b/KomaMRICore/test/test_files/utils.jl index 7bc9d7caf..a194a1a16 100644 --- a/KomaMRICore/test/test_files/utils.jl +++ b/KomaMRICore/test/test_files/utils.jl @@ -18,7 +18,7 @@ end function phantom_brain_simple_motion() obj = phantom_brain() - obj.motion = MotionVector(Translation(times=TimeRange(0.0, 10.0), dx=0.0, dy=1.0, dz=0.0)) + obj.motion = MotionList(Translation(time=TimeRange(0.0, 10.0), dx=0.0, dy=1.0, dz=0.0)) return obj end @@ -30,7 +30,7 @@ function phantom_brain_arbitrary_motion() dx = zeros(Ns, 2) dz = zeros(Ns, 2) dy = [zeros(Ns,1) ones(Ns,1)] - obj.motion = MotionVector(Trajectory( + obj.motion = MotionList(Trajectory( TimeRange(t_start, t_end), dx, dy, diff --git a/KomaMRIFiles/src/Phantom/Phantom.jl b/KomaMRIFiles/src/Phantom/Phantom.jl index 51ea57af9..b695ed2ac 100644 --- a/KomaMRIFiles/src/Phantom/Phantom.jl +++ b/KomaMRIFiles/src/Phantom/Phantom.jl @@ -23,7 +23,7 @@ function read_phantom(filename::String) end end end - # Motion + # AbstractMotion motion_group = fid["motion"] import_motion!(phantom_fields, motion_group) @@ -56,10 +56,10 @@ end function import_motion!(phantom_fields::Array, motion_group::HDF5.Group) T = eltype(phantom_fields[2][2]) motion_type = read_attribute(motion_group, "type") - if motion_type == "MotionVector" - simple_motion_types = last.(split.(string.(reduce(vcat,(subtypes(subtypes(Motion)[2])))), ".")) - arbitrary_motion_types = last.(split.(string.(reduce(vcat,(subtypes(subtypes(Motion)[1])))), ".")) - motion_array = Motion{T}[] + if motion_type == "MotionList" + simple_motion_types = last.(split.(string.(reduce(vcat,(subtypes(subtypes(AbstractMotion)[2])))), ".")) + arbitrary_motion_types = last.(split.(string.(reduce(vcat,(subtypes(subtypes(AbstractMotion)[1])))), ".")) + motion_array = AbstractMotion{T}[] for key in keys(motion_group) type_group = motion_group[key] type_str = split(key, "_")[2] @@ -67,26 +67,26 @@ function import_motion!(phantom_fields::Array, motion_group::HDF5.Group) args = [] for smtype in subtypes(SimpleMotion) if type_str == last(split(string(smtype), ".")) - times = import_time_range(type_group["times"]) - type_fields = filter(x -> x != :times, fieldnames(smtype)) + time = import_time_range(type_group["time"]) + type_fields = filter(x -> x != :time, fieldnames(smtype)) for key in type_fields push!(args, read_attribute(type_group, string(key))) end - push!(motion_array, smtype(times, args...)) + push!(motion_array, smtype(time, args...)) end end for amtype in subtypes(ArbitraryMotion) if type_str == last(split(string(amtype), ".")) - times = import_time_range(type_group["times"]) - type_fields = filter(x -> x != :times, fieldnames(amtype)) + time = import_time_range(type_group["time"]) + type_fields = filter(x -> x != :time, fieldnames(amtype)) for key in type_fields push!(args, read(type_group[string(key)])) end - push!(motion_array, amtype(times, args...)) + push!(motion_array, amtype(time, args...)) end end end - return push!(phantom_fields, (:motion, MotionVector(motion_array))) + return push!(phantom_fields, (:motion, MotionList(motion_array))) elseif motion_type == "NoMotion" return push!(phantom_fields, (:motion, NoMotion{T}())) end @@ -94,7 +94,7 @@ end function import_time_range(times_group::HDF5.Group) time_scale_type = read_attribute(times_group, "type") - for tstype in subtypes(TimeScale) + for tstype in subtypes(AbstractTimeSpan) if time_scale_type == last(split(string(tstype), ".")) args = [] for key in filter(x -> x != :type, fieldnames(tstype)) @@ -147,13 +147,13 @@ function write_phantom( return close(fid) end -function export_motion!(motion_group::HDF5.Group, mv::MotionVector{T}) where {T<:Real} - HDF5.attributes(motion_group)["type"] = "MotionVector" +function export_motion!(motion_group::HDF5.Group, mv::MotionList{T}) where {T<:Real} + HDF5.attributes(motion_group)["type"] = "MotionList" for (counter, m) in enumerate(mv.motions) type_name = typeof(m).name.name type_group = create_group(motion_group, "$(counter)_$type_name") - export_time_range!(type_group, m.times) - type_fields = filter(x -> x != :times, fieldnames(typeof(m))) + export_time_range!(type_group, m.time) + type_fields = filter(x -> x != :time, fieldnames(typeof(m))) for field in type_fields field_value = getfield(m, field) if typeof(field_value) <: Number @@ -169,12 +169,12 @@ function export_motion!(motion_group::HDF5.Group, motion::NoMotion{T}) where {T< HDF5.attributes(motion_group)["type"] = "NoMotion" end -function export_time_range!(type_group::HDF5.Group, times::TimeScale) - times_name = typeof(times).name.name - times_group = create_group(type_group, "times") +function export_time_range!(type_group::HDF5.Group, time::AbstractTimeSpan) + times_name = typeof(time).name.name + times_group = create_group(type_group, "time") HDF5.attributes(times_group)["type"] = string(times_name) - for field in fieldnames(typeof(times)) - field_value = getfield(times, field) + for field in fieldnames(typeof(time)) + field_value = getfield(time, field) HDF5.attributes(times_group)[string(field)] = field_value end end \ No newline at end of file diff --git a/KomaMRIFiles/test/runtests.jl b/KomaMRIFiles/test/runtests.jl index dad477288..b9dbda971 100644 --- a/KomaMRIFiles/test/runtests.jl +++ b/KomaMRIFiles/test/runtests.jl @@ -64,14 +64,14 @@ using TestItems, TestItemRunner path = @__DIR__ filename = path * "/test_files/brain_simplemotion_w.phantom" obj1 = brain_phantom2D() - obj1.motion = MotionVector( + obj1.motion = MotionList( Rotation( - times=Periodic(period=1.0), + time=Periodic(period=1.0), yaw=45.0, pitch=0.0, roll=0.0), Translation( - times=TimeRange(t_start=0.0, t_end=0.5), + time=TimeRange(t_start=0.0, t_end=0.5), dx=0.0, dy=0.02, dz=0.0 @@ -90,7 +90,7 @@ using TestItems, TestItemRunner K = 10 t_start = 0.0 t_end = 1.0 - obj1.motion = MotionVector(Trajectory( + obj1.motion = MotionList(Trajectory( TimeRange(t_start, t_end), 0.01.*rand(Ns, K-1), 0.01.*rand(Ns, K-1), diff --git a/examples/3.tutorials/lit-05-SimpleMotion.jl b/examples/3.tutorials/lit-05-SimpleMotion.jl index 93b3da121..22fc9659d 100644 --- a/examples/3.tutorials/lit-05-SimpleMotion.jl +++ b/examples/3.tutorials/lit-05-SimpleMotion.jl @@ -19,8 +19,8 @@ obj.Δw .= 0 # hide # # In this example, we will add a [`Translation`](@ref) of 2 cm in x, with duration of 200 ms (v = 0.1 m/s): -obj.motion = SimpleMotion( - Translation(t_start=0.0, t_end=200e-3, dx=2e-2, dy=0.0, dz=0.0) +obj.motion = MotionList( + Translation(time=TimeRange(t_start=0.0, t_end=200e-3), dx=2e-2, dy=0.0, dz=0.0) ) p1 = plot_phantom_map(obj, :T2 ; height=450, intermediate_time_samples=4) # hide From 79596adb1b289efbf6ffc943a6f24e2fa86f17bb Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Fri, 9 Aug 2024 20:29:34 +0200 Subject: [PATCH 07/91] Solve bug --- KomaMRIBase/src/KomaMRIBase.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KomaMRIBase/src/KomaMRIBase.jl b/KomaMRIBase/src/KomaMRIBase.jl index ca76a7462..7ca7f7f09 100644 --- a/KomaMRIBase/src/KomaMRIBase.jl +++ b/KomaMRIBase/src/KomaMRIBase.jl @@ -46,7 +46,7 @@ export kfoldperm, trapz, cumtrapz # Phantom export brain_phantom2D, brain_phantom3D, pelvis_phantom2D, heart_phantom # Motion -export MotionList, NoMotion +export AbstractMotion, MotionList, NoMotion export SimpleMotion, ArbitraryMotion export Translation, TranslationX, TranslationY, TranslationZ export Rotation, RotationX, RotationY, RotationZ From ea91613582e05d88d5b84c2fa4147b1382011622 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Fri, 9 Aug 2024 20:51:17 +0200 Subject: [PATCH 08/91] Solve bug --- KomaMRIPlots/src/ui/DisplayFunctions.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/KomaMRIPlots/src/ui/DisplayFunctions.jl b/KomaMRIPlots/src/ui/DisplayFunctions.jl index 5636f386f..c4c9ff9fc 100644 --- a/KomaMRIPlots/src/ui/DisplayFunctions.jl +++ b/KomaMRIPlots/src/ui/DisplayFunctions.jl @@ -1033,7 +1033,7 @@ function plot_phantom_map( kwargs..., ) - function interpolate_times(motion::AbstractMotion{T}) where {T<:Real} + function interpolate_times(motion::AbstractMotionList{T}) where {T<:Real} t = times(motion) if length(t)>1 # Interpolate time points (as many as indicated by intermediate_time_samples) @@ -1043,7 +1043,7 @@ function plot_phantom_map( return t end - function process_times(motion::AbstractMotion{T}) where {T<:Real} + function process_times(motion::AbstractMotionList{T}) where {T<:Real} sort_motions!(motion) t = interpolate_times(motion) # Decimate time points so their number is smaller than max_time_samples From e10660faf054fdd52bd4a6e607d1b02e7c76babe Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 12 Aug 2024 11:58:35 +0200 Subject: [PATCH 09/91] Export `AbstractMotionList` --- KomaMRIBase/src/KomaMRIBase.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KomaMRIBase/src/KomaMRIBase.jl b/KomaMRIBase/src/KomaMRIBase.jl index 7ca7f7f09..7da23d858 100644 --- a/KomaMRIBase/src/KomaMRIBase.jl +++ b/KomaMRIBase/src/KomaMRIBase.jl @@ -46,7 +46,7 @@ export kfoldperm, trapz, cumtrapz # Phantom export brain_phantom2D, brain_phantom3D, pelvis_phantom2D, heart_phantom # Motion -export AbstractMotion, MotionList, NoMotion +export AbstractMotion, AbstractMotionList, MotionList, NoMotion export SimpleMotion, ArbitraryMotion export Translation, TranslationX, TranslationY, TranslationZ export Rotation, RotationX, RotationY, RotationZ From 211337f461374e6f3a5fb2b6ea3f753d5248dd24 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 12 Aug 2024 12:20:21 +0200 Subject: [PATCH 10/91] Remove periodic motions from docs --- docs/src/reference/2-koma-base.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/src/reference/2-koma-base.md b/docs/src/reference/2-koma-base.md index 9d37e4797..4782379ed 100644 --- a/docs/src/reference/2-koma-base.md +++ b/docs/src/reference/2-koma-base.md @@ -38,9 +38,6 @@ SimpleMotion Translation Rotation HeartBeat -PeriodicTranslation -PeriodicRotation -PeriodicHeartBeat ``` ### `ArbitraryMotion <: MotionModel` From c78838e1c42ee508237a0aa8921b0466abe4d6d4 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 12 Aug 2024 12:31:04 +0200 Subject: [PATCH 11/91] Update `contracting_ring.phantom` example --- examples/2.phantoms/contracting_ring.phantom | Bin 1046688 -> 1047608 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/examples/2.phantoms/contracting_ring.phantom b/examples/2.phantoms/contracting_ring.phantom index e5c7fb7c18d563593dd3788c25c4b1e7d6dadc1c..9934c78bc741206c60db0332ceb8e49f658fad66 100644 GIT binary patch delta 1117 zcmZuwO=ufO6y7&8JK7m#S; zd*`})>jM1+9^C9_?-M9l1wIZDYmE=Uf(4-gNZlL^IfglFN+2v+Q90$Tzmxs+?!(*G zPx1~)$ZGA_w{zk8To^9dqRELHB)wc7CXmKam5f6hmsIjPYZ9VZz49_`N@9I4`vMHw zP>H37;vyjrUtYkfAYZ}>+!y3knhFs-!TDJ*@w_A_;RarlBn}b$QIb9=&EAuweoxZ- zHZ>&+(CfbwcALOFUQ^ju(5?e_vZSAA%=%hfhZt^XtV|by<6j*S7Y*iVXIeD}as`(K z8}A(v-eJ<$E}}%-)ynLkWc5L*uG@4`@P8r>5WYkpiR+9nQ4O@f;E44bQMvVk#vxy? za_$gmYTWvQ=OBfnf**%z%n05OE3;+6of&ebRxbzKbXPbf%j2heVuF+ejvR<%uY7#^sj8~_>$as&;UmUlz0v^;`uD@hVqx;)2ZgIeP&7QV ztse)sW?0(RE72fi+BRGgBe<@zH^k~QZ@bz}2J<_5)=GwxJ%M-Sxo1{jcGM~Cs8lm{ z^+sQP`iWt(@M6oergwCFtg*MWbnZ%F^7F!Z{Pnri;+8+Ts<^L8;; delta 1060 zcmZ`%L1-IC6x~0wJK8_%MT(tLH#AtbF&3dDN-mWN#7Zww912PcCAY{Vs%2tDvMWfO zLn2X(9YR22TYh?zPf1Qegs`S3gG=eD^(7D=3i7GWC9xaZlBsYy-j$vD&>4nd{{Qpd z|L^~W=5TdmxEx{CX63{;uvRNQW?jDt&09(T(mJ$=bCw@FVyHK?ZPAmllV_Zp{AYZH zz9~^F4Kv6)lVS`^XGNTXM-B!~Vbt>dNzY$&N=Q(3K9SSBXleA&5Iy{}dnf3bA$|c_ znLqk{*>cMk|PG+ zeSSngA)8>`0eXwQkr>WAP< zwYeCBGF1%y7OtDRS5?s~DoK}3oghT7O}zwFx`FxtEK>#bLB4=GT{2i2V$>`3k6~eU zNb2s8G*%kCisCSZyTC+*L4;Oy@f0+AV2Iv*b(VOyAsj>R$5hjo+*iC#tEQ4XN*7Ta zhubuUqF1rpN1;wY9n-y!_}3fA&}0*z@{}e&kPh$QbN-}>ltl%+*7`4Sibh+Qg*^Sy z!tT!IPP@+`E##FRr=6C9TWnY#;8YWrR3;C0aR5?O+r>e6MJv0QWorg4xA3U``wzFu zlzNYMcznZdvu@fRVA45l4gMIFoxJ`YEA#y`899@&W-=WwGWN&@4>#;XL%hX{dzo`pGbe1D~#uEdhwtI=cu?K#_r4H|e+NBF5ol!FzUzd!iCV+AC7e~!(no*K_xAHSAJk4_c7NRJjG+xF~vH5BKi@omGMdlO9PSY7u4k~IG=_?ANNg2lf7 zOCZrEy8^D{>>N`=y|Ep Date: Tue, 13 Aug 2024 14:03:03 +0200 Subject: [PATCH 12/91] Recover `tmp` variable for now --- KomaMRIBase/src/timing/TimeScale.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/KomaMRIBase/src/timing/TimeScale.jl b/KomaMRIBase/src/timing/TimeScale.jl index dad8167a0..ef4de6604 100644 --- a/KomaMRIBase/src/timing/TimeScale.jl +++ b/KomaMRIBase/src/timing/TimeScale.jl @@ -46,7 +46,8 @@ function unit_time(t::AbstractArray{T}, ts::TimeRange{T}) where {T<:Real} if ts.t_start == ts.t_end return (t .>= ts.t_start) .* oneunit(T) else - return min.(max.((t .- ts.t_start) ./ (ts.t_end - ts.t_start), zero(T)), oneunit(T)) + tmp = max.((t .- ts.t_start) ./ (ts.t_end - ts.t_start), zero(T)) + return min.(tmp, oneunit(T)) end end From a21e3d7346d355e45bd376e1b7fbd99632924035 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Tue, 13 Aug 2024 14:03:15 +0200 Subject: [PATCH 13/91] Solve docs bug --- examples/3.tutorials/lit-05-SimpleMotion.jl | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/3.tutorials/lit-05-SimpleMotion.jl b/examples/3.tutorials/lit-05-SimpleMotion.jl index 22fc9659d..f1e6c4f7b 100644 --- a/examples/3.tutorials/lit-05-SimpleMotion.jl +++ b/examples/3.tutorials/lit-05-SimpleMotion.jl @@ -4,17 +4,13 @@ using KomaMRI # hide sys = Scanner() # hide # It can also be interesting to see the effect of the patient's motion during an MRI scan. -# For this, Koma provides the ability to add `motion <: MotionModel` to the phantom. -# In this tutorial, we will show how to add a [`SimpleMotion`](@ref) model to a 2D brain phantom. +# For this, Koma provides the ability to add `motion <: AbstractMotionList` to the phantom. +# In this tutorial, we will show how to add a [`Translation`](@ref) motion to a 2D brain phantom. # First, let's load the 2D brain phantom used in the previous tutorials: obj = brain_phantom2D() obj.Δw .= 0 # hide -# The `SimpleMotion` model includes a list of `SimpleMotionType`'s, to enabling mix-and-matching simple motions. -# These are [`Translation`](@ref), [`Rotation`](@ref), [`HeartBeat`](@ref) and their periodic versions -# [`PeriodicTranslation`](@ref), [`PeriodicRotation`](@ref) and [`PeriodicHeartBeat`](@ref). - # ### Head Translation # # In this example, we will add a [`Translation`](@ref) of 2 cm in x, with duration of 200 ms (v = 0.1 m/s): From 76895c2d885b300a07135b112bcda00bd483d689 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Tue, 13 Aug 2024 14:08:40 +0200 Subject: [PATCH 14/91] Update docs to pass CI --- docs/src/reference/2-koma-base.md | 16 ++++++++++++---- docs/src/reference/3-koma-core.md | 1 + 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/src/reference/2-koma-base.md b/docs/src/reference/2-koma-base.md index 4782379ed..cf6cd8536 100644 --- a/docs/src/reference/2-koma-base.md +++ b/docs/src/reference/2-koma-base.md @@ -20,19 +20,20 @@ pelvis_phantom2D heart_phantom ``` -### `MotionModel`-related functions +### `MotionList`-related functions ```@docs +sort_motions! get_spin_coords ``` -### `SimpleMotion <: MotionModel` +### `SimpleMotion <: AbstractMotion` ```@docs SimpleMotion ``` -### `SimpleMotion types` +### `SimpleMotion` types ```@docs Translation @@ -40,12 +41,19 @@ Rotation HeartBeat ``` -### `ArbitraryMotion <: MotionModel` +### `ArbitraryMotion <: AbstractMotion` ```@docs ArbitraryMotion ``` +### `ArbitraryMotion` types + +```@docs +Trajectory +FlowTrajectory +``` + ## `Sequence`-related functions ```@docs diff --git a/docs/src/reference/3-koma-core.md b/docs/src/reference/3-koma-core.md index 5675091c7..5e8f64f03 100644 --- a/docs/src/reference/3-koma-core.md +++ b/docs/src/reference/3-koma-core.md @@ -10,6 +10,7 @@ CurrentModule = KomaMRICore simulate simulate_slice_profile default_sim_params +reset_magnetization! ``` ## GPU helper functions From 056c615b989d817f2d9778b616c41cb9bbfed39f Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Wed, 14 Aug 2024 13:21:12 +0200 Subject: [PATCH 15/91] Docstrings to pass CI --- .../motion/arbitrarymotion/FlowTrajectory.jl | 20 +++++++++++++++++++ .../motion/arbitrarymotion/Trajectory.jl | 19 ++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/FlowTrajectory.jl b/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/FlowTrajectory.jl index c459625a4..254941a01 100644 --- a/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/FlowTrajectory.jl +++ b/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/FlowTrajectory.jl @@ -1,3 +1,23 @@ +@doc raw""" + flowtrajectory = FlowTrajectory(time, dx, dy, dz) + +FlowTrajectory motion struct. (...) + +# Arguments +- `time`: (`::AbstractTimeSpan{T<:Real}`, `[s]`) time scale +- `dx`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in x +- `dy`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in y +- `dz`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in z +- `spin_reset`: (`::AbstractArray{Bool}`) reset spin state flags + +# Returns +- `flowtrajectory`: (`::FlowTrajectory`) FlowTrajectory struct + +# Examples +```julia-repl +julia> ftr = FlowTrajectory(time=TimeRange(0.0, 0.5), dx=[0.01 0.02], dy=[0.02 0.03], dz=[0.03 0.04], spin_reset=[false, false]) +``` +""" struct FlowTrajectory{T<:Real, TS<:AbstractTimeSpan{T}} <: ArbitraryMotion{T} time::TS dx::AbstractArray{T} diff --git a/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/Trajectory.jl b/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/Trajectory.jl index a059ac618..79c521452 100644 --- a/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/Trajectory.jl +++ b/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/Trajectory.jl @@ -1,3 +1,22 @@ +@doc raw""" + trajectory = Trajectory(time, dx, dy, dz) + +Trajectory motion struct. (...) + +# Arguments +- `time`: (`::AbstractTimeSpan{T<:Real}`, `[s]`) time scale +- `dx`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in x +- `dy`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in y +- `dz`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in z + +# Returns +- `trajectory`: (`::Trajectory`) Trajectory struct + +# Examples +```julia-repl +julia> tr = Trajectory(time=TimeRange(0.0, 0.5), dx=[0.01 0.02], dy=[0.02 0.03], dz=[0.03 0.04]) +``` +""" struct Trajectory{T<:Real, TS<:AbstractTimeSpan{T}} <: ArbitraryMotion{T} time::TS dx::AbstractArray{T} From 4901df75b759ec1fedfdaab6c1e399842d417b00 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 19 Aug 2024 18:49:25 +0200 Subject: [PATCH 16/91] Test `get_spin_coords` in core --- KomaMRICore/test/runtests.jl | 50 +++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index 554cb6354..bfbc961df 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -36,7 +36,7 @@ using TestItems, TestItemRunner #Environment variable set by CI const CI = get(ENV, "CI", nothing) -@run_package_tests filter=ti->(:core in ti.tags)&&(isnothing(CI) || :skipci ∉ ti.tags) #verbose=true +@run_package_tests filter=ti->(:spincoords in ti.tags)&&(isnothing(CI) || :skipci ∉ ti.tags) #verbose=true @testitem "Spinors×Mag" tags=[:core] begin using KomaMRICore: Rx, Ry, Rz, Q, rotx, roty, rotz, Un, Rφ, Rg @@ -341,6 +341,54 @@ end @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end +@testitem "getSpinCoords" tags=[:important, :core, :motion, :spincoords] begin + using Suppressor + include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) + + # 1 spin + ph = Phantom(x=[1.0], y=[1.0]) + Ns = length(ph) + t_start = 0.0 + t_end = 1.0 + Nt = 10 + dx = rand(Ns, Nt) + dy = rand(Ns, Nt) + dz = rand(Ns, Nt) + ph.motion = MotionList(Trajectory(TimeRange(t_start, t_end), dx, dy, dz)) + t = collect(range(t_start, t_end, Nt)) + ph = ph |> gpu + t = t |> gpu + xt, yt, zt = get_spin_coords(ph.motion, ph.x, ph.y, ph.z, t') + dx = dx |> gpu + dy = dy |> gpu + dz = dz |> gpu + @test xt == ph.x .+ dx + @test yt == ph.y .+ dy + @test zt == ph.z .+ dz + + # More than 1 spin + ph = Phantom(x=[1.0, 2.0], y=[1.0, 2.0]) + Ns = length(ph) + t_start = 0.0 + t_end = 1.0 + Nt = 10 + dx = rand(Ns, Nt) + dy = rand(Ns, Nt) + dz = rand(Ns, Nt) + ph.motion = MotionList(Trajectory(TimeRange(t_start, t_end), dx, dy, dz)) + t = collect(range(t_start, t_end, Nt)) + ph = ph |> gpu + t = t |> gpu + xt, yt, zt = get_spin_coords(ph.motion, ph.x, ph.y, ph.z, t') + dx = dx |> gpu + dy = dy |> gpu + dz = dz |> gpu + @test xt == ph.x .+ dx + @test yt == ph.y .+ dy + @test zt == ph.z .+ dz +end + @testitem "BlochDict" tags=[:important, :core] begin using Suppressor include("initialize_backend.jl") From 130a7f1f7c5464248b7081f3ec71dbd7be01d1b7 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 19 Aug 2024 19:02:51 +0200 Subject: [PATCH 17/91] Continue testing --- KomaMRICore/test/runtests.jl | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index bfbc961df..b1c256ecd 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -357,12 +357,12 @@ end dz = rand(Ns, Nt) ph.motion = MotionList(Trajectory(TimeRange(t_start, t_end), dx, dy, dz)) t = collect(range(t_start, t_end, Nt)) - ph = ph |> gpu - t = t |> gpu + ph = ph |> gpu |> f32 + t = t |> gpu |> f32 xt, yt, zt = get_spin_coords(ph.motion, ph.x, ph.y, ph.z, t') - dx = dx |> gpu - dy = dy |> gpu - dz = dz |> gpu + dx = dx |> gpu |> f32 + dy = dy |> gpu |> f32 + dz = dz |> gpu |> f32 @test xt == ph.x .+ dx @test yt == ph.y .+ dy @test zt == ph.z .+ dz @@ -378,12 +378,12 @@ end dz = rand(Ns, Nt) ph.motion = MotionList(Trajectory(TimeRange(t_start, t_end), dx, dy, dz)) t = collect(range(t_start, t_end, Nt)) - ph = ph |> gpu - t = t |> gpu + ph = ph |> gpu |> f32 + t = t |> gpu |> f32 xt, yt, zt = get_spin_coords(ph.motion, ph.x, ph.y, ph.z, t') - dx = dx |> gpu - dy = dy |> gpu - dz = dz |> gpu + dx = dx |> gpu |> f32 + dy = dy |> gpu |> f32 + dz = dz |> gpu |> f32 @test xt == ph.x .+ dx @test yt == ph.y .+ dy @test zt == ph.z .+ dz From b4ac68eb12accdfb10ca20c4944e2a4bf24dca8b Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 19 Aug 2024 19:18:08 +0200 Subject: [PATCH 18/91] Correct bug --- KomaMRICore/test/runtests.jl | 35 ++++++++--------------------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index b1c256ecd..fbcdd2e7b 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -345,28 +345,6 @@ end using Suppressor include("initialize_backend.jl") include(joinpath(@__DIR__, "test_files", "utils.jl")) - - # 1 spin - ph = Phantom(x=[1.0], y=[1.0]) - Ns = length(ph) - t_start = 0.0 - t_end = 1.0 - Nt = 10 - dx = rand(Ns, Nt) - dy = rand(Ns, Nt) - dz = rand(Ns, Nt) - ph.motion = MotionList(Trajectory(TimeRange(t_start, t_end), dx, dy, dz)) - t = collect(range(t_start, t_end, Nt)) - ph = ph |> gpu |> f32 - t = t |> gpu |> f32 - xt, yt, zt = get_spin_coords(ph.motion, ph.x, ph.y, ph.z, t') - dx = dx |> gpu |> f32 - dy = dy |> gpu |> f32 - dz = dz |> gpu |> f32 - @test xt == ph.x .+ dx - @test yt == ph.y .+ dy - @test zt == ph.z .+ dz - # More than 1 spin ph = Phantom(x=[1.0, 2.0], y=[1.0, 2.0]) Ns = length(ph) @@ -378,12 +356,15 @@ end dz = rand(Ns, Nt) ph.motion = MotionList(Trajectory(TimeRange(t_start, t_end), dx, dy, dz)) t = collect(range(t_start, t_end, Nt)) - ph = ph |> gpu |> f32 - t = t |> gpu |> f32 + + ph = ph |> f32 |> gpu + t = t |> f32 |> gpu + xt, yt, zt = get_spin_coords(ph.motion, ph.x, ph.y, ph.z, t') - dx = dx |> gpu |> f32 - dy = dy |> gpu |> f32 - dz = dz |> gpu |> f32 + + dx = dx |> f32 |> gpu + dy = dy |> f32 |> gpu + dz = dz |> f32 |> gpu @test xt == ph.x .+ dx @test yt == ph.y .+ dy @test zt == ph.z .+ dz From 5b59214dd7d3486a6e925fe021568bc383500199 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 19 Aug 2024 19:38:16 +0200 Subject: [PATCH 19/91] Add SimpleMotion test --- KomaMRICore/test/runtests.jl | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index fbcdd2e7b..3835b9337 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -341,7 +341,33 @@ end @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end -@testitem "getSpinCoords" tags=[:important, :core, :motion, :spincoords] begin +@testitem "getSpinCoords Simple" tags=[:important, :core, :motion, :spincoords] begin + using Suppressor + include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) + + ph = Phantom(x=[1.0], y=[1.0]) + t_start=0.0; t_end=1.0 + t = collect(range(t_start, t_end, 11)) + dx, dy, dz = [1.0, 0.0, 0.0] + vx, vy, vz = [dx, dy, dz] ./ (t_end - t_start) + ph.motion = MotionList(Translation(TimeRange(t_start, t_end), dx, dy, dz)) + + ph = ph |> f32 |> gpu + t = t |> f32 |> gpu + + xt, yt, zt = get_spin_coords(ph.motion, ph.x, ph.y, ph.z, t') + + dx = dx |> f32 |> gpu + dy = dy |> f32 |> gpu + dz = dz |> f32 |> gpu + + @test xt == ph.x .+ vx.*t' + @test yt == ph.y .+ vy.*t' + @test zt == ph.z .+ vz.*t' +end + +@testitem "getSpinCoords Arbitrary" tags=[:important, :core, :motion, :spincoords] begin using Suppressor include("initialize_backend.jl") include(joinpath(@__DIR__, "test_files", "utils.jl")) @@ -365,6 +391,7 @@ end dx = dx |> f32 |> gpu dy = dy |> f32 |> gpu dz = dz |> f32 |> gpu + @test xt == ph.x .+ dx @test yt == ph.y .+ dy @test zt == ph.z .+ dz From 0aa854066324fb4219e79dd03be08be693e74083 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 19 Aug 2024 19:41:40 +0200 Subject: [PATCH 20/91] Solve bug --- KomaMRICore/test/runtests.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index 3835b9337..a0f198689 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -362,6 +362,10 @@ end dy = dy |> f32 |> gpu dz = dz |> f32 |> gpu + vx = vx |> f32 |> gpu + vy = vy |> f32 |> gpu + vz = vz |> f32 |> gpu + @test xt == ph.x .+ vx.*t' @test yt == ph.y .+ vy.*t' @test zt == ph.z .+ vz.*t' From 36f96be71885b93371dd0a31710b29f435606f20 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 19 Aug 2024 19:58:45 +0200 Subject: [PATCH 21/91] Try again... --- KomaMRICore/test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index a0f198689..cf87593ff 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -341,7 +341,7 @@ end @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end -@testitem "getSpinCoords Simple" tags=[:important, :core, :motion, :spincoords] begin +@testitem "getSpinCoords Simple" tags=[:important, :core, :motion, :spincoords] begin using Suppressor include("initialize_backend.jl") include(joinpath(@__DIR__, "test_files", "utils.jl")) From 02c539d15774c8a17808a60492d76af0c3ea1900 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 19 Aug 2024 20:03:52 +0200 Subject: [PATCH 22/91] Again... --- KomaMRICore/test/runtests.jl | 45 +++++------------------------------- 1 file changed, 6 insertions(+), 39 deletions(-) diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index cf87593ff..92e8e2f75 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -340,52 +340,19 @@ end NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end - -@testitem "getSpinCoords Simple" tags=[:important, :core, :motion, :spincoords] begin - using Suppressor - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - ph = Phantom(x=[1.0], y=[1.0]) - t_start=0.0; t_end=1.0 - t = collect(range(t_start, t_end, 11)) - dx, dy, dz = [1.0, 0.0, 0.0] - vx, vy, vz = [dx, dy, dz] ./ (t_end - t_start) - ph.motion = MotionList(Translation(TimeRange(t_start, t_end), dx, dy, dz)) - - ph = ph |> f32 |> gpu - t = t |> f32 |> gpu - - xt, yt, zt = get_spin_coords(ph.motion, ph.x, ph.y, ph.z, t') - - dx = dx |> f32 |> gpu - dy = dy |> f32 |> gpu - dz = dz |> f32 |> gpu - - vx = vx |> f32 |> gpu - vy = vy |> f32 |> gpu - vz = vz |> f32 |> gpu - - @test xt == ph.x .+ vx.*t' - @test yt == ph.y .+ vy.*t' - @test zt == ph.z .+ vz.*t' -end - @testitem "getSpinCoords Arbitrary" tags=[:important, :core, :motion, :spincoords] begin using Suppressor include("initialize_backend.jl") include(joinpath(@__DIR__, "test_files", "utils.jl")) # More than 1 spin - ph = Phantom(x=[1.0, 2.0], y=[1.0, 2.0]) + ph = phantom_brain_arbitrary_motion() Ns = length(ph) t_start = 0.0 - t_end = 1.0 - Nt = 10 - dx = rand(Ns, Nt) - dy = rand(Ns, Nt) - dz = rand(Ns, Nt) - ph.motion = MotionList(Trajectory(TimeRange(t_start, t_end), dx, dy, dz)) - t = collect(range(t_start, t_end, Nt)) + t_end = 10.0 + dx = zeros(Ns, 2) + dz = zeros(Ns, 2) + dy = [zeros(Ns,1) ones(Ns,1)] + t = collect(range(t_start, t_end, 2)) ph = ph |> f32 |> gpu t = t |> f32 |> gpu From 25360e2b32d1b9b8e3b58c765a25a4e5e0fd2720 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 19 Aug 2024 20:43:37 +0200 Subject: [PATCH 23/91] Check if only fails with oneAPI --- KomaMRICore/test/Project.toml | 1 + KomaMRICore/test/runtests.jl | 23 +++++++++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/KomaMRICore/test/Project.toml b/KomaMRICore/test/Project.toml index 50a7da212..848749380 100644 --- a/KomaMRICore/test/Project.toml +++ b/KomaMRICore/test/Project.toml @@ -4,6 +4,7 @@ KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" KomaMRIBase = "d0bc0b20-b151-4d03-b2a4-6ca51751cb9c" KomaMRICore = "4baa4f4d-2ae9-40db-8331-a7d1080e3f4e" Preferences = "21216c6a-2e73-6563-6e65-726566657250" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index 92e8e2f75..d49f075fd 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -341,31 +341,38 @@ end @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end @testitem "getSpinCoords Arbitrary" tags=[:important, :core, :motion, :spincoords] begin - using Suppressor + using Suppressor, Random + include("initialize_backend.jl") include(joinpath(@__DIR__, "test_files", "utils.jl")) # More than 1 spin ph = phantom_brain_arbitrary_motion() Ns = length(ph) + Nt = 10 t_start = 0.0 t_end = 10.0 dx = zeros(Ns, 2) dz = zeros(Ns, 2) dy = [zeros(Ns,1) ones(Ns,1)] - t = collect(range(t_start, t_end, 2)) + Random.seed!(1234) + t = rand(t_start:0.1:t_end, Nt) ph = ph |> f32 |> gpu t = t |> f32 |> gpu xt, yt, zt = get_spin_coords(ph.motion, ph.x, ph.y, ph.z, t') - dx = dx |> f32 |> gpu - dy = dy |> f32 |> gpu - dz = dz |> f32 |> gpu + ux = t' .* 0.0 + uy = t' .* 0.1 + uz = t' .* 0.0 + + ux = ux |> f32 |> gpu + uy = uy |> f32 |> gpu + uz = uz |> f32 |> gpu - @test xt == ph.x .+ dx - @test yt == ph.y .+ dy - @test zt == ph.z .+ dz + @test xt == ph.x .+ ux + @test yt == ph.y .+ uy + @test zt == ph.z .+ uz end @testitem "BlochDict" tags=[:important, :core] begin From 0d8a541e4c922e37befbec1b4e9d28b959944f21 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 19 Aug 2024 21:10:37 +0200 Subject: [PATCH 24/91] Try now --- KomaMRICore/test/runtests.jl | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index d49f075fd..7922af043 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -348,7 +348,7 @@ end # More than 1 spin ph = phantom_brain_arbitrary_motion() Ns = length(ph) - Nt = 10 + Nt = 5 t_start = 0.0 t_end = 10.0 dx = zeros(Ns, 2) @@ -356,20 +356,18 @@ end dy = [zeros(Ns,1) ones(Ns,1)] Random.seed!(1234) t = rand(t_start:0.1:t_end, Nt) - - ph = ph |> f32 |> gpu - t = t |> f32 |> gpu - - xt, yt, zt = get_spin_coords(ph.motion, ph.x, ph.y, ph.z, t') - - ux = t' .* 0.0 + ux = t' .* 0.0 uy = t' .* 0.1 uz = t' .* 0.0 + ph = ph |> f32 |> gpu + t = t |> f32 |> gpu ux = ux |> f32 |> gpu uy = uy |> f32 |> gpu uz = uz |> f32 |> gpu + xt, yt, zt = get_spin_coords(ph.motion, ph.x, ph.y, ph.z, t') + @test xt == ph.x .+ ux @test yt == ph.y .+ uy @test zt == ph.z .+ uz From 59d6ecc052784445e3b9aebe1d4b706c65241a0b Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 19 Aug 2024 21:17:06 +0200 Subject: [PATCH 25/91] Aux comments --- KomaMRICore/test/runtests.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index 7922af043..c4d98e532 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -368,6 +368,9 @@ end xt, yt, zt = get_spin_coords(ph.motion, ph.x, ph.y, ph.z, t') + display(length(yt)) + display(length(yt[yt .== ph.y .+ uy])) + @test xt == ph.x .+ ux @test yt == ph.y .+ uy @test zt == ph.z .+ uz From 0d2c134c249faa6b842d76ebd813e6a8cd927c35 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 19 Aug 2024 21:25:52 +0200 Subject: [PATCH 26/91] Try with simple motion --- KomaMRICore/test/runtests.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index c4d98e532..9708a4df6 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -340,13 +340,13 @@ end NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end -@testitem "getSpinCoords Arbitrary" tags=[:important, :core, :motion, :spincoords] begin +@testitem "getSpinCoords Simple" tags=[:important, :core, :motion, :spincoords] begin using Suppressor, Random include("initialize_backend.jl") include(joinpath(@__DIR__, "test_files", "utils.jl")) # More than 1 spin - ph = phantom_brain_arbitrary_motion() + ph = phantom_brain_simple_motion() Ns = length(ph) Nt = 5 t_start = 0.0 From 2cbfa294961181dfd28289915d337a897a2409d1 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 19 Aug 2024 22:12:07 +0200 Subject: [PATCH 27/91] comments --- KomaMRICore/test/runtests.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index 9708a4df6..e48905184 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -36,7 +36,7 @@ using TestItems, TestItemRunner #Environment variable set by CI const CI = get(ENV, "CI", nothing) -@run_package_tests filter=ti->(:spincoords in ti.tags)&&(isnothing(CI) || :skipci ∉ ti.tags) #verbose=true +@run_package_tests filter=ti->(:motion in ti.tags)&&(isnothing(CI) || :skipci ∉ ti.tags) #verbose=true @testitem "Spinors×Mag" tags=[:core] begin using KomaMRICore: Rx, Ry, Rz, Q, rotx, roty, rotz, Un, Rφ, Rg @@ -318,6 +318,7 @@ end sig = @suppress simulate(obj, seq, sys; sim_params) sig = sig / prod(size(obj)) NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + print(NMRSE(sig, sig_jemris)) @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end @@ -338,9 +339,10 @@ end sig = @suppress simulate(obj, seq, sys; sim_params) sig = sig / prod(size(obj)) NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + print(NMRSE(sig, sig_jemris)) @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end -@testitem "getSpinCoords Simple" tags=[:important, :core, :motion, :spincoords] begin +@testitem "getSpinCoords Simple" tags=[:important, :core, :spincoords] begin using Suppressor, Random include("initialize_backend.jl") From 77b9530de612ae1e2554e61d7b8e47c6b8e7f5a3 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 19 Aug 2024 22:25:21 +0200 Subject: [PATCH 28/91] Recover to initial state --- KomaMRICore/test/Project.toml | 1 - KomaMRICore/test/runtests.jl | 37 +---------------------------------- 2 files changed, 1 insertion(+), 37 deletions(-) diff --git a/KomaMRICore/test/Project.toml b/KomaMRICore/test/Project.toml index 848749380..50a7da212 100644 --- a/KomaMRICore/test/Project.toml +++ b/KomaMRICore/test/Project.toml @@ -4,7 +4,6 @@ KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" KomaMRIBase = "d0bc0b20-b151-4d03-b2a4-6ca51751cb9c" KomaMRICore = "4baa4f4d-2ae9-40db-8331-a7d1080e3f4e" Preferences = "21216c6a-2e73-6563-6e65-726566657250" -Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index e48905184..487b9bf62 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -36,7 +36,7 @@ using TestItems, TestItemRunner #Environment variable set by CI const CI = get(ENV, "CI", nothing) -@run_package_tests filter=ti->(:motion in ti.tags)&&(isnothing(CI) || :skipci ∉ ti.tags) #verbose=true +@run_package_tests filter=ti->(:core in ti.tags)&&(isnothing(CI) || :skipci ∉ ti.tags) #verbose=true @testitem "Spinors×Mag" tags=[:core] begin using KomaMRICore: Rx, Ry, Rz, Q, rotx, roty, rotz, Un, Rφ, Rg @@ -342,41 +342,6 @@ end print(NMRSE(sig, sig_jemris)) @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end -@testitem "getSpinCoords Simple" tags=[:important, :core, :spincoords] begin - using Suppressor, Random - - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - # More than 1 spin - ph = phantom_brain_simple_motion() - Ns = length(ph) - Nt = 5 - t_start = 0.0 - t_end = 10.0 - dx = zeros(Ns, 2) - dz = zeros(Ns, 2) - dy = [zeros(Ns,1) ones(Ns,1)] - Random.seed!(1234) - t = rand(t_start:0.1:t_end, Nt) - ux = t' .* 0.0 - uy = t' .* 0.1 - uz = t' .* 0.0 - - ph = ph |> f32 |> gpu - t = t |> f32 |> gpu - ux = ux |> f32 |> gpu - uy = uy |> f32 |> gpu - uz = uz |> f32 |> gpu - - xt, yt, zt = get_spin_coords(ph.motion, ph.x, ph.y, ph.z, t') - - display(length(yt)) - display(length(yt[yt .== ph.y .+ uy])) - - @test xt == ph.x .+ ux - @test yt == ph.y .+ uy - @test zt == ph.z .+ uz -end @testitem "BlochDict" tags=[:important, :core] begin using Suppressor From 0416bb0779145c7ddcaaed3df1f8aae3eb36809b Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 19 Aug 2024 22:57:37 +0200 Subject: [PATCH 29/91] Comments --- KomaMRICore/test/runtests.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index 487b9bf62..1a9e64b6c 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -318,7 +318,7 @@ end sig = @suppress simulate(obj, seq, sys; sim_params) sig = sig / prod(size(obj)) NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - print(NMRSE(sig, sig_jemris)) + println("NMRSE SimpleMotion: ", NMRSE(sig, sig_jemris)) @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end @@ -339,7 +339,7 @@ end sig = @suppress simulate(obj, seq, sys; sim_params) sig = sig / prod(size(obj)) NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - print(NMRSE(sig, sig_jemris)) + println("NMRSE ArbitraryMotion: ", NMRSE(sig, sig_jemris)) @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end From b2685780ff0c608d08e5ea195e70dc98b15b24d9 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 19 Aug 2024 23:09:26 +0200 Subject: [PATCH 30/91] More comments --- KomaMRICore/test/runtests.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index 1a9e64b6c..4ccc64a74 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -386,7 +386,7 @@ end sig = sig / prod(size(obj)) NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - + @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end @@ -408,6 +408,7 @@ end sig = @suppress simulate(obj, seq, sys; sim_params) sig = sig / prod(size(obj)) NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + println("NMRSE SimpleMotion BlochSimple: ", NMRSE(sig, sig_jemris)) @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end @@ -429,6 +430,7 @@ end sig = @suppress simulate(obj, seq, sys; sim_params) sig = sig / prod(size(obj)) NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + println("NMRSE ArbitraryMotion BlochSimple: ", NMRSE(sig, sig_jemris)) @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end From c32b9bb65aa8d84e1f3e199fd2cd702b4c5fd3f1 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 19 Aug 2024 23:31:44 +0200 Subject: [PATCH 31/91] Try again --- KomaMRICore/test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index 4ccc64a74..d42ff6afb 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -301,7 +301,7 @@ end @test raw1.profiles[1].data ≈ raw2.profiles[1].data end -@testitem "Bloch SimpleMotion" tags=[:important, :core, :motion] begin +@testitem "Bloch SimpleMotion" tags=[:important, :core, :motion] begin using Suppressor include("initialize_backend.jl") include(joinpath(@__DIR__, "test_files", "utils.jl")) From 29950749514a80af07a6bfa5859eb2e1059a014b Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Fri, 23 Aug 2024 18:29:47 +0200 Subject: [PATCH 32/91] Big change --- KomaMRIBase/src/KomaMRIBase.jl | 14 +- KomaMRIBase/src/datatypes/Phantom.jl | 54 ++--- KomaMRIBase/src/datatypes/phantom/Motion.jl | 121 ----------- KomaMRIBase/src/datatypes/phantom/NoMotion.jl | 31 --- .../datatypes/phantom/motion/SimpleMotion.jl | 31 --- .../motion/arbitrarymotion/Trajectory.jl | 25 --- .../motion/simplemotion/Translation.jl | 71 ------- KomaMRIBase/src/motion/MotionSet.jl | 12 ++ .../src/motion/motionlist/ActionSpan.jl | 8 + KomaMRIBase/src/motion/motionlist/Motion.jl | 58 +++++ .../src/motion/motionlist/MotionList.jl | 120 +++++++++++ KomaMRIBase/src/motion/motionlist/SpinSpan.jl | 56 +++++ .../motionlist/TimeSpan.jl} | 27 ++- .../motionlist/actions/ArbitraryAction.jl} | 40 ++-- .../motion/motionlist/actions/SimpleAction.jl | 13 ++ .../actions/arbitraryactions/FlowPath.jl} | 12 +- .../actions/arbitraryactions/Path.jl | 23 ++ .../actions/simpleactions}/HeartBeat.jl | 35 ++-- .../actions/simpleactions/Rotate.jl} | 47 ++--- .../actions/simpleactions/Translate.jl | 65 ++++++ KomaMRIBase/src/motion/nomotion/NoMotion.jl | 53 +++++ KomaMRIBase/test/runtests.jl | 36 ++-- KomaMRICore/src/simulation/Flow.jl | 11 +- KomaMRICore/src/simulation/Functors.jl | 10 +- .../simulation/SimMethods/Magnetization.jl | 3 + KomaMRICore/test/runtests.jl | 16 +- KomaMRICore/test/test_files/utils.jl | 8 +- KomaMRIFiles/src/Phantom/Phantom.jl | 198 ++++++++---------- KomaMRIFiles/test/runtests.jl | 27 +-- KomaMRIPlots/src/ui/DisplayFunctions.jl | 4 +- docs/src/reference/2-koma-base.md | 20 +- examples/3.tutorials/lit-05-SimpleMotion.jl | 10 +- 32 files changed, 681 insertions(+), 578 deletions(-) delete mode 100644 KomaMRIBase/src/datatypes/phantom/Motion.jl delete mode 100644 KomaMRIBase/src/datatypes/phantom/NoMotion.jl delete mode 100644 KomaMRIBase/src/datatypes/phantom/motion/SimpleMotion.jl delete mode 100644 KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/Trajectory.jl delete mode 100644 KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Translation.jl create mode 100644 KomaMRIBase/src/motion/MotionSet.jl create mode 100644 KomaMRIBase/src/motion/motionlist/ActionSpan.jl create mode 100644 KomaMRIBase/src/motion/motionlist/Motion.jl create mode 100644 KomaMRIBase/src/motion/motionlist/MotionList.jl create mode 100644 KomaMRIBase/src/motion/motionlist/SpinSpan.jl rename KomaMRIBase/src/{timing/TimeScale.jl => motion/motionlist/TimeSpan.jl} (90%) rename KomaMRIBase/src/{datatypes/phantom/motion/ArbitraryMotion.jl => motion/motionlist/actions/ArbitraryAction.jl} (65%) create mode 100644 KomaMRIBase/src/motion/motionlist/actions/SimpleAction.jl rename KomaMRIBase/src/{datatypes/phantom/motion/arbitrarymotion/FlowTrajectory.jl => motion/motionlist/actions/arbitraryactions/FlowPath.jl} (50%) create mode 100644 KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/Path.jl rename KomaMRIBase/src/{datatypes/phantom/motion/simplemotion => motion/motionlist/actions/simpleactions}/HeartBeat.jl (56%) rename KomaMRIBase/src/{datatypes/phantom/motion/simplemotion/Rotation.jl => motion/motionlist/actions/simpleactions/Rotate.jl} (66%) create mode 100644 KomaMRIBase/src/motion/motionlist/actions/simpleactions/Translate.jl create mode 100644 KomaMRIBase/src/motion/nomotion/NoMotion.jl diff --git a/KomaMRIBase/src/KomaMRIBase.jl b/KomaMRIBase/src/KomaMRIBase.jl index 7da23d858..c1839b3cf 100644 --- a/KomaMRIBase/src/KomaMRIBase.jl +++ b/KomaMRIBase/src/KomaMRIBase.jl @@ -25,6 +25,8 @@ include("datatypes/sequence/ADC.jl") include("timing/KeyValuesCalculation.jl") include("datatypes/Sequence.jl") include("datatypes/sequence/Delay.jl") +# Motion +include("motion/MotionSet.jl") # Phantom include("datatypes/Phantom.jl") # Simulator @@ -46,12 +48,12 @@ export kfoldperm, trapz, cumtrapz # Phantom export brain_phantom2D, brain_phantom3D, pelvis_phantom2D, heart_phantom # Motion -export AbstractMotion, AbstractMotionList, MotionList, NoMotion -export SimpleMotion, ArbitraryMotion -export Translation, TranslationX, TranslationY, TranslationZ -export Rotation, RotationX, RotationY, RotationZ -export HeartBeat, Trajectory, FlowTrajectory -export AbstractTimeSpan, TimeRange, Periodic +export MotionList, NoMotion, Motion +export Translate, TranslateX, TranslateY, TranslateZ +export Rotate, RotateX, RotateY, RotateZ +export HeartBeat, Path, FlowPath +export TimeRange, Periodic +export SpinRange, AllSpins export sort_motions!, get_spin_coords # Secondary export get_kspace, rotx, roty, rotz diff --git a/KomaMRIBase/src/datatypes/Phantom.jl b/KomaMRIBase/src/datatypes/Phantom.jl index ca31222fa..374a9aa8e 100644 --- a/KomaMRIBase/src/datatypes/Phantom.jl +++ b/KomaMRIBase/src/datatypes/Phantom.jl @@ -1,10 +1,3 @@ -# TimeScale: -include("../timing/TimeScale.jl") -# Motion: -abstract type AbstractMotionList{T<:Real} end -include("phantom/Motion.jl") -include("phantom/NoMotion.jl") - """ obj = Phantom(name, x, y, z, ρ, T1, T2, T2s, Δw, Dλ1, Dλ2, Dθ, motion) @@ -54,7 +47,7 @@ julia> obj.ρ Dθ::AbstractVector{T} = zeros(eltype(x), size(x)) #Diff::Vector{DiffusionModel} #Diffusion map #Motion - motion::AbstractMotionList{T} = NoMotion{eltype(x)}() + motion::AbstractMotionSet{T} = NoMotion{eltype(x)}() end """Size and length of a phantom""" @@ -65,38 +58,35 @@ Base.iterate(x::Phantom) = (x[1], 2) Base.iterate(x::Phantom, i::Integer) = (i <= length(x)) ? (x[i], i + 1) : nothing Base.lastindex(x::Phantom) = length(x) Base.getindex(x::Phantom, i::Integer) = x[i:i] +Base.getindex(x::Phantom, c::Colon) = x[1:length(x)] +Base.view(x::Phantom, i::Integer) = @view(x[i:i]) +Base.view(x::Phantom, c::Colon) = @view(x[1:length(x)]) """Compare two phantoms""" function Base.:(==)(obj1::Phantom, obj2::Phantom) return reduce( &, - [ - getfield(obj1, field) == getfield(obj2, field) for - field in Iterators.filter(x -> !(x == :name), fieldnames(Phantom)) - ], + [getfield(obj1, field) == getfield(obj2, field) for + field in Iterators.filter(x -> !(x == :name), fieldnames(Phantom))], ) end function Base.:(≈)(obj1::Phantom, obj2::Phantom) return reduce( &, - [ - getfield(obj1, field) ≈ getfield(obj2, field) for - field in Iterators.filter(x -> !(x == :name), fieldnames(Phantom)) - ], + [getfield(obj1, field) ≈ getfield(obj2, field) for + field in Iterators.filter(x -> !(x == :name), fieldnames(Phantom))], ) end """Separate object spins in a sub-group""" -Base.getindex(obj::Phantom, p::Union{AbstractRange,AbstractVector,Colon}) = begin +Base.getindex(obj::Phantom, p::AbstractVector) = begin fields = [] for field in Iterators.filter(x -> x != :name, fieldnames(Phantom)) push!(fields, (field, getfield(obj, field)[p])) end return Phantom(; name=obj.name, fields...) end - -"""Separate object spins in a sub-group (lightweigth).""" -Base.view(obj::Phantom, p::Union{AbstractRange,AbstractVector,Colon}) = begin +Base.view(obj::Phantom, p::AbstractVector) = begin fields = [] for field in Iterators.filter(x -> x != :name, fieldnames(Phantom)) push!(fields, (field, @view(getfield(obj, field)[p]))) @@ -109,14 +99,13 @@ end Nmaxchars = 50 name = first(obj1.name * "+" * obj2.name, Nmaxchars) fields = [] - for field in Iterators.filter(x -> x != :name, fieldnames(Phantom)) + for field in Iterators.filter(x -> !(x in (:name, :motion)), fieldnames(Phantom)) push!(fields, (field, [getfield(obj1, field); getfield(obj2, field)])) end return Phantom(; - name=name, + name = name, fields..., - motion=vcat(obj1.motion, obj2.motion, length(obj1), length(obj2)) - ) + motion = vcat(obj1.motion, obj2.motion, length(obj1), length(obj2))) end """Scalar multiplication of a phantom""" @@ -193,17 +182,14 @@ function heart_phantom( Dλ2=Dλ2[ρ .!= 0], Dθ=Dθ[ρ .!= 0], motion=MotionList( - HeartBeat(; - time=Periodic(; period=period, asymmetry=asymmetry), - circumferential_strain=circumferential_strain, - radial_strain=radial_strain, - longitudinal_strain=0.0, + HeartBeat( + circumferential_strain, + radial_strain, + 0.0, + Periodic(; period=period, asymmetry=asymmetry), ), - Rotation(; - time=Periodic(; period=period, asymmetry=asymmetry), - yaw=rotation_angle, - pitch=0.0, - roll=0.0, + Rotate( + 0.0, 0.0, rotation_angle, Periodic(; period=period, asymmetry=asymmetry) ), ), ) diff --git a/KomaMRIBase/src/datatypes/phantom/Motion.jl b/KomaMRIBase/src/datatypes/phantom/Motion.jl deleted file mode 100644 index aa5b46a3d..000000000 --- a/KomaMRIBase/src/datatypes/phantom/Motion.jl +++ /dev/null @@ -1,121 +0,0 @@ -abstract type AbstractMotion{T<:Real} end - -is_composable(m::AbstractMotion) = false - -struct MotionList{T<:Real} <: AbstractMotionList{T} - motions::Vector{<:AbstractMotion{T}} -end - -MotionList(motions...) = length([motions]) > 0 ? MotionList([motions...]) : @error "You must provide at least one motion as input argument. If you do not want to define motion, use `NoMotion{T}()`" - -include("motion/SimpleMotion.jl") -include("motion/ArbitraryMotion.jl") - -Base.getindex(mv::MotionList, p::Union{AbstractRange, AbstractVector, Colon, Integer}) = MotionList(getindex.(mv.motions, Ref(p))) -Base.view(mv::MotionList, p::Union{AbstractRange, AbstractVector, Colon, Integer}) = MotionList(view.(mv.motions, Ref(p))) - -""" Addition of MotionLists """ -function Base.vcat(m1::MotionList{T}, m2::MotionList{T}, Ns1::Int, Ns2::Int) where {T<:Real} - mv1 = m1.motions - mv1_aux = AbstractMotion{T}[] - for i in 1:length(mv1) - if typeof(mv1[i]) <: ArbitraryMotion - zeros1 = similar(mv1[i].dx, Ns2, size(mv1[i].dx, 2)) - zeros1 .= zero(T) - push!(mv1_aux, typeof(mv1[i])(mv1[i].time, [[getfield(mv1[i], d); zeros1] for d in filter(x -> x != :time, fieldnames(typeof(mv1[i])))]...)) - else - push!(mv1_aux, mv1[i]) - end - end - mv2 = m2.motions - mv2_aux = AbstractMotion{T}[] - for i in 1:length(mv2) - if typeof(mv2[i]) <: ArbitraryMotion - zeros2 = similar(mv2[i].dx, Ns1, size(mv2[i].dx, 2)) - zeros2 .= zero(T) - push!(mv2_aux, typeof(mv2[i])(mv2[i].time, [[zeros2; getfield(mv2[i], d)] for d in filter(x -> x != :time, fieldnames(typeof(mv2[i])))]...)) - else - push!(mv2_aux, mv2[i]) - end - end - return MotionList([mv1_aux; mv2_aux]) -end - -""" Compare two motion vectors """ -function Base.:(==)(mv1::MotionList{T}, mv2::MotionList{T}) where {T<:Real} - sort_motions!(mv1) - sort_motions!(mv2) - return reduce(&, mv1.motions .== mv2.motions) -end -function Base.:(≈)(mv1::MotionList{T}, mv2::MotionList{T}) where {T<:Real} - sort_motions!(mv1) - sort_motions!(mv2) - return reduce(&, mv1.motions .≈ mv2.motions) -end - -""" - x, y, z = get_spin_coords(motion, x, y, z, t) - -Calculates the position of each spin at a set of arbitrary time instants, i.e. the time steps of the simulation. -For each dimension (x, y, z), the output matrix has ``N_{\text{spins}}`` rows and `length(t)` columns. - -# Arguments -- `motion`: (`::Vector{<:AbstractMotion{T<:Real}}`) phantom motion -- `x`: (`::AbstractVector{T<:Real}`, `[m]`) spin x-position vector -- `y`: (`::AbstractVector{T<:Real}`, `[m]`) spin y-position vector -- `z`: (`::AbstractVector{T<:Real}`, `[m]`) spin z-position vector -- `t`: (`::AbstractArray{T<:Real}`) horizontal array of time instants - -# Returns -- `x, y, z`: (`::Tuple{AbstractArray, AbstractArray, AbstractArray}`) spin positions over time -""" -function get_spin_coords( - mv::MotionList{T}, - x::AbstractVector{T}, - y::AbstractVector{T}, - z::AbstractVector{T}, - t::AbstractArray{T} -) where {T<:Real} - # Buffers for positions: - xt, yt, zt = x .+ 0*t, y .+ 0*t, z .+ 0*t - # Buffers for displacements: - ux, uy, uz = similar(xt), similar(yt), similar(zt) - - # Composable motions: they need to be run sequentially. Note that they depend on xt, yt , and zt - for m in Iterators.filter(is_composable, mv.motions) - displacement_x!(ux, m, xt, yt, zt, t) - displacement_y!(uy, m, xt, yt, zt, t) - displacement_z!(uz, m, xt, yt, zt, t) - xt .+= ux - yt .+= uy - zt .+= uz - end - # Additive motions: these motions can be run in parallel - for m in Iterators.filter(!is_composable, mv.motions) - displacement_x!(ux, m, x, y, z, t) - displacement_y!(uy, m, x, y, z, t) - displacement_z!(uz, m, x, y, z, t) - xt .+= ux - yt .+= uy - zt .+= uz - end - return xt, yt, zt -end - -""" - times = times(motion) -""" -times(m::AbstractMotion) = times(m.time) -times(mv::MotionList{T}) where {T<:Real} = begin - nodes = reduce(vcat, [times(m) for m in mv.motions]; init=[zero(T)]) - return unique(sort(nodes)) -end - -""" - sort_motions!(motion_list) -sort_motions motions in a list according to their starting time -""" -function sort_motions!(mv::MotionList{T}) where {T<:Real} - sort!(mv.motions; by=m -> times(m)[1]) - return nothing -end \ No newline at end of file diff --git a/KomaMRIBase/src/datatypes/phantom/NoMotion.jl b/KomaMRIBase/src/datatypes/phantom/NoMotion.jl deleted file mode 100644 index ed52d186d..000000000 --- a/KomaMRIBase/src/datatypes/phantom/NoMotion.jl +++ /dev/null @@ -1,31 +0,0 @@ -struct NoMotion{T<:Real} <: AbstractMotionList{T} end - -Base.getindex(mv::NoMotion, p::Union{AbstractRange, AbstractVector, Colon, Integer}) = mv -Base.view(mv::NoMotion, p::Union{AbstractRange, AbstractVector, Colon, Integer}) = mv - -""" Addition of NoMotions """ -Base.vcat(m1::NoMotion{T}, m2::AbstractMotionList{T}, Ns1::Int, Ns2::Int) where {T<:Real} = m2 -Base.vcat(m1::AbstractMotionList{T}, m2::NoMotion{T}, Ns1::Int, Ns2::Int) where {T<:Real} = m1 - -Base.:(==)(m1::NoMotion{T}, m2::NoMotion{T}) where {T<:Real} = true -Base.:(≈)(m1::NoMotion{T}, m2::NoMotion{T}) where {T<:Real} = true - -function get_spin_coords( - mv::NoMotion{T}, - x::AbstractVector{T}, - y::AbstractVector{T}, - z::AbstractVector{T}, - t::AbstractArray{T} -) where {T<:Real} - return x, y, z -end - -""" - times = times(motion) -""" -times(mv::NoMotion{T}) where {T<:Real} = [zero(T)] - -""" - sort_motions! -""" -sort_motions!(mv::NoMotion) = nothing \ No newline at end of file diff --git a/KomaMRIBase/src/datatypes/phantom/motion/SimpleMotion.jl b/KomaMRIBase/src/datatypes/phantom/motion/SimpleMotion.jl deleted file mode 100644 index f431d19a0..000000000 --- a/KomaMRIBase/src/datatypes/phantom/motion/SimpleMotion.jl +++ /dev/null @@ -1,31 +0,0 @@ -""" - motion = SimpleMotion(types) - -SimpleMotion model. It allows for the definition of motion by means of simple parameters. -The `SimpleMotion` struct is composed by only one field, called `types`, -which is a tuple of simple motion types. This tuple will contain as many elements -as simple motions we want to combine. - -# Arguments -- `types`: (`::Tuple{Vararg{<:SimpleMotionType{T}}}`) tuple of simple motion types - -# Returns -- `motion`: (`::SimpleMotion`) SimpleMotion struct - -# Examples -```julia-repl -julia> motion = SimpleMotion( - Translation(dx=0.01, dy=0.02, dz=0.0, t_start=0.0, t_end=0.5), - Rotation(pitch=15.0, roll=0.0, yaw=20.0, t_start=0.1, t_end=0.5), - HeartBeat(circumferential_strain=-0.3, radial_strain=-0.2, longitudinal_strain=0.0, t_start=0.2, t_end=0.5) - ) -``` -""" -abstract type SimpleMotion{T<:Real} <: AbstractMotion{T} end - -Base.getindex(motion::SimpleMotion, p::Union{AbstractRange, AbstractVector, Colon, Integer}) = motion -Base.view(motion::SimpleMotion, p::Union{AbstractRange, AbstractVector, Colon, Integer}) = motion - -include("simplemotion/Translation.jl") -include("simplemotion/Rotation.jl") -include("simplemotion/HeartBeat.jl") \ No newline at end of file diff --git a/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/Trajectory.jl b/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/Trajectory.jl deleted file mode 100644 index 79c521452..000000000 --- a/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/Trajectory.jl +++ /dev/null @@ -1,25 +0,0 @@ -@doc raw""" - trajectory = Trajectory(time, dx, dy, dz) - -Trajectory motion struct. (...) - -# Arguments -- `time`: (`::AbstractTimeSpan{T<:Real}`, `[s]`) time scale -- `dx`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in x -- `dy`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in y -- `dz`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in z - -# Returns -- `trajectory`: (`::Trajectory`) Trajectory struct - -# Examples -```julia-repl -julia> tr = Trajectory(time=TimeRange(0.0, 0.5), dx=[0.01 0.02], dy=[0.02 0.03], dz=[0.03 0.04]) -``` -""" -struct Trajectory{T<:Real, TS<:AbstractTimeSpan{T}} <: ArbitraryMotion{T} - time::TS - dx::AbstractArray{T} - dy::AbstractArray{T} - dz::AbstractArray{T} -end \ No newline at end of file diff --git a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Translation.jl b/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Translation.jl deleted file mode 100644 index 39949dda4..000000000 --- a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Translation.jl +++ /dev/null @@ -1,71 +0,0 @@ -@doc raw""" - translation = Translation(time, dx, dy, dz) - -Translation motion struct. It produces a linear translation of the phantom. -Its fields are the final displacements in the three axes (dx, dy, dz) -and the start and end time of the translation. - -# Arguments -- `time`: (`::AbstractTimeSpan{T<:Real}`, `[s]`) time scale -- `dx`: (`::Real`, `[m]`) translation in x -- `dy`: (`::Real`, `[m]`) translation in y -- `dz`: (`::Real`, `[m]`) translation in z - -# Returns -- `translation`: (`::Translation`) Translation struct - -# Examples -```julia-repl -julia> tr = Translation(time=TimeRange(0.0, 0.5), dx=0.01, dy=0.02, dz=0.03) -``` -""" -@with_kw struct Translation{T<:Real, TS<:AbstractTimeSpan{T}} <: SimpleMotion{T} - time :: TS - dx :: T - dy :: T - dz :: T -end - -TranslationX(time::AbstractTimeSpan{T}, dx::T) where {T<:Real} = Translation(time, dx, zero(T), zero(T)) -TranslationY(time::AbstractTimeSpan{T}, dy::T) where {T<:Real} = Translation(time, zero(T), dy, zero(T)) -TranslationZ(time::AbstractTimeSpan{T}, dz::T) where {T<:Real} = Translation(time, zero(T), zero(T), dz) - -function displacement_x!( - ux::AbstractArray{T}, - motion::Translation{T}, - x::AbstractVector{T}, - y::AbstractVector{T}, - z::AbstractVector{T}, - t::AbstractArray{T}, -) where {T<:Real} - t_unit = unit_time(t, motion.time) - ux .= t_unit .* motion.dx - return nothing -end - -function displacement_y!( - uy::AbstractArray{T}, - motion::Translation{T}, - x::AbstractVector{T}, - y::AbstractVector{T}, - z::AbstractVector{T}, - t::AbstractArray{T}, -) where {T<:Real} - t_unit = unit_time(t, motion.time) - uy .= t_unit .* motion.dy - return nothing -end - -function displacement_z!( - uz::AbstractArray{T}, - motion::Translation{T}, - x::AbstractVector{T}, - y::AbstractVector{T}, - z::AbstractVector{T}, - t::AbstractArray{T}, -) where {T<:Real} - t_unit = unit_time(t, motion.time) - uz .= t_unit .* motion.dz - return nothing -end - diff --git a/KomaMRIBase/src/motion/MotionSet.jl b/KomaMRIBase/src/motion/MotionSet.jl new file mode 100644 index 000000000..aebfafabe --- /dev/null +++ b/KomaMRIBase/src/motion/MotionSet.jl @@ -0,0 +1,12 @@ +abstract type AbstractMotionSet{T<:Real} end + +# NoMotion +include("nomotion/NoMotion.jl") + +# MotionList +include("motionlist/ActionSpan.jl") +include("motionlist/SpinSpan.jl") +include("motionlist/TimeSpan.jl") +include("motionlist/Motion.jl") +include("motionlist/MotionList.jl") + diff --git a/KomaMRIBase/src/motion/motionlist/ActionSpan.jl b/KomaMRIBase/src/motion/motionlist/ActionSpan.jl new file mode 100644 index 000000000..4cf1a778c --- /dev/null +++ b/KomaMRIBase/src/motion/motionlist/ActionSpan.jl @@ -0,0 +1,8 @@ +abstract type AbstractActionSpan{T<:Real} end + +is_composable(m::AbstractActionSpan) = false + +# Simple actions +include("actions/SimpleAction.jl") +# Arbitrary actions +include("actions/ArbitraryAction.jl") diff --git a/KomaMRIBase/src/motion/motionlist/Motion.jl b/KomaMRIBase/src/motion/motionlist/Motion.jl new file mode 100644 index 000000000..89870175b --- /dev/null +++ b/KomaMRIBase/src/motion/motionlist/Motion.jl @@ -0,0 +1,58 @@ +@with_kw mutable struct Motion{T<:Real} + action::AbstractActionSpan{T} + time::AbstractTimeSpan{T} + spins::AbstractSpinSpan +end + +""" Constructors """ +function Motion(action::A, time::TS, spins::AbstractSpinSpan) where {T<:Real, A<:AbstractActionSpan{T}, TS<:AbstractTimeSpan{T}} + return Motion{T}(action, time, spins) +end +function Motion(action::A, time::TS) where {T<:Real, A<:AbstractActionSpan{T}, TS<:AbstractTimeSpan{T}} + return Motion{T}(action, time, AllSpins()) +end +function Motion(action::A) where {T<:Real, A<:AbstractActionSpan{T}} + return Motion{T}(action, TimeRange(zero(T))) +end +function Motion(action::A, time::TS, range::Colon) where {T<:Real, A<:AbstractActionSpan{T}, TS<:AbstractTimeSpan{T}} + return Motion{T}(action, time, AllSpins()) +end +function Motion(action::A, time::TS, range::AbstractVector) where {T<:Real, A<:AbstractActionSpan{T}, TS<:AbstractTimeSpan{T}} + return Motion{T}(action, time, SpinRange(range)) +end + +# Custom constructors +function Translate(dx, dy, dz, time=TimeRange(0.0), spins=AllSpins()) + return Motion(Translate(dx, dy, dz), time, spins) +end +function Rotate(pitch, roll, yaw, time=TimeRange(0.0), spins=AllSpins()) + return Motion(Rotate(pitch, roll, yaw), time, spins) +end +function HeartBeat(circumferential_strain, radial_strain, longitudinal_strain, time=TimeRange(0.0), spins=AllSpins()) + return Motion(HeartBeat(circumferential_strain, radial_strain, longitudinal_strain), time, spins) +end +function Path(dx, dy, dz, time=TimeRange(0.0), spins=AllSpins()) + return Motion(Path(dx, dy, dz), time, spins) +end +function FlowPath(dx, dy, dz, spin_reset, time=TimeRange(0.0), spins=AllSpins()) + return Motion(FlowPath(dx, dy, dz, spin_reset), time, spins) +end + +""" Compare two Motions """ +Base.:(==)(m1::Motion, m2::Motion) = (typeof(m1) == typeof(m2)) & reduce(&, [getfield(m1, field) == getfield(m2, field) for field in fieldnames(typeof(m1))]) +Base.:(≈)(m1::Motion, m2::Motion) = (typeof(m1) == typeof(m2)) & reduce(&, [getfield(m1, field) ≈ getfield(m2, field) for field in fieldnames(typeof(m1))]) + +""" Motion sub-group """ +function Base.getindex(m::Motion, p::AbstractVector) + idx, spin_range = m.spins[p] + return Motion(m.action[idx], m.time, spin_range) +end +function Base.view(m::Motion, p::AbstractVector) + idx, spin_range = @view(m.spins[p]) + return Motion(@view(m.action[idx]), m.time, spin_range) +end + +# Auxiliary functions +times(m::Motion) = times(m.time) +add_motion!(motion_array, motion) = has_spins(motion.spins) ? push!(motion_array, motion) : nothing +is_composable(m::Motion) = is_composable(m.action) \ No newline at end of file diff --git a/KomaMRIBase/src/motion/motionlist/MotionList.jl b/KomaMRIBase/src/motion/motionlist/MotionList.jl new file mode 100644 index 000000000..e67348520 --- /dev/null +++ b/KomaMRIBase/src/motion/motionlist/MotionList.jl @@ -0,0 +1,120 @@ +struct MotionList{T<:Real} <: AbstractMotionSet{T} + motions::Vector{<:Motion{T}} +end + +""" Constructors """ +MotionList(motions...) = length([motions]) > 0 ? MotionList([motions...]) : @error "You must provide at least one motion as input argument. If you do not want to define motion, use `NoMotion{T}()`" + +""" MotionList sub-group """ +function Base.getindex(mv::MotionList{T}, p::AbstractVector) where {T<:Real} + motion_array_aux = Motion{T}[] + for m in mv.motions + add_motion!(motion_array_aux, m[p]) + end + return length(motion_array_aux) > 0 ? MotionList(motion_array_aux) : NoMotion{T}() +end +function Base.view(mv::MotionList{T}, p::AbstractVector) where {T<:Real} + motion_array_aux = Motion{T}[] + for m in mv.motions + add_motion!(motion_array_aux, @view(m[p])) + end + return length(motion_array_aux) > 0 ? MotionList(motion_array_aux) : NoMotion{T}() +end + +""" Addition of MotionLists """ +function Base.vcat(m1::MotionList{T}, m2::MotionList{T}, Ns1::Int, Ns2::Int) where {T<:Real} + mv_aux = Motion{T}[] + for m in m1.motions + m_aux = copy(m) + m_aux.spins = expand(m_aux.spins, Ns1) + push!(mv_aux, m_aux) + end + for m in m2.motions + m_aux = copy(m) + m_aux.spins = expand(m_aux.spins, Ns2) + m_aux.spins = SpinRange(m_aux.spins.range .+ Ns1) + push!(mv_aux, m_aux) + end + return MotionList(mv_aux) +end + +""" Compare two MotionLists """ +function Base.:(==)(mv1::MotionList{T}, mv2::MotionList{T}) where {T<:Real} + sort_motions!(mv1) + sort_motions!(mv2) + return reduce(&, mv1.motions .== mv2.motions) +end +function Base.:(≈)(mv1::MotionList{T}, mv2::MotionList{T}) where {T<:Real} + sort_motions!(mv1) + sort_motions!(mv2) + return reduce(&, mv1.motions .≈ mv2.motions) +end + +""" + x, y, z = get_spin_coords(motion, x, y, z, t) + +Calculates the position of each spin at a set of arbitrary time instants, i.e. the time steps of the simulation. +For each dimension (x, y, z), the output matrix has ``N_{\text{spins}}`` rows and `length(t)` columns. + +# Arguments +- `motion`: (`::MotionList{T<:Real}`) phantom motion +- `x`: (`::AbstractVector{T<:Real}`, `[m]`) spin x-position vector +- `y`: (`::AbstractVector{T<:Real}`, `[m]`) spin y-position vector +- `z`: (`::AbstractVector{T<:Real}`, `[m]`) spin z-position vector +- `t`: (`::AbstractArray{T<:Real}`) horizontal array of time instants + +# Returns +- `x, y, z`: (`::Tuple{AbstractArray, AbstractArray, AbstractArray}`) spin positions over time +""" +function get_spin_coords( + ml::MotionList{T}, + x::AbstractVector{T}, + y::AbstractVector{T}, + z::AbstractVector{T}, + t::AbstractArray{T} +) where {T<:Real} + # Buffers for positions: + xt, yt, zt = x .+ 0*t, y .+ 0*t, z .+ 0*t + # Buffers for displacements: + ux, uy, uz = xt .* 0, yt .* 0, zt .* 0 + # Composable motions: they need to be run sequentially. Note that they depend on xt, yt , and zt + for m in Iterators.filter(is_composable, ml.motions) + t_unit = unit_time(t, m.time) + idx = get_idx(m.spins) + displacement_x!(@view(ux[idx, :]), m.action, @view(xt[idx, :]), @view(yt[idx, :]), @view(zt[idx, :]), t_unit) + displacement_y!(@view(uy[idx, :]), m.action, @view(xt[idx, :]), @view(yt[idx, :]), @view(zt[idx, :]), t_unit) + displacement_z!(@view(uz[idx, :]), m.action, @view(xt[idx, :]), @view(yt[idx, :]), @view(zt[idx, :]), t_unit) + xt .+= ux; yt .+= uy; zt .+= uz + ux .*= 0; uy .*= 0; uz .*= 0 + end + # Additive motions: these motions can be run in parallel + for m in Iterators.filter(!is_composable, ml.motions) + t_unit = unit_time(t, m.time) + idx = get_idx(m.spins) + displacement_x!(@view(ux[idx, :]), m.action, @view(x[idx]), @view(y[idx]), @view(z[idx]), t_unit) + displacement_y!(@view(uy[idx, :]), m.action, @view(x[idx]), @view(y[idx]), @view(z[idx]), t_unit) + displacement_z!(@view(uz[idx, :]), m.action, @view(x[idx]), @view(y[idx]), @view(z[idx]), t_unit) + xt .+= ux; yt .+= uy; zt .+= uz + ux .*= 0; uy .*= 0; uz .*= 0 + end + return xt, yt, zt +end + +""" + times = times(motion) +""" +times(ml::MotionList{T}) where {T<:Real} = begin + nodes = reduce(vcat, [times(m) for m in ml.motions]; init=[zero(T)]) + return unique(sort(nodes)) +end + +""" + sort_motions!(motion_list) +sort_motions motions in a list according to their starting time +""" +function sort_motions!(mv::MotionList{T}) where {T<:Real} + sort!(mv.motions; by=m -> times(m)[1]) + return nothing +end + + diff --git a/KomaMRIBase/src/motion/motionlist/SpinSpan.jl b/KomaMRIBase/src/motion/motionlist/SpinSpan.jl new file mode 100644 index 000000000..e5b7b23eb --- /dev/null +++ b/KomaMRIBase/src/motion/motionlist/SpinSpan.jl @@ -0,0 +1,56 @@ +abstract type AbstractSpinSpan end + +# AllSpins +struct AllSpins <: AbstractSpinSpan end + +Base.getindex(spins::AllSpins, p::AbstractVector) = Colon(), spins +Base.view(spins::AllSpins, p::AbstractVector) = Colon(), spins + +Base.getindex(x, p::AllSpins) = x +Base.getindex(x, p::AllSpins, q) = x[:, q] +Base.view(x, p::AllSpins) = x +Base.view(x, p::AllSpins, q) = @view(x[:, q]) + +get_idx(spins::AllSpins) = Colon() +has_spins(spins::AllSpins) = true + +# SpinRange +@with_kw struct SpinRange <: AbstractSpinSpan + range::AbstractVector +end + +SpinRange(c::Colon) = AllSpins() +SpinRange(range::BitVector) = SpinRange(findall(x->x==true, range)) + +function Base.getindex(spins::SpinRange, p::AbstractVector) + idx = get_idx(spins.range, p) + spin_range = SpinRange(spins.range[idx]) + return idx, spin_range +end +function Base.view(spins::SpinRange, p::AbstractVector) + idx = get_idx(spins.range, p) + spin_range = SpinRange(@view(spins.range[idx])) + return idx, spin_range +end + +Base.getindex(spins::SpinRange, b::BitVector) = spins[findall(x->x==true, b)] +Base.view(spins::SpinRange, b::BitVector) = @view(spins[findall(x->x==true, b)]) + +Base.getindex(x, p::SpinRange) = x[p.range] +Base.getindex(x, p::SpinRange, q) = x[p.range, q] +Base.view(x, p::SpinRange) = @view(x[p.range]) +Base.view(x, p::SpinRange, q) = @view(x[p.range, q]) + +Base.:(==)(sr1::SpinRange, sr2::SpinRange) = sr1.range == sr2.range + +get_idx(spins::SpinRange) = spins.range +has_spins(spins::SpinRange) = length(spins.range) > 0 + +# Auxiliary functions +function get_idx(spin_range::AbstractVector, p::AbstractVector) + idx = findall(x -> x in p, spin_range) + return (length(idx) > 0 && idx == collect(first(idx):last(idx))) ? (first(idx):last(idx)) : idx +end + +expand(sr::SpinRange, Ns::Int) = sr +expand(sr::AllSpins, Ns::Int) = SpinRange(1:Ns) \ No newline at end of file diff --git a/KomaMRIBase/src/timing/TimeScale.jl b/KomaMRIBase/src/motion/motionlist/TimeSpan.jl similarity index 90% rename from KomaMRIBase/src/timing/TimeScale.jl rename to KomaMRIBase/src/motion/motionlist/TimeSpan.jl index ef4de6604..dcaa721a1 100644 --- a/KomaMRIBase/src/timing/TimeScale.jl +++ b/KomaMRIBase/src/motion/motionlist/TimeSpan.jl @@ -1,18 +1,17 @@ abstract type AbstractTimeSpan{T<:Real} end +# TimeRange @with_kw struct TimeRange{T<:Real} <: AbstractTimeSpan{T} t_start ::T - t_end ::T = t_start + t_end ::T @assert t_end >= t_start "t_end must be greater or equal than t_start" end -@with_kw struct Periodic{T<:Real} <: AbstractTimeSpan{T} - period::T - asymmetry::T = eltype(period)(0.5) -end +""" Constructors """ +TimeRange(t_start) = TimeRange(t_start, t_start) -times(ts::TimeRange) = [ts.t_start, ts.t_end] -times(ts::Periodic{T}) where {T<:Real} = [zero(T), ts.period * ts.asymmetry, ts.period] +""" times """ +times(ts::TimeRange) = [ts.t_start, ts.t_end] """ t_unit = unit_time(t, time_range) @@ -51,6 +50,20 @@ function unit_time(t::AbstractArray{T}, ts::TimeRange{T}) where {T<:Real} end end + + +# Periodic +@with_kw struct Periodic{T<:Real} <: AbstractTimeSpan{T} + period::T + asymmetry::T = typeof(period)(0.5) +end + +""" Constructors """ +Periodic(period) = Periodic(period, typeof(period)(0.5)) + +""" times """ +times(ts::Periodic{T}) where {T<:Real} = [zero(T), ts.period * ts.asymmetry, ts.period] + """ t_unit = unit_time(t, periodic) diff --git a/KomaMRIBase/src/datatypes/phantom/motion/ArbitraryMotion.jl b/KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl similarity index 65% rename from KomaMRIBase/src/datatypes/phantom/motion/ArbitraryMotion.jl rename to KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl index 5a41f425e..adad75f84 100644 --- a/KomaMRIBase/src/datatypes/phantom/motion/ArbitraryMotion.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl @@ -17,19 +17,21 @@ const Interpolator2D = Interpolations.GriddedInterpolation{ } """ - ArbitraryMotion + ArbitraryAction + +(...) """ -abstract type ArbitraryMotion{T<:Real} <: AbstractMotion{T} end +abstract type ArbitraryAction{T<:Real} <: AbstractActionSpan{T} end -function Base.getindex(motion::ArbitraryMotion, p::Union{AbstractRange, AbstractVector, Colon, Integer}) - return typeof(motion)(motion.time, [getfield(motion, d)[p,:] for d in filter(x -> x != :time, fieldnames(typeof(motion)))]...) +function Base.getindex(action::ArbitraryAction, p::Union{AbstractVector, Colon}) + return typeof(action)([getfield(action, d)[p,:] for d in fieldnames(typeof(action))]...) end -function Base.view(motion::ArbitraryMotion, p::Union{AbstractRange, AbstractVector, Colon, Integer}) - return typeof(motion)(motion.time, [@view(getfield(motion, d)[p,:]) for d in filter(x -> x != :time, fieldnames(typeof(motion)))]...) +function Base.view(action::ArbitraryAction, p::Union{AbstractVector, Colon}) + return typeof(action)([@view(getfield(action, d)[p,:]) for d in fieldnames(typeof(action))]...) end -Base.:(==)(m1::ArbitraryMotion, m2::ArbitraryMotion) = (typeof(m1) == typeof(m2)) & reduce(&, [getfield(m1, field) == getfield(m2, field) for field in fieldnames(typeof(m1))]) -Base.:(≈)(m1::ArbitraryMotion, m2::ArbitraryMotion) = (typeof(m1) == typeof(m2)) & reduce(&, [getfield(m1, field) ≈ getfield(m2, field) for field in fieldnames(typeof(m1))]) +Base.:(==)(m1::ArbitraryAction, m2::ArbitraryAction) = (typeof(m1) == typeof(m2)) & reduce(&, [getfield(m1, field) == getfield(m2, field) for field in fieldnames(typeof(m1))]) +Base.:(≈)(m1::ArbitraryAction, m2::ArbitraryAction) = (typeof(m1) == typeof(m2)) & reduce(&, [getfield(m1, field) ≈ getfield(m2, field) for field in fieldnames(typeof(m1))]) function GriddedInterpolation(nodes, A, ITP) return Interpolations.GriddedInterpolation{eltype(A), length(nodes), typeof(A), typeof(ITP), typeof(nodes)}(nodes, A, ITP) @@ -61,42 +63,42 @@ end function displacement_x!( ux::AbstractArray{T}, - motion::ArbitraryMotion{T}, + action::ArbitraryAction{T}, x::AbstractArray{T}, y::AbstractArray{T}, z::AbstractArray{T}, t::AbstractArray{T}, ) where {T<:Real} - itp = interpolate(motion.dx, Gridded(Linear()), Val(size(x,1))) - ux .= resample(itp, unit_time(t, motion.time)) + itp = interpolate(action.dx, Gridded(Linear()), Val(size(action.dx,1))) + ux .= resample(itp, t) return nothing end function displacement_y!( uy::AbstractArray{T}, - motion::ArbitraryMotion{T}, + action::ArbitraryAction{T}, x::AbstractArray{T}, y::AbstractArray{T}, z::AbstractArray{T}, t::AbstractArray{T}, ) where {T<:Real} - itp = interpolate(motion.dy, Gridded(Linear()), Val(size(x,1))) - uy .= resample(itp, unit_time(t, motion.time)) + itp = interpolate(action.dy, Gridded(Linear()), Val(size(action.dy,1))) + uy .= resample(itp, t) return nothing end function displacement_z!( uz::AbstractArray{T}, - motion::ArbitraryMotion{T}, + action::ArbitraryAction{T}, x::AbstractArray{T}, y::AbstractArray{T}, z::AbstractArray{T}, t::AbstractArray{T}, ) where {T<:Real} - itp = interpolate(motion.dz, Gridded(Linear()), Val(size(x,1))) - uz .= resample(itp, unit_time(t, motion.time)) + itp = interpolate(action.dz, Gridded(Linear()), Val(size(action.dz,1))) + uz .= resample(itp, t) return nothing end -include("arbitrarymotion/Trajectory.jl") -include("arbitrarymotion/FlowTrajectory.jl") \ No newline at end of file +include("arbitraryactions/Path.jl") +include("arbitraryactions/FlowPath.jl") \ No newline at end of file diff --git a/KomaMRIBase/src/motion/motionlist/actions/SimpleAction.jl b/KomaMRIBase/src/motion/motionlist/actions/SimpleAction.jl new file mode 100644 index 000000000..b9ab0544e --- /dev/null +++ b/KomaMRIBase/src/motion/motionlist/actions/SimpleAction.jl @@ -0,0 +1,13 @@ +""" + SimpleAction + +(...) +""" +abstract type SimpleAction{T<:Real} <: AbstractActionSpan{T} end + +Base.getindex(action::SimpleAction, p::Union{AbstractVector, Colon}) = action +Base.view(action::SimpleAction, p::Union{AbstractVector, Colon}) = action + +include("simpleactions/Translate.jl") +include("simpleactions/Rotate.jl") +include("simpleactions/HeartBeat.jl") \ No newline at end of file diff --git a/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/FlowTrajectory.jl b/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl similarity index 50% rename from KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/FlowTrajectory.jl rename to KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl index 254941a01..7000da0b3 100644 --- a/KomaMRIBase/src/datatypes/phantom/motion/arbitrarymotion/FlowTrajectory.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl @@ -1,25 +1,23 @@ @doc raw""" - flowtrajectory = FlowTrajectory(time, dx, dy, dz) + flowpath = FlowPath(dx, dy, dz) -FlowTrajectory motion struct. (...) +FlowPath motion struct. (...) # Arguments -- `time`: (`::AbstractTimeSpan{T<:Real}`, `[s]`) time scale - `dx`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in x - `dy`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in y - `dz`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in z - `spin_reset`: (`::AbstractArray{Bool}`) reset spin state flags # Returns -- `flowtrajectory`: (`::FlowTrajectory`) FlowTrajectory struct +- `flowpath: (`::FlowPath`) FlowPath struct # Examples ```julia-repl -julia> ftr = FlowTrajectory(time=TimeRange(0.0, 0.5), dx=[0.01 0.02], dy=[0.02 0.03], dz=[0.03 0.04], spin_reset=[false, false]) +julia> fp = FlowPath(dx=[0.01 0.02], dy=[0.02 0.03], dz=[0.03 0.04], spin_reset=[false, false]) ``` """ -struct FlowTrajectory{T<:Real, TS<:AbstractTimeSpan{T}} <: ArbitraryMotion{T} - time::TS +@with_kw struct FlowPath{T<:Real} <: ArbitraryAction{T} dx::AbstractArray{T} dy::AbstractArray{T} dz::AbstractArray{T} diff --git a/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/Path.jl b/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/Path.jl new file mode 100644 index 000000000..aa3aa0b41 --- /dev/null +++ b/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/Path.jl @@ -0,0 +1,23 @@ +@doc raw""" + path = Path(dx, dy, dz) + +Path motion struct. (...) + +# Arguments +- `dx`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in x +- `dy`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in y +- `dz`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in z + +# Returns +- `path`: (`::Path`) Path struct + +# Examples +```julia-repl +julia> p = Path(dx=[0.01 0.02], dy=[0.02 0.03], dz=[0.03 0.04]) +``` +""" +@with_kw struct Path{T<:Real} <: ArbitraryAction{T} + dx::AbstractArray{T} + dy::AbstractArray{T} + dz::AbstractArray{T} +end \ No newline at end of file diff --git a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/HeartBeat.jl b/KomaMRIBase/src/motion/motionlist/actions/simpleactions/HeartBeat.jl similarity index 56% rename from KomaMRIBase/src/datatypes/phantom/motion/simplemotion/HeartBeat.jl rename to KomaMRIBase/src/motion/motionlist/actions/simpleactions/HeartBeat.jl index ea1fdc211..0d8f3b215 100644 --- a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/HeartBeat.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/simpleactions/HeartBeat.jl @@ -1,11 +1,10 @@ @doc raw""" - heartbeat = HeartBeat(time, circumferential_strain, radial_strain, longitudinal_strain) + heartbeat = HeartBeat(circumferential_strain, radial_strain, longitudinal_strain) HeartBeat struct. It produces a heartbeat-like motion, characterised by three types of strain: Circumferential, Radial and Longitudinal # Arguments -- `time`: (`::AbstractTimeSpan{T<:Real}`, `[s]`) time scale - `circumferential_strain`: (`::Real`) contraction parameter - `radial_strain`: (`::Real`) contraction parameter - `longitudinal_strain`: (`::Real`) contraction parameter @@ -15,32 +14,30 @@ Circumferential, Radial and Longitudinal # Examples ```julia-repl -julia> hb = HeartBeat(time=Periodic(period=1.0, asymmetry=0.3), circumferential_strain=-0.3, radial_strain=-0.2, longitudinal_strain=0.0) +julia> hb = HeartBeat(circumferential_strain=-0.3, radial_strain=-0.2, longitudinal_strain=0.0) ``` """ -@with_kw struct HeartBeat{T<:Real, TS<:AbstractTimeSpan{T}} <: SimpleMotion{T} - time :: TS +@with_kw struct HeartBeat{T<:Real} <: SimpleAction{T} circumferential_strain :: T radial_strain :: T - longitudinal_strain :: T = typeof(circumferential_strain)(0.0) + longitudinal_strain :: T end -is_composable(motion::HeartBeat) = true +is_composable(action::HeartBeat) = true function displacement_x!( ux::AbstractArray{T}, - motion::HeartBeat{T}, + action::HeartBeat{T}, x::AbstractArray{T}, y::AbstractArray{T}, z::AbstractArray{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion.time) r = sqrt.(x .^ 2 + y .^ 2) θ = atan.(y, x) - Δ_circunferential = motion.circumferential_strain * maximum(r) - Δ_radial = -motion.radial_strain * (maximum(r) .- r) - Δr = t_unit .* (Δ_circunferential .+ Δ_radial) + Δ_circunferential = action.circumferential_strain * maximum(r) + Δ_radial = -action.radial_strain * (maximum(r) .- r) + Δr = t .* (Δ_circunferential .+ Δ_radial) # Map negative radius to r=0 neg = (r .+ Δr) .< 0 Δr = (.!neg) .* Δr @@ -51,18 +48,17 @@ end function displacement_y!( uy::AbstractArray{T}, - motion::HeartBeat{T}, + action::HeartBeat{T}, x::AbstractArray{T}, y::AbstractArray{T}, z::AbstractArray{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion.time) r = sqrt.(x .^ 2 + y .^ 2) θ = atan.(y, x) - Δ_circunferential = motion.circumferential_strain * maximum(r) - Δ_radial = -motion.radial_strain * (maximum(r) .- r) - Δr = t_unit .* (Δ_circunferential .+ Δ_radial) + Δ_circunferential = action.circumferential_strain * maximum(r) + Δ_radial = -action.radial_strain * (maximum(r) .- r) + Δr = t .* (Δ_circunferential .+ Δ_radial) # Map negative radius to r=0 neg = (r .+ Δr) .< 0 Δr = (.!neg) .* Δr @@ -73,13 +69,12 @@ end function displacement_z!( uz::AbstractArray{T}, - motion::HeartBeat{T}, + action::HeartBeat{T}, x::AbstractArray{T}, y::AbstractArray{T}, z::AbstractArray{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion.time) - uz .= t_unit .* (z .* motion.longitudinal_strain) + uz .= t .* (z .* action.longitudinal_strain) return nothing end \ No newline at end of file diff --git a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Rotation.jl b/KomaMRIBase/src/motion/motionlist/actions/simpleactions/Rotate.jl similarity index 66% rename from KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Rotation.jl rename to KomaMRIBase/src/motion/motionlist/actions/simpleactions/Rotate.jl index e511f78d0..3bdf5269e 100644 --- a/KomaMRIBase/src/datatypes/phantom/motion/simplemotion/Rotation.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/simpleactions/Rotate.jl @@ -1,7 +1,7 @@ @doc raw""" - rotation = Rotation(time, pitch, roll, yaw) + rotate = Rotate(pitch, roll, yaw) -Rotation motion struct. It produces a rotation of the phantom in the three axes: +Rotate motion struct. It produces a rotation of the phantom in the three axes: x (pitch), y (roll), and z (yaw). We follow the RAS (Right-Anterior-Superior) orientation, and the rotations are applied following the right-hand rule (counter-clockwise): @@ -38,44 +38,41 @@ R &= R_z(\alpha) R_y(\beta) R_x(\gamma) \\ ``` # Arguments -- `time`: (`::AbstractTimeSpan{T<:Real}`, `[s]`) time scale - `pitch`: (`::Real`, `[º]`) rotation in x - `roll`: (`::Real`, `[º]`) rotation in y - `yaw`: (`::Real`, `[º]`) rotation in z # Returns -- `rotation`: (`::Rotation`) Rotation struct +- `rotate`: (`::Rotate`) Rotate struct # Examples ```julia-repl -julia> rt = Rotation(time=TimeRange(0.0, 0.5), pitch=15.0, roll=0.0, yaw=20.0) +julia> rt = Rotate(pitch=15.0, roll=0.0, yaw=20.0) ``` """ -@with_kw struct Rotation{T<:Real, TS<:AbstractTimeSpan{T}} <: SimpleMotion{T} - time :: TS +@with_kw struct Rotate{T<:Real} <: SimpleAction{T} pitch :: T roll :: T yaw :: T end -RotationX(time::AbstractTimeSpan{T}, pitch::T) where {T<:Real} = Rotation(time, pitch, zero(T), zero(T)) -RotationY(time::AbstractTimeSpan{T}, roll::T) where {T<:Real} = Rotation(time, zero(T), roll, zero(T)) -RotationZ(time::AbstractTimeSpan{T}, yaw::T) where {T<:Real} = Rotation(time, zero(T), zero(T), yaw) +RotateX(pitch::T) where {T<:Real} = Rotate(pitch, zero(T), zero(T)) +RotateY(roll::T) where {T<:Real} = Rotate(zero(T), roll, zero(T)) +RotateZ(yaw::T) where {T<:Real} = Rotate(zero(T), zero(T), yaw) -is_composable(motion::Rotation) = true +is_composable(action::Rotate) = true function displacement_x!( ux::AbstractArray{T}, - motion::Rotation{T}, + action::Rotate{T}, x::AbstractArray{T}, y::AbstractArray{T}, z::AbstractArray{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion.time) - α = t_unit .* (motion.yaw) - β = t_unit .* (motion.roll) - γ = t_unit .* (motion.pitch) + α = t .* (action.yaw) + β = t .* (action.roll) + γ = t .* (action.pitch) ux .= cosd.(α) .* cosd.(β) .* x + (cosd.(α) .* sind.(β) .* sind.(γ) .- sind.(α) .* cosd.(γ)) .* y + (cosd.(α) .* sind.(β) .* cosd.(γ) .+ sind.(α) .* sind.(γ)) .* z .- x @@ -84,16 +81,15 @@ end function displacement_y!( uy::AbstractArray{T}, - motion::Rotation{T}, + action::Rotate{T}, x::AbstractArray{T}, y::AbstractArray{T}, z::AbstractArray{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion.time) - α = t_unit .* (motion.yaw) - β = t_unit .* (motion.roll) - γ = t_unit .* (motion.pitch) + α = t .* (action.yaw) + β = t .* (action.roll) + γ = t .* (action.pitch) uy .= sind.(α) .* cosd.(β) .* x + (sind.(α) .* sind.(β) .* sind.(γ) .+ cosd.(α) .* cosd.(γ)) .* y + (sind.(α) .* sind.(β) .* cosd.(γ) .- cosd.(α) .* sind.(γ)) .* z .- y @@ -102,16 +98,15 @@ end function displacement_z!( uz::AbstractArray{T}, - motion::Rotation{T}, + action::Rotate{T}, x::AbstractArray{T}, y::AbstractArray{T}, z::AbstractArray{T}, t::AbstractArray{T}, ) where {T<:Real} - t_unit = unit_time(t, motion.time) - α = t_unit .* (motion.yaw) - β = t_unit .* (motion.roll) - γ = t_unit .* (motion.pitch) + α = t .* (action.yaw) + β = t .* (action.roll) + γ = t .* (action.pitch) uz .= -sind.(β) .* x + cosd.(β) .* sind.(γ) .* y + cosd.(β) .* cosd.(γ) .* z .- z diff --git a/KomaMRIBase/src/motion/motionlist/actions/simpleactions/Translate.jl b/KomaMRIBase/src/motion/motionlist/actions/simpleactions/Translate.jl new file mode 100644 index 000000000..fa5e2cae7 --- /dev/null +++ b/KomaMRIBase/src/motion/motionlist/actions/simpleactions/Translate.jl @@ -0,0 +1,65 @@ +@doc raw""" + translation = Translate(dx, dy, dz) + +Translate motion struct. It produces a linear translation of the phantom. +Its fields are the final displacements in the three axes (dx, dy, dz). + +# Arguments +- `dx`: (`::Real`, `[m]`) translation in x +- `dy`: (`::Real`, `[m]`) translation in y +- `dz`: (`::Real`, `[m]`) translation in z + +# Returns +- `translate`: (`::Translate`) Translate struct + +# Examples +```julia-repl +julia> tr = Translate(dx=0.01, dy=0.02, dz=0.03) +``` +""" +@with_kw struct Translate{T<:Real} <: SimpleAction{T} + dx :: T + dy :: T + dz :: T +end + +TranslateX(dx::T) where {T<:Real} = Translate(dx, zero(T), zero(T)) +TranslateY(dy::T) where {T<:Real} = Translate(zero(T), dy, zero(T)) +TranslateZ(dz::T) where {T<:Real} = Translate(zero(T), zero(T), dz) + +function displacement_x!( + ux::AbstractArray{T}, + action::Translate{T}, + x::AbstractVector{T}, + y::AbstractVector{T}, + z::AbstractVector{T}, + t::AbstractArray{T}, +) where {T<:Real} + ux .= t.* action.dx + return nothing +end + +function displacement_y!( + uy::AbstractArray{T}, + action::Translate{T}, + x::AbstractVector{T}, + y::AbstractVector{T}, + z::AbstractVector{T}, + t::AbstractArray{T}, +) where {T<:Real} + uy .= t .* action.dy + return nothing +end + +function displacement_z!( + uz::AbstractArray{T}, + action::Translate{T}, + x::AbstractVector{T}, + y::AbstractVector{T}, + z::AbstractVector{T}, + t::AbstractArray{T}, +) where {T<:Real} + uz .= t .* action.dz + return nothing +end + diff --git a/KomaMRIBase/src/motion/nomotion/NoMotion.jl b/KomaMRIBase/src/motion/nomotion/NoMotion.jl new file mode 100644 index 000000000..c2f595344 --- /dev/null +++ b/KomaMRIBase/src/motion/nomotion/NoMotion.jl @@ -0,0 +1,53 @@ +struct NoMotion{T<:Real} <: AbstractMotionSet{T} end + +Base.getindex(mv::NoMotion, p::AbstractVector) = mv +Base.view(mv::NoMotion, p::AbstractVector) = mv + +""" Addition of NoMotions """ +Base.vcat(m1::NoMotion{T}, m2::NoMotion{T}, Ns1::Int, Ns2::Int) where {T<:Real} = NoMotion{T}() +function Base.vcat(m1::NoMotion{T}, m2::AbstractMotionSet{T}, Ns1::Int, Ns2::Int) where {T<:Real} + mv_aux = Motion{T}[] + for m in m2.motions + m_aux = copy(m) + if m_aux.spins == Colon() + m_aux.spins = 1:Ns2 + end + m_aux.spins = m_aux.spins .+ Ns1 + push!(mv_aux, m_aux) + end + return MotionList(mv_aux) +end +function Base.vcat(m1::AbstractMotionSet{T}, m2::NoMotion{T}, Ns1::Int, Ns2::Int) where {T<:Real} + mv_aux = Motion{T}[] + for m in m1.motions + m_aux = copy(m) + if m_aux.spins == Colon() + m_aux.spins = 1:Ns1 + end + push!(mv_aux, m_aux) + end + return MotionList(mv_aux) +end + +Base.:(==)(m1::NoMotion{T}, m2::NoMotion{T}) where {T<:Real} = true +Base.:(≈)(m1::NoMotion{T}, m2::NoMotion{T}) where {T<:Real} = true + +function get_spin_coords( + mv::NoMotion{T}, + x::AbstractVector{T}, + y::AbstractVector{T}, + z::AbstractVector{T}, + t::AbstractArray{T} +) where {T<:Real} + return x, y, z +end + +""" + times = times(motion) +""" +times(mv::NoMotion{T}) where {T<:Real} = [zero(T)] + +""" + sort_motions! +""" +sort_motions!(mv::NoMotion) = nothing \ No newline at end of file diff --git a/KomaMRIBase/test/runtests.jl b/KomaMRIBase/test/runtests.jl index 9ec13403a..8fba143cd 100644 --- a/KomaMRIBase/test/runtests.jl +++ b/KomaMRIBase/test/runtests.jl @@ -392,13 +392,13 @@ end @test yt == ph.y @test zt == ph.z end - @testset "Translation" begin + @testset "Translate" begin ph = Phantom(x=[1.0], y=[1.0]) t_start=0.0; t_end=1.0 t = collect(range(t_start, t_end, 11)) dx, dy, dz = [1.0, 0.0, 0.0] vx, vy, vz = [dx, dy, dz] ./ (t_end - t_start) - translation = MotionList(Translation(TimeRange(t_start, t_end), dx, dy, dz)) + translation = MotionList(Translate(dx, dy, dz, TimeRange(t_start, t_end))) xt, yt, zt = get_spin_coords(translation, ph.x, ph.y, ph.z, t') @test xt == ph.x .+ vx.*t' @test yt == ph.y .+ vy.*t' @@ -406,13 +406,13 @@ end # ----- t_start = t_end -------- t_start = t_end = 0.0 t = [-0.5, -0.25, 0.0, 0.25, 0.5] - translation = MotionList(Translation(TimeRange(t_start, t_end), dx, dy, dz)) + translation = MotionList(Translate(dx, dy, dz, TimeRange(t_start, t_end))) xt, yt, zt = get_spin_coords(translation, ph.x, ph.y, ph.z, t') @test xt == ph.x .+ dx*[0, 0, 1, 1, 1]' @test yt == ph.y .+ dy*[0, 0, 1, 1, 1]' @test zt == ph.z .+ dz*[0, 0, 1, 1, 1]' end - @testset "PeriodicTranslation" begin + @testset "PeriodicTranslate" begin ph = Phantom(x=[1.0], y=[1.0]) t_start=0.0; t_end=1.0 t = collect(range(t_start, t_end, 11)) @@ -420,20 +420,20 @@ end asymmetry = 0.5 dx, dy, dz = [1.0, 0.0, 0.0] vx, vy, vz = [dx, dy, dz] ./ (t_end - t_start) - periodictranslation = MotionList(Translation(Periodic(period, asymmetry), dx, dy, dz)) + periodictranslation = MotionList(Translate(dx, dy, dz, Periodic(period, asymmetry))) xt, yt, zt = get_spin_coords(periodictranslation, ph.x, ph.y, ph.z, t') @test xt == ph.x .+ vx.*t' @test yt == ph.y .+ vy.*t' @test zt == ph.z .+ vz.*t' end - @testset "Rotation" begin + @testset "Rotate" begin ph = Phantom(x=[1.0], y=[1.0]) t_start=0.0; t_end=1.0 t = collect(range(t_start, t_end, 11)) pitch = 45.0 roll = 0.0 yaw = 45.0 - rotation = MotionList(Rotation(TimeRange(t_start, t_end), pitch, roll, yaw)) + rotation = MotionList(Rotate(pitch, roll, yaw, TimeRange(t_start, t_end))) xt, yt, zt = get_spin_coords(rotation, ph.x, ph.y, ph.z, t') r = vcat(ph.x, ph.y, ph.z) R = rotz(π*yaw/180) * roty(π*roll/180) * rotx(π*pitch/180) @@ -444,7 +444,7 @@ end # ----- t_start = t_end -------- t_start = t_end = 0.0 t = [-0.5, -0.25, 0.0, 0.25, 0.5] - rotation = MotionList(Rotation(TimeRange(t_start, t_end), pitch, roll, yaw)) + rotation = MotionList(Rotate(pitch, roll, yaw, TimeRange(t_start, t_end))) xt, yt, zt = get_spin_coords(rotation, ph.x, ph.y, ph.z, t') @test xt ≈ [ph.x ph.x rot_x rot_x rot_x] @test yt ≈ [ph.y ph.y rot_y rot_y rot_y] @@ -459,7 +459,7 @@ end pitch = 45.0 roll = 0.0 yaw = 45.0 - periodicrotation = MotionList(Rotation(Periodic(period, asymmetry), pitch, roll, yaw)) + periodicrotation = MotionList(Rotate(pitch, roll, yaw, Periodic(period, asymmetry))) xt, yt, zt = get_spin_coords(periodicrotation, ph.x, ph.y, ph.z, t') r = vcat(ph.x, ph.y, ph.z) R = rotz(π*yaw/180) * roty(π*roll/180) * rotx(π*pitch/180) @@ -475,7 +475,7 @@ end circumferential_strain = -0.1 radial_strain = 0.0 longitudinal_strain = -0.1 - heartbeat = MotionList(HeartBeat(TimeRange(t_start, t_end), circumferential_strain, radial_strain, longitudinal_strain)) + heartbeat = MotionList(HeartBeat(circumferential_strain, radial_strain, longitudinal_strain, TimeRange(t_start, t_end))) xt, yt, zt = get_spin_coords(heartbeat, ph.x, ph.y, ph.z, t') r = sqrt.(ph.x .^ 2 + ph.y .^ 2) θ = atan.(ph.y, ph.x) @@ -485,7 +485,7 @@ end # ----- t_start = t_end -------- t_start = t_end = 0.0 t = [-0.5, -0.25, 0.0, 0.25, 0.5] - heartbeat = MotionList(HeartBeat(TimeRange(t_start, t_end), circumferential_strain, radial_strain, longitudinal_strain)) + heartbeat = MotionList(HeartBeat(circumferential_strain, radial_strain, longitudinal_strain, TimeRange(t_start, t_end))) xt, yt, zt = get_spin_coords(heartbeat, ph.x, ph.y, ph.z, t') r = sqrt.(ph.x .^ 2 + ph.y .^ 2) θ = atan.(ph.y, ph.x) @@ -505,7 +505,7 @@ end circumferential_strain = -0.1 radial_strain = 0.0 longitudinal_strain = -0.1 - periodicheartbeat = MotionList(HeartBeat(Periodic(period, asymmetry), circumferential_strain, radial_strain, longitudinal_strain)) + periodicheartbeat = MotionList(HeartBeat(circumferential_strain, radial_strain, longitudinal_strain, Periodic(period, asymmetry))) xt, yt, zt = get_spin_coords(periodicheartbeat, ph.x, ph.y, ph.z, t') r = sqrt.(ph.x .^ 2 + ph.y .^ 2) θ = atan.(ph.y, ph.x) @@ -513,7 +513,7 @@ end @test yt[:,end] == ph.y .* (1 .+ circumferential_strain * maximum(r) .* sin.(θ)) @test zt[:,end] == ph.z .* (1 .+ longitudinal_strain) end - @testset "Trajectory" begin + @testset "Path" begin # 1 spin ph = Phantom(x=[1.0], y=[1.0]) Ns = length(ph) @@ -523,7 +523,7 @@ end dx = rand(Ns, Nt) dy = rand(Ns, Nt) dz = rand(Ns, Nt) - arbitrarymotion = MotionList(Trajectory(TimeRange(t_start, t_end), dx, dy, dz)) + arbitrarymotion = MotionList(Path(dx, dy, dz, TimeRange(t_start, t_end))) t = range(t_start, t_end, Nt) xt, yt, zt = get_spin_coords(arbitrarymotion, ph.x, ph.y, ph.z, t') @test xt == ph.x .+ dx @@ -538,7 +538,7 @@ end dx = rand(Ns, Nt) dy = rand(Ns, Nt) dz = rand(Ns, Nt) - arbitrarymotion = MotionList(Trajectory(TimeRange(t_start, t_end), dx, dy, dz)) + arbitrarymotion = MotionList(Path(dx, dy, dz, TimeRange(t_start, t_end))) t = range(t_start, t_end, Nt) xt, yt, zt = get_spin_coords(arbitrarymotion, ph.x, ph.y, ph.z, t') @test xt == ph.x .+ dx @@ -547,15 +547,15 @@ end end simplemotion = MotionList( - Translation(time=Periodic(period=0.5, asymmetry=0.5), dx=0.05, dy=0.05, dz=0.0), - Rotation(time=TimeRange(t_start=0.05, t_end=0.5), pitch=0.0, roll=0.0, yaw=π / 2) + Translate(0.05, 0.05, 0.0, Periodic(period=0.5, asymmetry=0.5)), + Rotate(0.0, 0.0, 90.0, TimeRange(t_start=0.05, t_end=0.5)) ) Ns = length(obj1) Nt = 3 t_start = 0.0 t_end = 1.0 - arbitrarymotion = MotionList(Trajectory(TimeRange(t_start, t_end), 0.01 .* rand(Ns, Nt), 0.01 .* rand(Ns, Nt), 0.01 .* rand(Ns, Nt))) + arbitrarymotion = MotionList(Path(0.01 .* rand(Ns, Nt), 0.01 .* rand(Ns, Nt), 0.01 .* rand(Ns, Nt), TimeRange(t_start, t_end))) # Test phantom subset obs1 = Phantom( diff --git a/KomaMRICore/src/simulation/Flow.jl b/KomaMRICore/src/simulation/Flow.jl index 78af0d1df..a8f025c4d 100644 --- a/KomaMRICore/src/simulation/Flow.jl +++ b/KomaMRICore/src/simulation/Flow.jl @@ -7,18 +7,19 @@ end function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion::MotionList{T}, t::AbstractArray{T}) where {T<:Real} for m in motion.motions - reset_magnetization!(M, Mxy, m, t) + idx = KomaMRIBase.get_idx(m.spins) + reset_magnetization!(@view(M[idx]), @view(Mxy[idx, :]), m.action, t) end return nothing end -function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion::AbstractMotion{T}, t::AbstractArray{T}) where {T<:Real} +function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, action::KomaMRIBase.AbstractActionSpan{T}, t::AbstractArray{T}) where {T<:Real} return nothing end -function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion::FlowTrajectory{T}, t::AbstractArray{T}) where {T<:Real} - itp = interpolate(motion.spin_reset, Gridded(Constant{Previous}), Val(size(x,1))) - flags = resample(itp, unit_time(t, motion.time)) +function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, action::FlowPath{T}, t::AbstractArray{T}) where {T<:Real} + itp = interpolate(action.spin_reset, Gridded(Constant{Previous}), Val(size(action.spin_reset, 1))) + flags = resample(itp, unit_time(t, action.time)) reset = any(flags; dims=2) flags = .!(cumsum(flags; dims=2) .>= 1) Mxy .*= flags diff --git a/KomaMRICore/src/simulation/Functors.jl b/KomaMRICore/src/simulation/Functors.jl index 3217ab0a3..13c523fe5 100644 --- a/KomaMRICore/src/simulation/Functors.jl +++ b/KomaMRICore/src/simulation/Functors.jl @@ -53,6 +53,7 @@ x = gpu(x, CUDABackend()) """ gpu(x, backend::KA.GPU) = fmap(x -> adapt(backend, x), x; exclude=_isleaf) adapt_storage(backend::KA.GPU, xs::MotionList) = MotionList(gpu.(xs.motions, Ref(backend))) +adapt_storage(backend::KA.GPU, xs::Motion) = Motion(gpu(xs.action, backend), gpu(xs.time, backend), xs.spins) # To CPU """ @@ -77,6 +78,7 @@ adapt_storage(T::Type{<:Real}, xs::AbstractArray{<:Complex}) = convert.(Complex{ adapt_storage(T::Type{<:Real}, xs::AbstractArray{<:Bool}) = xs adapt_storage(T::Type{<:Real}, xs::NoMotion) = NoMotion{T}() adapt_storage(T::Type{<:Real}, xs::MotionList) = MotionList(paramtype.(T, xs.motions)) +adapt_storage(T::Type{<:Real}, xs::Motion) = Motion(paramtype(T, xs.action), paramtype(T, xs.time), xs.spins) """ f32(m) @@ -101,11 +103,11 @@ f64(m) = paramtype(Float64, m) #The functor macro makes it easier to call a function in all the parameters # Phantom @functor Phantom -@functor Translation -@functor Rotation +@functor Translate +@functor Rotate @functor HeartBeat -@functor Trajectory -@functor FlowTrajectory +@functor Path +@functor FlowPath @functor TimeRange @functor Periodic # Spinor diff --git a/KomaMRICore/src/simulation/SimMethods/Magnetization.jl b/KomaMRICore/src/simulation/SimMethods/Magnetization.jl index 1ed77bdcf..d718155fb 100644 --- a/KomaMRICore/src/simulation/SimMethods/Magnetization.jl +++ b/KomaMRICore/src/simulation/SimMethods/Magnetization.jl @@ -21,6 +21,9 @@ Base.getindex(M::Mag, i::Integer) = Mag(M.xy[i,:], M.z[i,:]) # M[a:b] Base.getindex(M::Mag, i::UnitRange) = Mag(M.xy[i], M.z[i]) Base.view(M::Mag, i::UnitRange) = @views Mag(M.xy[i], M.z[i]) +# M[:] +Base.getindex(M::Mag, i::Colon) = M[1:length(M.z)] +Base.view(M::Mag, i::Colon) = @view(M[1:length(M.z)]) # Definition of rotation Spinor×SpinStateRepresentation @doc raw""" diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index d42ff6afb..f69c351fe 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -301,7 +301,7 @@ end @test raw1.profiles[1].data ≈ raw2.profiles[1].data end -@testitem "Bloch SimpleMotion" tags=[:important, :core, :motion] begin +@testitem "Bloch SimpleAction" tags=[:important, :core, :motion] begin using Suppressor include("initialize_backend.jl") include(joinpath(@__DIR__, "test_files", "utils.jl")) @@ -318,11 +318,11 @@ end sig = @suppress simulate(obj, seq, sys; sim_params) sig = sig / prod(size(obj)) NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - println("NMRSE SimpleMotion: ", NMRSE(sig, sig_jemris)) + println("NMRSE SimpleAction: ", NMRSE(sig, sig_jemris)) @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end -@testitem "Bloch ArbitraryMotion" tags=[:important, :core, :motion] begin +@testitem "Bloch ArbitraryAction" tags=[:important, :core, :motion] begin using Suppressor include("initialize_backend.jl") include(joinpath(@__DIR__, "test_files", "utils.jl")) @@ -339,7 +339,7 @@ end sig = @suppress simulate(obj, seq, sys; sim_params) sig = sig / prod(size(obj)) NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - println("NMRSE ArbitraryMotion: ", NMRSE(sig, sig_jemris)) + println("NMRSE ArbitraryAction: ", NMRSE(sig, sig_jemris)) @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end @@ -390,7 +390,7 @@ end @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end -@testitem "BlochSimple SimpleMotion" tags=[:important, :core, :motion] begin +@testitem "BlochSimple SimpleAction" tags=[:important, :core, :motion] begin using Suppressor include("initialize_backend.jl") include(joinpath(@__DIR__, "test_files", "utils.jl")) @@ -408,11 +408,11 @@ end sig = @suppress simulate(obj, seq, sys; sim_params) sig = sig / prod(size(obj)) NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - println("NMRSE SimpleMotion BlochSimple: ", NMRSE(sig, sig_jemris)) + println("NMRSE SimpleAction BlochSimple: ", NMRSE(sig, sig_jemris)) @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end -@testitem "BlochSimple ArbitraryMotion" tags=[:important, :core, :motion] begin +@testitem "BlochSimple ArbitraryAction" tags=[:important, :core, :motion] begin using Suppressor include("initialize_backend.jl") include(joinpath(@__DIR__, "test_files", "utils.jl")) @@ -430,7 +430,7 @@ end sig = @suppress simulate(obj, seq, sys; sim_params) sig = sig / prod(size(obj)) NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - println("NMRSE ArbitraryMotion BlochSimple: ", NMRSE(sig, sig_jemris)) + println("NMRSE ArbitraryAction BlochSimple: ", NMRSE(sig, sig_jemris)) @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end diff --git a/KomaMRICore/test/test_files/utils.jl b/KomaMRICore/test/test_files/utils.jl index a194a1a16..012bc3a0c 100644 --- a/KomaMRICore/test/test_files/utils.jl +++ b/KomaMRICore/test/test_files/utils.jl @@ -18,7 +18,7 @@ end function phantom_brain_simple_motion() obj = phantom_brain() - obj.motion = MotionList(Translation(time=TimeRange(0.0, 10.0), dx=0.0, dy=1.0, dz=0.0)) + obj.motion = MotionList(Translate(0.0, 1.0, 0.0, TimeRange(0.0, 10.0))) return obj end @@ -30,11 +30,11 @@ function phantom_brain_arbitrary_motion() dx = zeros(Ns, 2) dz = zeros(Ns, 2) dy = [zeros(Ns,1) ones(Ns,1)] - obj.motion = MotionList(Trajectory( - TimeRange(t_start, t_end), + obj.motion = MotionList(Path( dx, dy, - dz)) + dz, + TimeRange(t_start, t_end))) return obj end diff --git a/KomaMRIFiles/src/Phantom/Phantom.jl b/KomaMRIFiles/src/Phantom/Phantom.jl index b695ed2ac..a2a3f38ee 100644 --- a/KomaMRIFiles/src/Phantom/Phantom.jl +++ b/KomaMRIFiles/src/Phantom/Phantom.jl @@ -5,10 +5,18 @@ Reads a (.phantom) file and creates a Phantom structure from it """ function read_phantom(filename::String) fid = HDF5.h5open(filename, "r") - phantom_fields = [] - version = read_attribute(fid, "Version") dims = read_attribute(fid, "Dims") Ns = read_attribute(fid, "Ns") + # Version + file_version = VersionNumber(read_attribute(fid, "Version")) + program_version = pkgversion(KomaMRIFiles) + if file_version.major != program_version.major | file_version.minor != program_version.minor + @warn "Version mismatch detected: + File version: $file_version + KomaMRIFiles version: $program_version + This may lead to compatibility issues. Please update the file or the program to the matching version." + end + phantom_fields = [] # Name name = read_attribute(fid, "Name") push!(phantom_fields, (:name, name)) @@ -16,108 +24,90 @@ function read_phantom(filename::String) for key in ["position", "contrast"] group = fid[key] for label in HDF5.keys(group) - param = group[label] - values = read_param(param) - if values != "Default" - push!(phantom_fields, (Symbol(label), values)) - end + values = read(group[label]) + push!(phantom_fields, (Symbol(label), values)) end end - # AbstractMotion - motion_group = fid["motion"] - import_motion!(phantom_fields, motion_group) - + # Motion + if "motion" in keys(fid) + motion_group = fid["motion"] + import_motion!(phantom_fields, motion_group) + end obj = Phantom(; phantom_fields...) close(fid) return obj end -function read_param(param::HDF5.Group) - if "type" in HDF5.keys(attrs(param)) - type = attrs(param)["type"] - if type == "Explicit" - values = read(param["values"]) - elseif type == "Indexed" - index = read(param["values"]) - @assert Ns == length(index) "Error: $(label) vector dimensions mismatch" - table = read(param["table"]) - N = read_attribute(param, "N") - @assert N == length(table) "Error: $(label) table dimensions mismatch" - values = table[index] - elseif type == "Default" - values = "Default" - end - else - values = read(param["values"]) - end - return values -end - function import_motion!(phantom_fields::Array, motion_group::HDF5.Group) T = eltype(phantom_fields[2][2]) - motion_type = read_attribute(motion_group, "type") - if motion_type == "MotionList" - simple_motion_types = last.(split.(string.(reduce(vcat,(subtypes(subtypes(AbstractMotion)[2])))), ".")) - arbitrary_motion_types = last.(split.(string.(reduce(vcat,(subtypes(subtypes(AbstractMotion)[1])))), ".")) - motion_array = AbstractMotion{T}[] - for key in keys(motion_group) - type_group = motion_group[key] - type_str = split(key, "_")[2] - @assert type_str in vcat(simple_motion_types, arbitrary_motion_types) "Motion Type: $(type_str) has not been implemented in KomaMRIBase $(KomaMRIBase.__VERSION__)" - args = [] - for smtype in subtypes(SimpleMotion) - if type_str == last(split(string(smtype), ".")) - time = import_time_range(type_group["time"]) - type_fields = filter(x -> x != :time, fieldnames(smtype)) - for key in type_fields - push!(args, read_attribute(type_group, string(key))) - end - push!(motion_array, smtype(time, args...)) - end - end - for amtype in subtypes(ArbitraryMotion) - if type_str == last(split(string(amtype), ".")) - time = import_time_range(type_group["time"]) - type_fields = filter(x -> x != :time, fieldnames(amtype)) - for key in type_fields - push!(args, read(type_group[string(key)])) - end - push!(motion_array, amtype(time, args...)) - end - end + motion_array = Motion{T}[] + for key in keys(motion_group) + motion = motion_group[key] + motion_fields = [] + for name in keys(motion) # action, time, spins + import_motion_field!(motion_fields, motion, name) end - return push!(phantom_fields, (:motion, MotionList(motion_array))) - elseif motion_type == "NoMotion" - return push!(phantom_fields, (:motion, NoMotion{T}())) + push!(motion_array, Motion(; motion_fields...)) end + push!(phantom_fields, (:motion, MotionList(motion_array))) end -function import_time_range(times_group::HDF5.Group) - time_scale_type = read_attribute(times_group, "type") - for tstype in subtypes(AbstractTimeSpan) - if time_scale_type == last(split(string(tstype), ".")) - args = [] - for key in filter(x -> x != :type, fieldnames(tstype)) - push!(args, read_attribute(times_group, string(key))) +function import_motion_field!(motion_fields::Array, motion::HDF5.Group, name::String) + field_group = motion[name] + type = read_attribute(field_group, "type") + + get_subtypes(t::Type) = reduce(vcat,(subtypes(t))) + get_subtype_strings(t::Type) = last.(split.(string.(get_subtypes(t::Type)), ".")) + + subtype_strings = reduce(vcat, get_subtype_strings.([ + KomaMRIBase.SimpleAction, + KomaMRIBase.ArbitraryAction, + KomaMRIBase.AbstractTimeSpan, + KomaMRIBase.AbstractSpinSpan + ])) + + subtype_vector = reduce(vcat, get_subtypes.([ + KomaMRIBase.SimpleAction, + KomaMRIBase.ArbitraryAction, + KomaMRIBase.AbstractTimeSpan, + KomaMRIBase.AbstractSpinSpan + ])) + + motion_subfields = [] + for (i, subtype_string) in enumerate(subtype_strings) + if type == subtype_string + for subname in fieldnames(subtype_vector[i]) # dx, dy, dz, pitch, roll... + key = string(subname) + subfield_value = key in keys(field_group) ? read(field_group, key) : read_attribute(field_group, key) + import_motion_subfield!(motion_subfields, subfield_value, key) end - return tstype(args...) + push!(motion_fields, (Symbol(name), subtype_vector[i](motion_subfields...))) end end end +function import_motion_subfield!(motion_subfields::Array, subfield_value::Union{Real, Array}, key::String) + push!(motion_subfields, subfield_value) + return nothing +end +function import_motion_subfield!(motion_subfields::Array, subfield_value::String, key::String) + endpoints = parse.(Int, split(subfield_value, ":")) + range = length(endpoints) == 3 ? (endpoints[1]:endpoints[2]:endpoints[3]) : (endpoints[1]:endpoints[2]) + push!(motion_subfields, range) + return nothing +end + + """ phantom = write_phantom(ph,filename) Writes a (.phantom) file from a Phantom struct. """ function write_phantom( - # By the moment, only "Explicit" type - # is considered when writing .phantom files obj::Phantom, filename::String; store_coords=[:x, :y, :z], - store_contrasts=[:ρ, :T1, :T2, :T2s, :Δw], - store_motion=true, + store_contrasts=[:ρ, :T1, :T2, :T2s, :Δw] ) # Create HDF5 phantom file fid = h5open(filename, "w") @@ -130,51 +120,47 @@ function write_phantom( # Positions pos = create_group(fid, "position") for x in store_coords - create_group(pos, String(x))["values"] = getfield(obj, x) + pos[String(x)] = getfield(obj, x) end # Contrast (Rho, T1, T2, T2s Deltaw) contrast = create_group(fid, "contrast") for x in store_contrasts - param = create_group(contrast, String(x)) - HDF5.attributes(param)["type"] = "Explicit" #TODO: consider "Indexed" type - param["values"] = getfield(obj, x) + contrast[String(x)] = getfield(obj, x) end # Motion - if store_motion + if typeof(obj.motion) <: MotionList motion_group = create_group(fid, "motion") export_motion!(motion_group, obj.motion) end return close(fid) end -function export_motion!(motion_group::HDF5.Group, mv::MotionList{T}) where {T<:Real} - HDF5.attributes(motion_group)["type"] = "MotionList" - for (counter, m) in enumerate(mv.motions) - type_name = typeof(m).name.name - type_group = create_group(motion_group, "$(counter)_$type_name") - export_time_range!(type_group, m.time) - type_fields = filter(x -> x != :time, fieldnames(typeof(m))) - for field in type_fields - field_value = getfield(m, field) - if typeof(field_value) <: Number - HDF5.attributes(type_group)[string(field)] = field_value - elseif typeof(field_value) <: AbstractArray - type_group[string(field)] = field_value - end +function export_motion!(motion_group::HDF5.Group, motion_list::MotionList) + sort_motions!(motion_list) + for (counter, m) in enumerate(motion_list.motions) + motion = create_group(motion_group, "motion_$(counter)") + for key in fieldnames(Motion) # action, time, spins + field_group = create_group(motion, string(key)) + field_value = getfield(m, key) + export_motion_field!(field_group, field_value) end end end -function export_motion!(motion_group::HDF5.Group, motion::NoMotion{T}) where {T<:Real} - HDF5.attributes(motion_group)["type"] = "NoMotion" +function export_motion_field!(field_group::HDF5.Group, field_value) + HDF5.attributes(field_group)["type"] = string(typeof(field_value).name.name) + for subname in fieldnames(typeof(field_value)) # dx, dy, dz, pitch, roll... + subfield = getfield(field_value, subname) + export_motion_subfield!(field_group, subfield, string(subname)) + end end -function export_time_range!(type_group::HDF5.Group, time::AbstractTimeSpan) - times_name = typeof(time).name.name - times_group = create_group(type_group, "time") - HDF5.attributes(times_group)["type"] = string(times_name) - for field in fieldnames(typeof(time)) - field_value = getfield(time, field) - HDF5.attributes(times_group)[string(field)] = field_value - end +function export_motion_subfield!(field_group::HDF5.Group, subfield::Real, subname::String) + HDF5.attributes(field_group)[subname] = subfield +end +function export_motion_subfield!(field_group::HDF5.Group, subfield::AbstractRange, subname::String) + HDF5.attributes(field_group)[subname] = step(subfield) == 1 ? "$(first(subfield)):$(last(subfield))" : "$(first(subfield)):$(step(subfield)):$(last(subfield))" +end +function export_motion_subfield!(field_group::HDF5.Group, subfield::Array, subname::String) + field_group[subname] = subfield end \ No newline at end of file diff --git a/KomaMRIFiles/test/runtests.jl b/KomaMRIFiles/test/runtests.jl index b9dbda971..81fadba70 100644 --- a/KomaMRIFiles/test/runtests.jl +++ b/KomaMRIFiles/test/runtests.jl @@ -59,30 +59,21 @@ using TestItems, TestItemRunner obj2 = read_phantom(filename) @test obj1 == obj2 end - @testset "SimpleMotion" begin - # SimpleMotion + @testset "SimpleAction" begin + # SimpleAction path = @__DIR__ filename = path * "/test_files/brain_simplemotion_w.phantom" obj1 = brain_phantom2D() obj1.motion = MotionList( - Rotation( - time=Periodic(period=1.0), - yaw=45.0, - pitch=0.0, - roll=0.0), - Translation( - time=TimeRange(t_start=0.0, t_end=0.5), - dx=0.0, - dy=0.02, - dz=0.0 - ) + Rotate(0.0, 0.0, 45.0, Periodic(period=1.0)), + Translate(0.0, 0.02, 0.0, TimeRange(t_start=0.0, t_end=0.5)) ) write_phantom(obj1, filename) obj2 = read_phantom(filename) @test obj1 == obj2 end - @testset "ArbitraryMotion" begin - # ArbitraryMotion + @testset "ArbitraryAction" begin + # ArbitraryAction path = @__DIR__ filename = path * "/test_files/brain_arbitrarymotion_w.phantom" obj1 = brain_phantom2D() @@ -90,11 +81,11 @@ using TestItems, TestItemRunner K = 10 t_start = 0.0 t_end = 1.0 - obj1.motion = MotionList(Trajectory( - TimeRange(t_start, t_end), + obj1.motion = MotionList(Path( 0.01.*rand(Ns, K-1), 0.01.*rand(Ns, K-1), - 0.01.*rand(Ns, K-1))) + 0.01.*rand(Ns, K-1), + TimeRange(t_start, t_end))) write_phantom(obj1, filename) obj2 = read_phantom(filename) @test obj1 == obj2 diff --git a/KomaMRIPlots/src/ui/DisplayFunctions.jl b/KomaMRIPlots/src/ui/DisplayFunctions.jl index c4c9ff9fc..f361d2f7a 100644 --- a/KomaMRIPlots/src/ui/DisplayFunctions.jl +++ b/KomaMRIPlots/src/ui/DisplayFunctions.jl @@ -1033,7 +1033,7 @@ function plot_phantom_map( kwargs..., ) - function interpolate_times(motion::AbstractMotionList{T}) where {T<:Real} + function interpolate_times(motion::KomaMRIBase.AbstractMotionSet{T}) where {T<:Real} t = times(motion) if length(t)>1 # Interpolate time points (as many as indicated by intermediate_time_samples) @@ -1043,7 +1043,7 @@ function plot_phantom_map( return t end - function process_times(motion::AbstractMotionList{T}) where {T<:Real} + function process_times(motion::KomaMRIBase.AbstractMotionSet{T}) where {T<:Real} sort_motions!(motion) t = interpolate_times(motion) # Decimate time points so their number is smaller than max_time_samples diff --git a/docs/src/reference/2-koma-base.md b/docs/src/reference/2-koma-base.md index cf6cd8536..eb626311e 100644 --- a/docs/src/reference/2-koma-base.md +++ b/docs/src/reference/2-koma-base.md @@ -27,31 +27,31 @@ sort_motions! get_spin_coords ``` -### `SimpleMotion <: AbstractMotion` +### `SimpleAction <: AbstractActionSpan` ```@docs -SimpleMotion +SimpleAction ``` -### `SimpleMotion` types +### `SimpleAction` types ```@docs -Translation -Rotation +Translate +Rotate HeartBeat ``` -### `ArbitraryMotion <: AbstractMotion` +### `ArbitraryAction <: AbstractActionSpan` ```@docs -ArbitraryMotion +ArbitraryAction ``` -### `ArbitraryMotion` types +### `ArbitraryAction` types ```@docs -Trajectory -FlowTrajectory +Path +FlowPath ``` ## `Sequence`-related functions diff --git a/examples/3.tutorials/lit-05-SimpleMotion.jl b/examples/3.tutorials/lit-05-SimpleMotion.jl index f1e6c4f7b..31f14dc1b 100644 --- a/examples/3.tutorials/lit-05-SimpleMotion.jl +++ b/examples/3.tutorials/lit-05-SimpleMotion.jl @@ -4,8 +4,8 @@ using KomaMRI # hide sys = Scanner() # hide # It can also be interesting to see the effect of the patient's motion during an MRI scan. -# For this, Koma provides the ability to add `motion <: AbstractMotionList` to the phantom. -# In this tutorial, we will show how to add a [`Translation`](@ref) motion to a 2D brain phantom. +# For this, Koma provides the ability to add `motion <: AbstractMotionSet` to the phantom. +# In this tutorial, we will show how to add a [`Translate`](@ref) motion to a 2D brain phantom. # First, let's load the 2D brain phantom used in the previous tutorials: obj = brain_phantom2D() @@ -13,10 +13,10 @@ obj.Δw .= 0 # hide # ### Head Translation # -# In this example, we will add a [`Translation`](@ref) of 2 cm in x, with duration of 200 ms (v = 0.1 m/s): +# In this example, we will add a [`Translate`](@ref) of 2 cm in x, with duration of 200 ms (v = 0.1 m/s): obj.motion = MotionList( - Translation(time=TimeRange(t_start=0.0, t_end=200e-3), dx=2e-2, dy=0.0, dz=0.0) + Translate(2e-2, 0.0, 0.0, TimeRange(t_start=0.0, t_end=200e-3)) ) p1 = plot_phantom_map(obj, :T2 ; height=450, intermediate_time_samples=4) # hide @@ -67,7 +67,7 @@ p2 = plot_image(abs.(image1[:, :, 1]); height=400) # hide # S_{\mathrm{MC}}\left(t\right)=S\left(t\right)\cdot\mathrm{e}^{\mathrm{i}\Delta\phi_{\mathrm{corr}}}=S\left(t\right)\cdot\mathrm{e}^{\mathrm{i}2\pi\boldsymbol{k}\left(t\right)\cdot\boldsymbol{u}\left(t\right)} # ``` -# In practice, we would need to estimate or measure the motion before performing a motion-corrected reconstruction, but for this example, we will directly use the displacement functions ``\boldsymbol{u}(\boldsymbol{x}, t)`` defined by `obj.motion::SimpleMotion`. +# In practice, we would need to estimate or measure the motion before performing a motion-corrected reconstruction, but for this example, we will directly use the displacement functions ``\boldsymbol{u}(\boldsymbol{x}, t)`` defined by `obj.motion::SimpleAction`. # Since translations are rigid motions (``\boldsymbol{u}(\boldsymbol{x}, t)=\boldsymbol{u}(t)`` no position dependence), we can obtain the required displacements by calculating ``\boldsymbol{u}(\boldsymbol{x}=\boldsymbol{0},\ t=t_{\mathrm{adc}})``. sample_times = get_adc_sampling_times(seq1) displacements = hcat(get_spin_coords(obj.motion, [0.0], [0.0], [0.0], sample_times)...) From 725e68a690e6186ba3625317683ede776df24a87 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Sat, 24 Aug 2024 14:17:39 +0200 Subject: [PATCH 33/91] Try again to test cpu multi-thread error --- KomaMRIBase/src/motion/motionlist/MotionList.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/KomaMRIBase/src/motion/motionlist/MotionList.jl b/KomaMRIBase/src/motion/motionlist/MotionList.jl index e67348520..9a5958072 100644 --- a/KomaMRIBase/src/motion/motionlist/MotionList.jl +++ b/KomaMRIBase/src/motion/motionlist/MotionList.jl @@ -76,8 +76,8 @@ function get_spin_coords( # Buffers for positions: xt, yt, zt = x .+ 0*t, y .+ 0*t, z .+ 0*t # Buffers for displacements: - ux, uy, uz = xt .* 0, yt .* 0, zt .* 0 - # Composable motions: they need to be run sequentially. Note that they depend on xt, yt , and zt + ux, uy, uz = xt .* zero(T), yt .* zero(T), zt .* zero(T) + # Composable motions: they need to be run sequentially. Note that they depend on xt, yt, and zt for m in Iterators.filter(is_composable, ml.motions) t_unit = unit_time(t, m.time) idx = get_idx(m.spins) @@ -85,7 +85,7 @@ function get_spin_coords( displacement_y!(@view(uy[idx, :]), m.action, @view(xt[idx, :]), @view(yt[idx, :]), @view(zt[idx, :]), t_unit) displacement_z!(@view(uz[idx, :]), m.action, @view(xt[idx, :]), @view(yt[idx, :]), @view(zt[idx, :]), t_unit) xt .+= ux; yt .+= uy; zt .+= uz - ux .*= 0; uy .*= 0; uz .*= 0 + ux .*= zero(T); uy .*= zero(T); uz .*= zero(T) end # Additive motions: these motions can be run in parallel for m in Iterators.filter(!is_composable, ml.motions) @@ -95,7 +95,7 @@ function get_spin_coords( displacement_y!(@view(uy[idx, :]), m.action, @view(x[idx]), @view(y[idx]), @view(z[idx]), t_unit) displacement_z!(@view(uz[idx, :]), m.action, @view(x[idx]), @view(y[idx]), @view(z[idx]), t_unit) xt .+= ux; yt .+= uy; zt .+= uz - ux .*= 0; uy .*= 0; uz .*= 0 + ux .*= zero(T); uy .*= zero(T); uz .*= zero(T) end return xt, yt, zt end From 057ba723d69fdcbcf3f20bfba8737cd5bb1463f4 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Sat, 24 Aug 2024 14:24:25 +0200 Subject: [PATCH 34/91] Docstrings to pass CI --- KomaMRIBase/src/motion/motionlist/Motion.jl | 5 +++++ KomaMRIBase/src/motion/motionlist/MotionList.jl | 5 +++++ KomaMRIBase/src/motion/motionlist/TimeSpan.jl | 12 ++++++++++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/KomaMRIBase/src/motion/motionlist/Motion.jl b/KomaMRIBase/src/motion/motionlist/Motion.jl index 89870175b..acfc075d7 100644 --- a/KomaMRIBase/src/motion/motionlist/Motion.jl +++ b/KomaMRIBase/src/motion/motionlist/Motion.jl @@ -1,3 +1,8 @@ +""" + m = Motion(action, time, spins) + +Motion struct. (...) +""" @with_kw mutable struct Motion{T<:Real} action::AbstractActionSpan{T} time::AbstractTimeSpan{T} diff --git a/KomaMRIBase/src/motion/motionlist/MotionList.jl b/KomaMRIBase/src/motion/motionlist/MotionList.jl index 9a5958072..5702bcfa4 100644 --- a/KomaMRIBase/src/motion/motionlist/MotionList.jl +++ b/KomaMRIBase/src/motion/motionlist/MotionList.jl @@ -1,3 +1,8 @@ +""" + m_list = MotionList(motion_array...) + +MotionList struct. (...) +""" struct MotionList{T<:Real} <: AbstractMotionSet{T} motions::Vector{<:Motion{T}} end diff --git a/KomaMRIBase/src/motion/motionlist/TimeSpan.jl b/KomaMRIBase/src/motion/motionlist/TimeSpan.jl index dcaa721a1..e35100780 100644 --- a/KomaMRIBase/src/motion/motionlist/TimeSpan.jl +++ b/KomaMRIBase/src/motion/motionlist/TimeSpan.jl @@ -1,6 +1,10 @@ abstract type AbstractTimeSpan{T<:Real} end -# TimeRange +""" + tr = TimeRange(t_start, t_end) + +TimeRange struct. (...) +""" @with_kw struct TimeRange{T<:Real} <: AbstractTimeSpan{T} t_start ::T t_end ::T @@ -52,7 +56,11 @@ end -# Periodic +""" + p = Periodic(period, asymmetry) + +Periodic struct. (...) +""" @with_kw struct Periodic{T<:Real} <: AbstractTimeSpan{T} period::T asymmetry::T = typeof(period)(0.5) From a1330a465a38a9cba4a0842409d2592e38ca7a1b Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Sat, 24 Aug 2024 16:34:30 +0200 Subject: [PATCH 35/91] Solve multi-thread bug --- KomaMRIBase/src/motion/motionlist/SpinSpan.jl | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/KomaMRIBase/src/motion/motionlist/SpinSpan.jl b/KomaMRIBase/src/motion/motionlist/SpinSpan.jl index e5b7b23eb..36847c5ed 100644 --- a/KomaMRIBase/src/motion/motionlist/SpinSpan.jl +++ b/KomaMRIBase/src/motion/motionlist/SpinSpan.jl @@ -6,11 +6,6 @@ struct AllSpins <: AbstractSpinSpan end Base.getindex(spins::AllSpins, p::AbstractVector) = Colon(), spins Base.view(spins::AllSpins, p::AbstractVector) = Colon(), spins -Base.getindex(x, p::AllSpins) = x -Base.getindex(x, p::AllSpins, q) = x[:, q] -Base.view(x, p::AllSpins) = x -Base.view(x, p::AllSpins, q) = @view(x[:, q]) - get_idx(spins::AllSpins) = Colon() has_spins(spins::AllSpins) = true @@ -24,23 +19,18 @@ SpinRange(range::BitVector) = SpinRange(findall(x->x==true, range)) function Base.getindex(spins::SpinRange, p::AbstractVector) idx = get_idx(spins.range, p) - spin_range = SpinRange(spins.range[idx]) + spin_range = SpinRange(spins.range[idx] .- minimum(p) .+ 1) return idx, spin_range end function Base.view(spins::SpinRange, p::AbstractVector) idx = get_idx(spins.range, p) - spin_range = SpinRange(@view(spins.range[idx])) + spin_range = SpinRange(@view(spins.range[idx]) .- minimum(p) .+ 1) return idx, spin_range end Base.getindex(spins::SpinRange, b::BitVector) = spins[findall(x->x==true, b)] Base.view(spins::SpinRange, b::BitVector) = @view(spins[findall(x->x==true, b)]) -Base.getindex(x, p::SpinRange) = x[p.range] -Base.getindex(x, p::SpinRange, q) = x[p.range, q] -Base.view(x, p::SpinRange) = @view(x[p.range]) -Base.view(x, p::SpinRange, q) = @view(x[p.range, q]) - Base.:(==)(sr1::SpinRange, sr2::SpinRange) = sr1.range == sr2.range get_idx(spins::SpinRange) = spins.range From 8e44e492a5f86d221068463a15243b64f2d71b75 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Sat, 24 Aug 2024 17:28:44 +0200 Subject: [PATCH 36/91] Solve bug, now it is --- KomaMRIBase/src/motion/motionlist/SpinSpan.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/KomaMRIBase/src/motion/motionlist/SpinSpan.jl b/KomaMRIBase/src/motion/motionlist/SpinSpan.jl index 36847c5ed..9c9abb3ba 100644 --- a/KomaMRIBase/src/motion/motionlist/SpinSpan.jl +++ b/KomaMRIBase/src/motion/motionlist/SpinSpan.jl @@ -3,8 +3,8 @@ abstract type AbstractSpinSpan end # AllSpins struct AllSpins <: AbstractSpinSpan end -Base.getindex(spins::AllSpins, p::AbstractVector) = Colon(), spins -Base.view(spins::AllSpins, p::AbstractVector) = Colon(), spins +Base.getindex(spins::AllSpins, p::AbstractVector) = p, spins +Base.view(spins::AllSpins, p::AbstractVector) = p, spins get_idx(spins::AllSpins) = Colon() has_spins(spins::AllSpins) = true From 0121d2dc96eb243dbc300bd5208b327644112d3a Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Sat, 24 Aug 2024 17:49:52 +0200 Subject: [PATCH 37/91] Solve docs CI? --- KomaMRIBase/src/motion/motionlist/SpinSpan.jl | 12 +++++++-- KomaMRIBase/src/motion/nomotion/NoMotion.jl | 5 ++++ docs/src/reference/2-koma-base.md | 26 ++++++------------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/KomaMRIBase/src/motion/motionlist/SpinSpan.jl b/KomaMRIBase/src/motion/motionlist/SpinSpan.jl index 9c9abb3ba..f6f91f15b 100644 --- a/KomaMRIBase/src/motion/motionlist/SpinSpan.jl +++ b/KomaMRIBase/src/motion/motionlist/SpinSpan.jl @@ -1,6 +1,10 @@ abstract type AbstractSpinSpan end -# AllSpins +""" + as = AllSpin() + +AllSpin struct. (...) +""" struct AllSpins <: AbstractSpinSpan end Base.getindex(spins::AllSpins, p::AbstractVector) = p, spins @@ -9,7 +13,11 @@ Base.view(spins::AllSpins, p::AbstractVector) = p, spins get_idx(spins::AllSpins) = Colon() has_spins(spins::AllSpins) = true -# SpinRange +""" + sr = SpinRange(range) + +SpinRange struct. (...) +""" @with_kw struct SpinRange <: AbstractSpinSpan range::AbstractVector end diff --git a/KomaMRIBase/src/motion/nomotion/NoMotion.jl b/KomaMRIBase/src/motion/nomotion/NoMotion.jl index c2f595344..f6a00d1e5 100644 --- a/KomaMRIBase/src/motion/nomotion/NoMotion.jl +++ b/KomaMRIBase/src/motion/nomotion/NoMotion.jl @@ -1,3 +1,8 @@ +""" + nm = NoMotion{T<:Real}() + +NoMotion struct. (...) +""" struct NoMotion{T<:Real} <: AbstractMotionSet{T} end Base.getindex(mv::NoMotion, p::AbstractVector) = mv diff --git a/docs/src/reference/2-koma-base.md b/docs/src/reference/2-koma-base.md index eb626311e..7c9d1a7d9 100644 --- a/docs/src/reference/2-koma-base.md +++ b/docs/src/reference/2-koma-base.md @@ -23,33 +23,23 @@ heart_phantom ### `MotionList`-related functions ```@docs +NoMotion +MotionList +Motion sort_motions! get_spin_coords +TimeRange +Periodic +AllSpins +SpinRange ``` -### `SimpleAction <: AbstractActionSpan` - -```@docs -SimpleAction -``` - -### `SimpleAction` types +### `AbstractActionSpan` types ```@docs Translate Rotate HeartBeat -``` - -### `ArbitraryAction <: AbstractActionSpan` - -```@docs -ArbitraryAction -``` - -### `ArbitraryAction` types - -```@docs Path FlowPath ``` From c63aa40e13cbdb1357d2729ad2f118d55e2dbef8 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 26 Aug 2024 10:12:35 +0200 Subject: [PATCH 38/91] Compare MotionLists' lengths --- KomaMRIBase/src/motion/motionlist/MotionList.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/KomaMRIBase/src/motion/motionlist/MotionList.jl b/KomaMRIBase/src/motion/motionlist/MotionList.jl index 5702bcfa4..d5cac0189 100644 --- a/KomaMRIBase/src/motion/motionlist/MotionList.jl +++ b/KomaMRIBase/src/motion/motionlist/MotionList.jl @@ -45,16 +45,21 @@ end """ Compare two MotionLists """ function Base.:(==)(mv1::MotionList{T}, mv2::MotionList{T}) where {T<:Real} + if length(mv1) != length(mv2) return false end sort_motions!(mv1) sort_motions!(mv2) return reduce(&, mv1.motions .== mv2.motions) end function Base.:(≈)(mv1::MotionList{T}, mv2::MotionList{T}) where {T<:Real} + if length(mv1) != length(mv2) return false end sort_motions!(mv1) sort_motions!(mv2) return reduce(&, mv1.motions .≈ mv2.motions) end +""" MotionList length """ +Base.length(m::MotionList) = length(m.motions) + """ x, y, z = get_spin_coords(motion, x, y, z, t) @@ -108,7 +113,7 @@ end """ times = times(motion) """ -times(ml::MotionList{T}) where {T<:Real} = begin +function times(ml::MotionList{T}) where {T<:Real} nodes = reduce(vcat, [times(m) for m in ml.motions]; init=[zero(T)]) return unique(sort(nodes)) end From d550bcd47021cf4b7ae3267fdbb4db87db178d30 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 26 Aug 2024 14:07:45 +0200 Subject: [PATCH 39/91] Solve bugs --- KomaMRIBase/src/motion/motionlist/SpinSpan.jl | 8 +++----- KomaMRIBase/src/motion/nomotion/NoMotion.jl | 10 +++------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/KomaMRIBase/src/motion/motionlist/SpinSpan.jl b/KomaMRIBase/src/motion/motionlist/SpinSpan.jl index f6f91f15b..412dbf18c 100644 --- a/KomaMRIBase/src/motion/motionlist/SpinSpan.jl +++ b/KomaMRIBase/src/motion/motionlist/SpinSpan.jl @@ -27,13 +27,11 @@ SpinRange(range::BitVector) = SpinRange(findall(x->x==true, range)) function Base.getindex(spins::SpinRange, p::AbstractVector) idx = get_idx(spins.range, p) - spin_range = SpinRange(spins.range[idx] .- minimum(p) .+ 1) - return idx, spin_range + return get_idx(p, spins.range), SpinRange(idx) end function Base.view(spins::SpinRange, p::AbstractVector) idx = get_idx(spins.range, p) - spin_range = SpinRange(@view(spins.range[idx]) .- minimum(p) .+ 1) - return idx, spin_range + return get_idx(p, spins.range), SpinRange(idx) end Base.getindex(spins::SpinRange, b::BitVector) = spins[findall(x->x==true, b)] @@ -46,7 +44,7 @@ has_spins(spins::SpinRange) = length(spins.range) > 0 # Auxiliary functions function get_idx(spin_range::AbstractVector, p::AbstractVector) - idx = findall(x -> x in p, spin_range) + idx = findall(x -> x in spin_range, p) return (length(idx) > 0 && idx == collect(first(idx):last(idx))) ? (first(idx):last(idx)) : idx end diff --git a/KomaMRIBase/src/motion/nomotion/NoMotion.jl b/KomaMRIBase/src/motion/nomotion/NoMotion.jl index f6a00d1e5..c82cc0377 100644 --- a/KomaMRIBase/src/motion/nomotion/NoMotion.jl +++ b/KomaMRIBase/src/motion/nomotion/NoMotion.jl @@ -14,10 +14,8 @@ function Base.vcat(m1::NoMotion{T}, m2::AbstractMotionSet{T}, Ns1::Int, Ns2::Int mv_aux = Motion{T}[] for m in m2.motions m_aux = copy(m) - if m_aux.spins == Colon() - m_aux.spins = 1:Ns2 - end - m_aux.spins = m_aux.spins .+ Ns1 + m_aux.spins = expand(m_aux.spins, Ns2) + m_aux.spins = SpinRange(m_aux.spins.range .+ Ns1) push!(mv_aux, m_aux) end return MotionList(mv_aux) @@ -26,9 +24,7 @@ function Base.vcat(m1::AbstractMotionSet{T}, m2::NoMotion{T}, Ns1::Int, Ns2::Int mv_aux = Motion{T}[] for m in m1.motions m_aux = copy(m) - if m_aux.spins == Colon() - m_aux.spins = 1:Ns1 - end + m_aux.spins = expand(m_aux.spins, Ns1) push!(mv_aux, m_aux) end return MotionList(mv_aux) From 8db53d4f2edde6dd1084c151650cd04d3296d870 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 26 Aug 2024 14:11:17 +0200 Subject: [PATCH 40/91] Auxiliary `length` method for SpinRanges --- KomaMRIBase/src/motion/motionlist/SpinSpan.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/KomaMRIBase/src/motion/motionlist/SpinSpan.jl b/KomaMRIBase/src/motion/motionlist/SpinSpan.jl index 412dbf18c..2821b065f 100644 --- a/KomaMRIBase/src/motion/motionlist/SpinSpan.jl +++ b/KomaMRIBase/src/motion/motionlist/SpinSpan.jl @@ -39,6 +39,8 @@ Base.view(spins::SpinRange, b::BitVector) = @view(spins[findall(x->x==true, b)]) Base.:(==)(sr1::SpinRange, sr2::SpinRange) = sr1.range == sr2.range +Base.length(sr::SpinRange) = length(sr.range) + get_idx(spins::SpinRange) = spins.range has_spins(spins::SpinRange) = length(spins.range) > 0 From d7a3ed4f8f2a4a71c0a922d67314312eb8f75d4b Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Tue, 27 Aug 2024 10:44:43 +0200 Subject: [PATCH 41/91] Solve bugs for flow --- KomaMRIBase/src/motion/motionlist/TimeSpan.jl | 13 +++++++------ .../motionlist/actions/arbitraryactions/FlowPath.jl | 4 +++- KomaMRIFiles/src/Phantom/Phantom.jl | 3 +++ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/KomaMRIBase/src/motion/motionlist/TimeSpan.jl b/KomaMRIBase/src/motion/motionlist/TimeSpan.jl index e35100780..394525689 100644 --- a/KomaMRIBase/src/motion/motionlist/TimeSpan.jl +++ b/KomaMRIBase/src/motion/motionlist/TimeSpan.jl @@ -104,11 +104,12 @@ function unit_time(t::AbstractArray{T}, ts::Periodic{T}) where {T<:Real} t_rise = ts.period * ts.asymmetry t_fall = ts.period * (oneunit(T) - ts.asymmetry) t_relative = mod.(t, ts.period) - t_unit = - ifelse.( - t_relative .< t_rise, - t_relative ./ t_rise, - oneunit(T) .- (t_relative .- t_rise) ./ t_fall, - ) + if t_rise == 0 + t_unit = ifelse.(t_relative .< t_rise, zero(T), oneunit(T) .- t_relative ./ t_fall) + elseif t_fall == 0 + t_unit = ifelse.(t_relative .< t_rise, t_relative ./ t_rise, oneunit(T)) + else + t_unit = ifelse.( t_relative .< t_rise, t_relative ./ t_rise, oneunit(T) .- (t_relative .- t_rise) ./ t_fall) + end return t_unit end diff --git a/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl b/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl index 7000da0b3..cf570c465 100644 --- a/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl @@ -22,4 +22,6 @@ julia> fp = FlowPath(dx=[0.01 0.02], dy=[0.02 0.03], dz=[0.03 0.04], spin_reset= dy::AbstractArray{T} dz::AbstractArray{T} spin_reset::AbstractArray{Bool} -end \ No newline at end of file +end + +FlowPath(dx::AbstractArray{T}, dy::AbstractArray{T}, dz::AbstractArray{T}, spin_reset::Array) where T<:Real = FlowPath(dx, dy, dz, Bool.(spin_reset)) \ No newline at end of file diff --git a/KomaMRIFiles/src/Phantom/Phantom.jl b/KomaMRIFiles/src/Phantom/Phantom.jl index a2a3f38ee..d3a848061 100644 --- a/KomaMRIFiles/src/Phantom/Phantom.jl +++ b/KomaMRIFiles/src/Phantom/Phantom.jl @@ -163,4 +163,7 @@ function export_motion_subfield!(field_group::HDF5.Group, subfield::AbstractRang end function export_motion_subfield!(field_group::HDF5.Group, subfield::Array, subname::String) field_group[subname] = subfield +end +function export_motion_subfield!(field_group::HDF5.Group, subfield::BitMatrix, subname::String) + field_group[subname] = Int.(subfield) end \ No newline at end of file From afa7c83ac13b9cfd8d23d2500354a30c39f14204 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Wed, 28 Aug 2024 17:26:02 +0200 Subject: [PATCH 42/91] Update docstrings --- KomaMRIBase/src/datatypes/Phantom.jl | 29 +-- KomaMRIBase/src/motion/motionlist/Motion.jl | 166 +++++++++++++++--- .../src/motion/motionlist/MotionList.jl | 30 +++- KomaMRIBase/src/motion/motionlist/SpinSpan.jl | 32 +++- KomaMRIBase/src/motion/motionlist/TimeSpan.jl | 35 +++- .../motionlist/actions/ArbitraryAction.jl | 5 - .../motion/motionlist/actions/SimpleAction.jl | 5 - .../actions/arbitraryactions/FlowPath.jl | 18 +- .../actions/arbitraryactions/Path.jl | 36 +++- .../actions/simpleactions/HeartBeat.jl | 4 +- .../actions/simpleactions/Rotate.jl | 4 +- .../actions/simpleactions/Translate.jl | 6 +- KomaMRIBase/src/motion/nomotion/NoMotion.jl | 12 +- docs/src/reference/2-koma-base.md | 29 ++- 14 files changed, 332 insertions(+), 79 deletions(-) diff --git a/KomaMRIBase/src/datatypes/Phantom.jl b/KomaMRIBase/src/datatypes/Phantom.jl index 374a9aa8e..d6f65aebb 100644 --- a/KomaMRIBase/src/datatypes/Phantom.jl +++ b/KomaMRIBase/src/datatypes/Phantom.jl @@ -17,7 +17,7 @@ a property value representing a spin. This struct serves as an input for the sim - `Dλ1`: (`::AbstractVector{T<:Real}`) spin Dλ1 (diffusion) parameter vector - `Dλ2`: (`::AbstractVector{T<:Real}`) spin Dλ2 (diffusion) parameter vector - `Dθ`: (`::AbstractVector{T<:Real}`) spin Dθ (diffusion) parameter vector -- `motion`: (`::MotionModel{T<:Real}`) motion model +- `motion`: (`::AbstractMotionSet{T<:Real}`) motion set # Returns - `obj`: (`::Phantom`) Phantom struct @@ -125,25 +125,30 @@ function get_dims(obj::Phantom) end """ - obj = heart_phantom(...) + obj = heart_phantom( + circumferential_strain, radial_strain, rotation_angle; + heart_rate, asymmetry + ) Heart-like LV 2D phantom. The variable `circumferential_strain` and `radial_strain` are for streching (if positive) or contraction (if negative). `rotation_angle` is for rotation. -# Arguments -- `circumferential_strain`: (`::Real`, `=-0.3`) contraction parameter -- `radial_strain`: (`::Real`, `=-0.3`) contraction parameter -- `rotation_angle`: (`::Real`, `=1`) rotation parameter +# Keywords +- `circumferential_strain`: (`::Real`, `=-0.3`) contraction parameter. Between -1 and 1 +- `radial_strain`: (`::Real`, `=-0.3`) contraction parameter. Between -1 and 1 +- `rotation_angle`: (`::Real`, `=15.0`, `[º]`) maximum rotation angle +- `heart_rate`: (`::Real`, `=60`, `[bpm]`) heartbeat frequency +- `temporal_asymmetry`: (`::Real`, `=0.2`) time fraction of the period in which the systole occurs. Therefore, diastole lasts for `period * (1 - temporal_asymmetry)` # Returns -- `phantom`: (`::Phantom`) Heart-like LV phantom struct +- `obj`: (`::Phantom`) Heart-like LV phantom struct """ -function heart_phantom( +function heart_phantom(; circumferential_strain=-0.3, radial_strain=-0.3, - rotation_angle=15.0; + rotation_angle=15.0, heart_rate=60, - asymmetry=0.2, + temporal_asymmetry=0.2, ) #PARAMETERS FOV = 10e-2 # [m] Diameter ventricule @@ -186,10 +191,10 @@ function heart_phantom( circumferential_strain, radial_strain, 0.0, - Periodic(; period=period, asymmetry=asymmetry), + Periodic(; period=period, asymmetry=temporal_asymmetry), ), Rotate( - 0.0, 0.0, rotation_angle, Periodic(; period=period, asymmetry=asymmetry) + 0.0, 0.0, rotation_angle, Periodic(; period=period, asymmetry=temporal_asymmetry) ), ), ) diff --git a/KomaMRIBase/src/motion/motionlist/Motion.jl b/KomaMRIBase/src/motion/motionlist/Motion.jl index acfc075d7..4355c88dc 100644 --- a/KomaMRIBase/src/motion/motionlist/Motion.jl +++ b/KomaMRIBase/src/motion/motionlist/Motion.jl @@ -1,45 +1,159 @@ """ - m = Motion(action, time, spins) + motion = Motion(action) + motion = Motion(action, time) + motion = Motion(action, time, spins) -Motion struct. (...) +Motion struct. It defines the motion, during a certain time interval, +of a given group of spins. It is composed by three fields: `action`, which +defines the motion itself, `time`, which accounts for the time during +which the motion takes place, and `spins`, which indicates the spins +that are affected by that motion. + +# Arguments +- `action`: (`::AbstractActionSpan{T<:Real}`) action, such as [`Translate`](@ref) or [`Rotate`](@ref) +- `time`: (`::AbstractTimeSpan{T<:Real}`, `=TimeRange(0.0)`) time information about the motion +- `spins`: (`::AbstractSpinSpan`, `=AllSpins()`) spin indexes affected by the motion + +# Returns +- `motion`: (`::Motion`) Motion struct + +# Examples +```julia-repl +julia> motion = Motion( + action = Translate(0.01, 0.0, 0.02), + time = TimeRange(0.0, 1.0), + spins = SpinRange(1:10) + ) +``` """ @with_kw mutable struct Motion{T<:Real} action::AbstractActionSpan{T} - time::AbstractTimeSpan{T} - spins::AbstractSpinSpan -end - -""" Constructors """ -function Motion(action::A, time::TS, spins::AbstractSpinSpan) where {T<:Real, A<:AbstractActionSpan{T}, TS<:AbstractTimeSpan{T}} - return Motion{T}(action, time, spins) -end -function Motion(action::A, time::TS) where {T<:Real, A<:AbstractActionSpan{T}, TS<:AbstractTimeSpan{T}} - return Motion{T}(action, time, AllSpins()) -end -function Motion(action::A) where {T<:Real, A<:AbstractActionSpan{T}} - return Motion{T}(action, TimeRange(zero(T))) -end -function Motion(action::A, time::TS, range::Colon) where {T<:Real, A<:AbstractActionSpan{T}, TS<:AbstractTimeSpan{T}} - return Motion{T}(action, time, AllSpins()) -end -function Motion(action::A, time::TS, range::AbstractVector) where {T<:Real, A<:AbstractActionSpan{T}, TS<:AbstractTimeSpan{T}} - return Motion{T}(action, time, SpinRange(range)) + time ::AbstractTimeSpan{T} = TimeRange(zero(typeof(action).parameters[1])) + spins ::AbstractSpinSpan = AllSpins() end # Custom constructors -function Translate(dx, dy, dz, time=TimeRange(0.0), spins=AllSpins()) +""" + translate = Translate(dx, dy, dz, time, spins) + +# Arguments +- `dx`: (`::Real`, `[m]`) translation in x +- `dy`: (`::Real`, `[m]`) translation in y +- `dz`: (`::Real`, `[m]`) translation in z +- `time`: (`::AbstractTimeSpan{T<:Real}`) time information about the motion +- `spins`: (`::AbstractSpinSpan`) spin indexes affected by the motion + +# Returns +- `translate`: (`::Motion`) Motion struct + +# Examples +```julia-repl +julia> translate = Translate(0.01, 0.02, 0.03, TimeRange(0.0, 1.0), SpinRange(1:10)) +``` +""" +function Translate(dx, dy, dz, time=TimeRange(zero(eltype(dx))), spins=AllSpins()) return Motion(Translate(dx, dy, dz), time, spins) end -function Rotate(pitch, roll, yaw, time=TimeRange(0.0), spins=AllSpins()) + +""" + rotate = Rotate(pitch, roll, yaw, spins) + +# Arguments +- `pitch`: (`::Real`, `[º]`) rotation in x +- `roll`: (`::Real`, `[º]`) rotation in y +- `yaw`: (`::Real`, `[º]`) rotation in z +- `time`: (`::AbstractTimeSpan{T<:Real}`) time information about the motion +- `spins`: (`::AbstractSpinSpan`) spin indexes affected by the motion + +# Returns +- `rotate`: (`::Motion`) Motion struct with [`Rotate`](@ref) action + +# Examples +```julia-repl +julia> rotate = Rotate(15.0, 0.0, 20.0, TimeRange(0.0, 1.0), SpinRange(1:10)) +``` +""" +function Rotate(pitch, roll, yaw, time=TimeRange(zero(eltype(pitch))), spins=AllSpins()) return Motion(Rotate(pitch, roll, yaw), time, spins) end -function HeartBeat(circumferential_strain, radial_strain, longitudinal_strain, time=TimeRange(0.0), spins=AllSpins()) + +""" + heartbeat = HeartBeat(circumferential_strain, radial_strain, longitudinal_strainl, time, spins) + +# Arguments +- `circumferential_strain`: (`::Real`) contraction parameter +- `radial_strain`: (`::Real`) contraction parameter +- `longitudinal_strain`: (`::Real`) contraction parameter +- `time`: (`::AbstractTimeSpan{T<:Real}`) time information about the motion +- `spins`: (`::AbstractSpinSpan`) spin indexes affected by the motion + +# Returns +- `heartbeat`: (`::Motion`) Motion struct with [`HeartBeat`](@ref) action + +# Examples +```julia-repl +julia> heartbeat = HeartBeat(-0.3, -0.2, 0.0, TimeRange(0.0, 1.0), SpinRange(1:10)) +``` +""" +function HeartBeat(circumferential_strain, radial_strain, longitudinal_strain, time=TimeRange(zero(eltype(circumferential_strain))), spins=AllSpins()) return Motion(HeartBeat(circumferential_strain, radial_strain, longitudinal_strain), time, spins) end -function Path(dx, dy, dz, time=TimeRange(0.0), spins=AllSpins()) + +""" + path = Path(dx, dy, dz, time, spins) + +# Arguments +- `dx`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in x +- `dy`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in y +- `dz`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in z +- `time`: (`::AbstractTimeSpan{T<:Real}`) time information about the motion +- `spins`: (`::AbstractSpinSpan`) spin indexes affected by the motion + +# Returns +- `path`: (`::Motion`) Motion struct with [`Path`](@ref) action + +# Examples +```julia-repl +julia> path = Path( + [0.01 0.02; 0.02 0.03], + [0.02 0.03; 0.03 0.04], + [0.03 0.04; 0.04 0.05], + TimeRange(0.0, 1.0), + SpinRange(1:10) + ) +``` +""" +function Path(dx, dy, dz, time=TimeRange(zero(eltype(dx))), spins=AllSpins()) return Motion(Path(dx, dy, dz), time, spins) end -function FlowPath(dx, dy, dz, spin_reset, time=TimeRange(0.0), spins=AllSpins()) + +""" + flowpath = FlowPath(dx, dy, dz, spin_reset, time, spins) + +# Arguments +- `dx`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in x +- `dy`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in y +- `dz`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in z +- `spin_reset`: (`::AbstractArray{Bool}`) reset spin state flags +- `time`: (`::AbstractTimeSpan{T<:Real}`) time information about the motion +- `spins`: (`::AbstractSpinSpan`) spin indexes affected by the motion + +# Returns +- `flowpath`: (`::Motion`) Motion struct with [`FlowPath`](@ref) action + +# Examples +```julia-repl +julia> flowpath = FlowPath( + [0.01 0.02; 0.02 0.03], + [0.02 0.03; 0.03 0.04], + [0.03 0.04; 0.04 0.05], + [false false; false true], + TimeRange(0.0, 1.0), + SpinRange(1:10) + ) +``` +""" +function FlowPath(dx, dy, dz, spin_reset, time=TimeRange(zero(eltype(dx))), spins=AllSpins()) return Motion(FlowPath(dx, dy, dz, spin_reset), time, spins) end diff --git a/KomaMRIBase/src/motion/motionlist/MotionList.jl b/KomaMRIBase/src/motion/motionlist/MotionList.jl index d5cac0189..a552b582b 100644 --- a/KomaMRIBase/src/motion/motionlist/MotionList.jl +++ b/KomaMRIBase/src/motion/motionlist/MotionList.jl @@ -1,7 +1,31 @@ """ - m_list = MotionList(motion_array...) + motionlist = MotionList(motions...) -MotionList struct. (...) +MotionList struct. The other option, instead of `NoMotion`, +is to define a dynamic phantom by means of the `MotionList` struct. +It is composed by one or more [`Motion`](@ref) instances. + +# Arguments +- `motions`: (`::Vector{Motion{T<:Real}}`) vector of `Motion` instances + +# Returns +- `motionlist`: (`::MotionList`) MotionList struct + +# Examples +```julia-repl +julia> motionlist = MotionList( + Motion( + action = Translate(0.01, 0.0, 0.02), + time = TimeRange(0.0, 1.0), + spins = AllSpins() + ), + Motion( + action = Rotate(0.0, 0.0, 45.0), + time = Periodic(1.0), + spins = SpinRange(1:10) + ) + ) +``` """ struct MotionList{T<:Real} <: AbstractMotionSet{T} motions::Vector{<:Motion{T}} @@ -64,7 +88,7 @@ Base.length(m::MotionList) = length(m.motions) x, y, z = get_spin_coords(motion, x, y, z, t) Calculates the position of each spin at a set of arbitrary time instants, i.e. the time steps of the simulation. -For each dimension (x, y, z), the output matrix has ``N_{\text{spins}}`` rows and `length(t)` columns. +For each dimension (x, y, z), the output matrix has ``N_{\t{spins}}`` rows and `length(t)` columns. # Arguments - `motion`: (`::MotionList{T<:Real}`) phantom motion diff --git a/KomaMRIBase/src/motion/motionlist/SpinSpan.jl b/KomaMRIBase/src/motion/motionlist/SpinSpan.jl index 2821b065f..6de5888c7 100644 --- a/KomaMRIBase/src/motion/motionlist/SpinSpan.jl +++ b/KomaMRIBase/src/motion/motionlist/SpinSpan.jl @@ -1,9 +1,18 @@ abstract type AbstractSpinSpan end """ - as = AllSpin() + allspins = AllSpins() -AllSpin struct. (...) +AllSpin struct. It is a specialized type that inherits from AbstractSpinSpan +and is used to cover all the spins of a phantom. + +# Returns +- `allspins`: (`::AllSpins`) AllSpins struct + +# Examples +```julia-repl +julia> allspins = AllSpins() +``` """ struct AllSpins <: AbstractSpinSpan end @@ -13,10 +22,25 @@ Base.view(spins::AllSpins, p::AbstractVector) = p, spins get_idx(spins::AllSpins) = Colon() has_spins(spins::AllSpins) = true + """ - sr = SpinRange(range) + spinrange = SpinRange(range) + +SpinRange struct. It is a specialized type that inherits from AbstractSpinSpan +and is used to select a certain range and number of spins. + +# Arguments +- `range`: (`::AbstractVector`) spin id's. This argument can be a Range, a Vector or a BitVector + +# Returns +- `spinrange`: (`::SpinRange`) SpinRange struct -SpinRange struct. (...) +# Examples +```julia-repl +julia> spinrange = SpinRange(1:10) +julia> spinrange = SpinRange([1, 3, 5, 7]) +julia> spinrange = SpinRange(obj.x .> 0) +``` """ @with_kw struct SpinRange <: AbstractSpinSpan range::AbstractVector diff --git a/KomaMRIBase/src/motion/motionlist/TimeSpan.jl b/KomaMRIBase/src/motion/motionlist/TimeSpan.jl index 394525689..baf40806e 100644 --- a/KomaMRIBase/src/motion/motionlist/TimeSpan.jl +++ b/KomaMRIBase/src/motion/motionlist/TimeSpan.jl @@ -1,9 +1,22 @@ abstract type AbstractTimeSpan{T<:Real} end """ - tr = TimeRange(t_start, t_end) + timerange = TimeRange(t_start, t_end) -TimeRange struct. (...) +TimeRange struct. It is a specialized type that inherits from AbstractTimeSpan and +defines a time interval, with start and end times. + +# Arguments +- `t_start`: (`::Real`, `[s]`) start time +- `t_end`: (`::Real`, `[s]`) end time + +# Returns +- `timerange`: (`::TimeRange`) TimeRange struct + +# Examples +```julia-repl +julia> timerange = TimeRange(0.0, 1.0) +``` """ @with_kw struct TimeRange{T<:Real} <: AbstractTimeSpan{T} t_start ::T @@ -57,9 +70,23 @@ end """ - p = Periodic(period, asymmetry) + periodic = Periodic(period, asymmetry) -Periodic struct. (...) +Periodic struct. It is a specialized type that inherits from AbstractTimeSpan, +designed to work with time intervals that repeat periodically. It includes a measure of +asymmetry in order to recreate a asymmetric period. + +# Arguments +- `period`: (`::Real`, `[s]`) period duration +- `asymmetry`: (`::Real`, `=0.5`) temporal asymmetry factor. Between 0 and 1. + +# Returns +- `periodic`: (`::Periodic`) Periodic struct + +# Examples +```julia-repl +julia> periodic = Periodic(1.0, 0.2) +``` """ @with_kw struct Periodic{T<:Real} <: AbstractTimeSpan{T} period::T diff --git a/KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl b/KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl index adad75f84..5b67070fd 100644 --- a/KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl @@ -16,11 +16,6 @@ const Interpolator2D = Interpolations.GriddedInterpolation{ K<:Tuple{AbstractVector{T}, AbstractVector{T}}, } -""" - ArbitraryAction - -(...) -""" abstract type ArbitraryAction{T<:Real} <: AbstractActionSpan{T} end function Base.getindex(action::ArbitraryAction, p::Union{AbstractVector, Colon}) diff --git a/KomaMRIBase/src/motion/motionlist/actions/SimpleAction.jl b/KomaMRIBase/src/motion/motionlist/actions/SimpleAction.jl index b9ab0544e..d2db8bfd8 100644 --- a/KomaMRIBase/src/motion/motionlist/actions/SimpleAction.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/SimpleAction.jl @@ -1,8 +1,3 @@ -""" - SimpleAction - -(...) -""" abstract type SimpleAction{T<:Real} <: AbstractActionSpan{T} end Base.getindex(action::SimpleAction, p::Union{AbstractVector, Colon}) = action diff --git a/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl b/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl index cf570c465..b11615a94 100644 --- a/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl @@ -1,7 +1,14 @@ @doc raw""" - flowpath = FlowPath(dx, dy, dz) + flowpath = FlowPath(dx, dy, dz, spin_reset) -FlowPath motion struct. (...) +FlowPath struct. This action is the same as `Path`, +except that it includes an additional field, called `spin_reset`, +which accounts for spins leaving the volume and being remapped +to another input position. When this happens, the magnetization +state of these spins must be reset during the simulation. + +As with the `dx`, `dy` and `dz` matrices, "spin_reset" +has a size of (``N_{spins}}`` x ``N_{discrete times}``). # Arguments - `dx`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in x @@ -14,7 +21,12 @@ FlowPath motion struct. (...) # Examples ```julia-repl -julia> fp = FlowPath(dx=[0.01 0.02], dy=[0.02 0.03], dz=[0.03 0.04], spin_reset=[false, false]) +julia> flowpath = FlowPath( + dx=[0.01 0.02; 0.02 0.03], + dy=[0.02 0.03; 0.03 0.04], + dz=[0.03 0.04; 0.04 -0.04], + spin_reset=[false false; false true] + ) ``` """ @with_kw struct FlowPath{T<:Real} <: ArbitraryAction{T} diff --git a/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/Path.jl b/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/Path.jl index aa3aa0b41..ee83159be 100644 --- a/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/Path.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/Path.jl @@ -1,7 +1,35 @@ @doc raw""" path = Path(dx, dy, dz) -Path motion struct. (...) +Path struct. For this action (and for `FlowPath`), +motion is not defined solely on the basis of +three numerical parameters, one for each spatial direction, +as occurs for the `Translate`, `Rotate` and `HeartBeat` actions. + +For this action, it is necessary to define +motion for each spin independently, in x (`dx`), y (`dy`) and z (`dz`). +`dx`, `dy` and `dz` are now three matrixes, of (``N_{\t{spins}}``* x ``N_{discrete\\,times}``) each. +This means that each row corresponds to a spin trajectory over a set of discrete time instants. + +!!! note + *When creating a motion with `Flow` or `FlowPath`, you must make sure that + the number of rows of the matrices `dx`, `dy` and `dz` matches the number + of spins that are affected by the motion. + + Remember that the range of spins affected by a motion + is defined by the `spins` field of the `Motion` struct + + example: + ```julia-repl + julia> motion = Motion( + action = Path( + dx=[0.01 0.02; 0.02 0.03], # 2 rows + dy=[0.02 0.03; 0.03 0.04], + dz=[0.03 0.04; 0.04 0.05]), + time = TimeRange(0.0, 1.0), + spins = SpinRange(1:2) # 2 spins + ) + ``` # Arguments - `dx`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in x @@ -13,7 +41,11 @@ Path motion struct. (...) # Examples ```julia-repl -julia> p = Path(dx=[0.01 0.02], dy=[0.02 0.03], dz=[0.03 0.04]) +julia> path = Path( + dx=[0.01 0.02; 0.02 0.03], + dy=[0.02 0.03; 0.03 0.04], + dz=[0.03 0.04; 0.04 0.05] + ) ``` """ @with_kw struct Path{T<:Real} <: ArbitraryAction{T} diff --git a/KomaMRIBase/src/motion/motionlist/actions/simpleactions/HeartBeat.jl b/KomaMRIBase/src/motion/motionlist/actions/simpleactions/HeartBeat.jl index 0d8f3b215..45497a91d 100644 --- a/KomaMRIBase/src/motion/motionlist/actions/simpleactions/HeartBeat.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/simpleactions/HeartBeat.jl @@ -2,7 +2,7 @@ heartbeat = HeartBeat(circumferential_strain, radial_strain, longitudinal_strain) HeartBeat struct. It produces a heartbeat-like motion, characterised by three types of strain: -Circumferential, Radial and Longitudinal +circumferential, radial and longitudinal # Arguments - `circumferential_strain`: (`::Real`) contraction parameter @@ -14,7 +14,7 @@ Circumferential, Radial and Longitudinal # Examples ```julia-repl -julia> hb = HeartBeat(circumferential_strain=-0.3, radial_strain=-0.2, longitudinal_strain=0.0) +julia> heartbeat = HeartBeat(circumferential_strain=-0.3, radial_strain=-0.2, longitudinal_strain=0.0) ``` """ @with_kw struct HeartBeat{T<:Real} <: SimpleAction{T} diff --git a/KomaMRIBase/src/motion/motionlist/actions/simpleactions/Rotate.jl b/KomaMRIBase/src/motion/motionlist/actions/simpleactions/Rotate.jl index 3bdf5269e..5302a58a2 100644 --- a/KomaMRIBase/src/motion/motionlist/actions/simpleactions/Rotate.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/simpleactions/Rotate.jl @@ -1,7 +1,7 @@ @doc raw""" rotate = Rotate(pitch, roll, yaw) -Rotate motion struct. It produces a rotation of the phantom in the three axes: +Rotate struct. It produces a rotation in the three axes: x (pitch), y (roll), and z (yaw). We follow the RAS (Right-Anterior-Superior) orientation, and the rotations are applied following the right-hand rule (counter-clockwise): @@ -47,7 +47,7 @@ R &= R_z(\alpha) R_y(\beta) R_x(\gamma) \\ # Examples ```julia-repl -julia> rt = Rotate(pitch=15.0, roll=0.0, yaw=20.0) +julia> rotate = Rotate(pitch=15.0, roll=0.0, yaw=20.0) ``` """ @with_kw struct Rotate{T<:Real} <: SimpleAction{T} diff --git a/KomaMRIBase/src/motion/motionlist/actions/simpleactions/Translate.jl b/KomaMRIBase/src/motion/motionlist/actions/simpleactions/Translate.jl index fa5e2cae7..03d4fb709 100644 --- a/KomaMRIBase/src/motion/motionlist/actions/simpleactions/Translate.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/simpleactions/Translate.jl @@ -1,7 +1,7 @@ @doc raw""" - translation = Translate(dx, dy, dz) + translate = Translate(dx, dy, dz) -Translate motion struct. It produces a linear translation of the phantom. +Translate struct. It produces a linear translation. Its fields are the final displacements in the three axes (dx, dy, dz). # Arguments @@ -14,7 +14,7 @@ Its fields are the final displacements in the three axes (dx, dy, dz). # Examples ```julia-repl -julia> tr = Translate(dx=0.01, dy=0.02, dz=0.03) +julia> translate = Translate(dx=0.01, dy=0.02, dz=0.03) ``` """ @with_kw struct Translate{T<:Real} <: SimpleAction{T} diff --git a/KomaMRIBase/src/motion/nomotion/NoMotion.jl b/KomaMRIBase/src/motion/nomotion/NoMotion.jl index c82cc0377..4e624c82f 100644 --- a/KomaMRIBase/src/motion/nomotion/NoMotion.jl +++ b/KomaMRIBase/src/motion/nomotion/NoMotion.jl @@ -1,7 +1,15 @@ """ - nm = NoMotion{T<:Real}() + nomotion = NoMotion{T<:Real}() -NoMotion struct. (...) +NoMotion struct. It is used to create static phantoms. + +# Returns +- `nomotion`: (`::NoMotion`) NoMotion struct + +# Examples +```julia-repl +julia> nomotion = NoMotion{Float64}() +``` """ struct NoMotion{T<:Real} <: AbstractMotionSet{T} end diff --git a/docs/src/reference/2-koma-base.md b/docs/src/reference/2-koma-base.md index 7c9d1a7d9..816d0d8ed 100644 --- a/docs/src/reference/2-koma-base.md +++ b/docs/src/reference/2-koma-base.md @@ -20,18 +20,20 @@ pelvis_phantom2D heart_phantom ``` -### `MotionList`-related functions +## `Motion`-related functions +### `AbstractMotionSet` types and related functions ```@docs NoMotion MotionList -Motion sort_motions! get_spin_coords -TimeRange -Periodic -AllSpins -SpinRange +``` + +### `Motion` + +```@docs +Motion ``` ### `AbstractActionSpan` types @@ -44,6 +46,21 @@ Path FlowPath ``` +### `AbstractTimeSpan` types and related functions + +```@docs +TimeRange +Periodic +unit_time +``` + +### `AbstractSpinSapn` types + +```@docs +AllSpins +SpinRange +``` + ## `Sequence`-related functions ```@docs From 5d5a9a1a411a10ecfd21e31eba5a4424f6037c76 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 29 Aug 2024 10:06:25 +0200 Subject: [PATCH 43/91] Fix some docstrings --- KomaMRIBase/src/motion/motionlist/MotionList.jl | 16 ++++++++++++---- KomaMRIBase/src/motion/motionlist/SpinSpan.jl | 2 +- KomaMRIBase/src/motion/motionlist/TimeSpan.jl | 10 ++++++---- .../actions/arbitraryactions/FlowPath.jl | 4 ++-- .../motionlist/actions/arbitraryactions/Path.jl | 2 +- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/KomaMRIBase/src/motion/motionlist/MotionList.jl b/KomaMRIBase/src/motion/motionlist/MotionList.jl index a552b582b..bb1e00062 100644 --- a/KomaMRIBase/src/motion/motionlist/MotionList.jl +++ b/KomaMRIBase/src/motion/motionlist/MotionList.jl @@ -85,13 +85,13 @@ end Base.length(m::MotionList) = length(m.motions) """ - x, y, z = get_spin_coords(motion, x, y, z, t) + x, y, z = get_spin_coords(motionset, x, y, z, t) Calculates the position of each spin at a set of arbitrary time instants, i.e. the time steps of the simulation. For each dimension (x, y, z), the output matrix has ``N_{\t{spins}}`` rows and `length(t)` columns. # Arguments -- `motion`: (`::MotionList{T<:Real}`) phantom motion +- `motionset`: (`::AbstractMotionSet{T<:Real}`) phantom motion - `x`: (`::AbstractVector{T<:Real}`, `[m]`) spin x-position vector - `y`: (`::AbstractVector{T<:Real}`, `[m]`) spin y-position vector - `z`: (`::AbstractVector{T<:Real}`, `[m]`) spin z-position vector @@ -143,8 +143,16 @@ function times(ml::MotionList{T}) where {T<:Real} end """ - sort_motions!(motion_list) -sort_motions motions in a list according to their starting time + sort_motions!(motionset) +Sorts motions in a list according to their starting time. It modifies the original list. +If `motionset::NoMotion`, this function does nothing. +If `motionset::MotionList`, this function sorts its motions. + +# Arguments +- `motionset`: (`::AbstractMotionSet{T<:Real}`) phantom motion + +# Returns +- `nothing` """ function sort_motions!(mv::MotionList{T}) where {T<:Real} sort!(mv.motions; by=m -> times(m)[1]) diff --git a/KomaMRIBase/src/motion/motionlist/SpinSpan.jl b/KomaMRIBase/src/motion/motionlist/SpinSpan.jl index 6de5888c7..c4ee26d15 100644 --- a/KomaMRIBase/src/motion/motionlist/SpinSpan.jl +++ b/KomaMRIBase/src/motion/motionlist/SpinSpan.jl @@ -3,7 +3,7 @@ abstract type AbstractSpinSpan end """ allspins = AllSpins() -AllSpin struct. It is a specialized type that inherits from AbstractSpinSpan +AllSpins struct. It is a specialized type that inherits from AbstractSpinSpan and is used to cover all the spins of a phantom. # Returns diff --git a/KomaMRIBase/src/motion/motionlist/TimeSpan.jl b/KomaMRIBase/src/motion/motionlist/TimeSpan.jl index baf40806e..46f482c33 100644 --- a/KomaMRIBase/src/motion/motionlist/TimeSpan.jl +++ b/KomaMRIBase/src/motion/motionlist/TimeSpan.jl @@ -34,7 +34,7 @@ times(ts::TimeRange) = [ts.t_start, ts.t_end] t_unit = unit_time(t, time_range) The `unit_time` function normalizes a given array of time values t -to a unit interval [0, 1] based on a specified start time t_start and end time t_end. +to a unit interval [0, 1] based on a specified start time `t_start` and end time `t_end`. This function is used for non-periodic motions, where each element of t is transformed to fit within the range [0, 1] based on the provided start and end times. @@ -53,10 +53,11 @@ julia> t_unit = KomaMRIBase.unit_time([0.0, 1.0, 2.0, 3.0, 4.0, 5.0], TimeRange( 6-element Vector{Float64}: 0.0 0.0 - 0.3333333333333333 - 0.6666666666666666 + 0.333 + 0.666 1.0 1.0 +``` """ function unit_time(t::AbstractArray{T}, ts::TimeRange{T}) where {T<:Real} if ts.t_start == ts.t_end @@ -118,7 +119,7 @@ or normalizing time values in periodic processes. # Examples ```julia-repl -julia> t_unit = KomaMRIBase.unit_time_triangular([0.0, 1.0, 2.0, 3.0, 4.0, 5.0], Periodic(4.0, 0.5)) +julia> t_unit = KomaMRIBase.unit_time([0.0, 1.0, 2.0, 3.0, 4.0, 5.0], Periodic(4.0, 0.5)) 6-element Vector{Float64}: 0.0 0.5 @@ -126,6 +127,7 @@ julia> t_unit = KomaMRIBase.unit_time_triangular([0.0, 1.0, 2.0, 3.0, 4.0, 5.0], 0.5 0.0 0.5 +``` """ function unit_time(t::AbstractArray{T}, ts::Periodic{T}) where {T<:Real} t_rise = ts.period * ts.asymmetry diff --git a/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl b/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl index b11615a94..3f1238400 100644 --- a/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl @@ -7,8 +7,8 @@ which accounts for spins leaving the volume and being remapped to another input position. When this happens, the magnetization state of these spins must be reset during the simulation. -As with the `dx`, `dy` and `dz` matrices, "spin_reset" -has a size of (``N_{spins}}`` x ``N_{discrete times}``). +As with the `dx`, `dy` and `dz` matrices, `spin_reset` +has a size of (``N_{spins} \times \; N_{discrete\,times}``). # Arguments - `dx`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in x diff --git a/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/Path.jl b/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/Path.jl index ee83159be..5c3141413 100644 --- a/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/Path.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/Path.jl @@ -8,7 +8,7 @@ as occurs for the `Translate`, `Rotate` and `HeartBeat` actions. For this action, it is necessary to define motion for each spin independently, in x (`dx`), y (`dy`) and z (`dz`). -`dx`, `dy` and `dz` are now three matrixes, of (``N_{\t{spins}}``* x ``N_{discrete\\,times}``) each. +`dx`, `dy` and `dz` are now three matrixes, of (``N_{spins}* \times \; N_{discrete\,times}``) each. This means that each row corresponds to a spin trajectory over a set of discrete time instants. !!! note From 36bc578a305f62b560f402a1ce25b7af3b5b67bc Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 29 Aug 2024 10:07:27 +0200 Subject: [PATCH 44/91] Remove unnecessary docstring --- KomaMRIBase/src/motion/nomotion/NoMotion.jl | 3 --- 1 file changed, 3 deletions(-) diff --git a/KomaMRIBase/src/motion/nomotion/NoMotion.jl b/KomaMRIBase/src/motion/nomotion/NoMotion.jl index 4e624c82f..a909ed204 100644 --- a/KomaMRIBase/src/motion/nomotion/NoMotion.jl +++ b/KomaMRIBase/src/motion/nomotion/NoMotion.jl @@ -56,7 +56,4 @@ end """ times(mv::NoMotion{T}) where {T<:Real} = [zero(T)] -""" - sort_motions! -""" sort_motions!(mv::NoMotion) = nothing \ No newline at end of file From 71960f71eaf45ed65ce4e8183ffeb9ea939783ad Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 29 Aug 2024 10:20:45 +0200 Subject: [PATCH 45/91] Try to include docstring of `Translate` function --- docs/src/reference/2-koma-base.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/reference/2-koma-base.md b/docs/src/reference/2-koma-base.md index 816d0d8ed..3c1b4709c 100644 --- a/docs/src/reference/2-koma-base.md +++ b/docs/src/reference/2-koma-base.md @@ -40,6 +40,7 @@ Motion ```@docs Translate +Translate(dx, dy, dz, time, spins) Rotate HeartBeat Path From e81f617592f4f39fddf25b1c8904b645922d47cb Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 29 Aug 2024 10:41:14 +0200 Subject: [PATCH 46/91] It worked, so add the rest of the functions --- docs/src/reference/2-koma-base.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/src/reference/2-koma-base.md b/docs/src/reference/2-koma-base.md index 3c1b4709c..07bb22ae7 100644 --- a/docs/src/reference/2-koma-base.md +++ b/docs/src/reference/2-koma-base.md @@ -42,9 +42,13 @@ Motion Translate Translate(dx, dy, dz, time, spins) Rotate +Rotate(pitch, roll, yaw, time, spins) HeartBeat +HeartBeat(circumferential_strain, radial_strain, longitudinal_strain, time, spins) Path +Path(dx, dy, dz, time, spins) FlowPath +FlowPath(dx, dy, dz, spin_reset, time, spins) ``` ### `AbstractTimeSpan` types and related functions From 15c33b2c5fb0f74daa0d5739582157e2e9bec7db Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 29 Aug 2024 11:08:59 +0200 Subject: [PATCH 47/91] Not export `reset_magnetization!` --- KomaMRICore/src/KomaMRICore.jl | 2 -- docs/src/reference/3-koma-core.md | 1 - 2 files changed, 3 deletions(-) diff --git a/KomaMRICore/src/KomaMRICore.jl b/KomaMRICore/src/KomaMRICore.jl index e85097485..ca83e6fc5 100644 --- a/KomaMRICore/src/KomaMRICore.jl +++ b/KomaMRICore/src/KomaMRICore.jl @@ -29,7 +29,5 @@ export Mag export simulate, simulate_slice_profile # Spinors export Spinor, Rx, Ry, Rz, Q, Un -# Flow -export reset_magnetization! end diff --git a/docs/src/reference/3-koma-core.md b/docs/src/reference/3-koma-core.md index 5e8f64f03..5675091c7 100644 --- a/docs/src/reference/3-koma-core.md +++ b/docs/src/reference/3-koma-core.md @@ -10,7 +10,6 @@ CurrentModule = KomaMRICore simulate simulate_slice_profile default_sim_params -reset_magnetization! ``` ## GPU helper functions From c943212c8d12ac15c5f99e0cd67bb9258a38f2e9 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 29 Aug 2024 11:09:12 +0200 Subject: [PATCH 48/91] Fix typo in docstring --- .../src/motion/motionlist/actions/arbitraryactions/FlowPath.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl b/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl index 3f1238400..af502626b 100644 --- a/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl @@ -17,7 +17,7 @@ has a size of (``N_{spins} \times \; N_{discrete\,times}``). - `spin_reset`: (`::AbstractArray{Bool}`) reset spin state flags # Returns -- `flowpath: (`::FlowPath`) FlowPath struct +- `flowpath`: (`::FlowPath`) FlowPath struct # Examples ```julia-repl From 0baf29c3b5e86b264e0b7252e35ec8549a1ebcbe Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 29 Aug 2024 12:14:33 +0200 Subject: [PATCH 49/91] Try to separate tests for core and motion (see Oceananigans.jl) --- .buildkite/pipeline.yml | 11 +- .buildkite/runtests.yml | 1 - KomaMRICore/test/runtests.jl | 486 ++------------------------------ KomaMRICore/test/test_core.jl | 380 +++++++++++++++++++++++++ KomaMRICore/test/test_motion.jl | 85 ++++++ 5 files changed, 499 insertions(+), 464 deletions(-) create mode 100644 KomaMRICore/test/test_core.jl create mode 100644 KomaMRICore/test/test_motion.jl diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index c9b5336d1..d5d9f3e02 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -1,5 +1,14 @@ steps: - - label: ":pipeline: Launch Tests" + - label: ":pipeline: Core Tests" + env: + TEST_GROUP: "core" + command: buildkite-agent pipeline upload .buildkite/runtests.yml + agents: + queue: "juliagpu" + + - label: ":pipeline: Motion Tests" + env: + TEST_GROUP: "motion" command: buildkite-agent pipeline upload .buildkite/runtests.yml agents: queue: "juliagpu" diff --git a/.buildkite/runtests.yml b/.buildkite/runtests.yml index edcb133b7..96de30cf3 100644 --- a/.buildkite/runtests.yml +++ b/.buildkite/runtests.yml @@ -1,5 +1,4 @@ steps: - - group: ":julia: Tests" steps: - label: "CPU: Run tests on v{{matrix.version}}" matrix: diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index 5173d6dfa..24d5b4a81 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -33,475 +33,37 @@ using TestItems, TestItemRunner # ### -#Environment variable set by CI -const CI = get(ENV, "CI", nothing) - -@run_package_tests filter=ti->(:core in ti.tags)&&(isnothing(CI) || :skipci ∉ ti.tags) #verbose=true - -@testitem "Spinors×Mag" tags=[:core] begin - using KomaMRICore: Rx, Ry, Rz, Q, rotx, roty, rotz, Un, Rφ, Rg - - ## Verifying that operators perform counter-clockwise rotations - v = [1, 2, 3] - m = Mag([complex(v[1:2]...)], [v[3]]) - # Rx - @test rotx(π/2) * v ≈ [1, -3, 2] - @test (Rx(π/2) * m).xy ≈ [1.0 - 3.0im] - @test (Rx(π/2) * m).z ≈ [2.0] - # Ry - @test roty(π/2) * v ≈ [3, 2, -1] - @test (Ry(π/2) * m).xy ≈ [3.0 + 2.0im] - @test (Ry(π/2) * m).z ≈ [-1.0] - # Rz - @test rotz(π/2) * v ≈ [-2, 1, 3] - @test (Rz(π/2) * m).xy ≈ [-2.0 + 1.0im] - @test (Rz(π/2) * m).z ≈ [3.0] - # Rn - @test Un(π/2, [1,0,0]) * v ≈ rotx(π/2) * v - @test Un(π/2, [0,1,0]) * v ≈ roty(π/2) * v - @test Un(π/2, [0,0,1]) * v ≈ rotz(π/2) * v - @test (Q(π/2, 1.0+0.0im, 0.0) * m).xy ≈ (Rx(π/2) * m).xy - @test (Q(π/2, 1.0+0.0im, 0.0) * m).z ≈ (Rx(π/2) * m).z - @test (Q(π/2, 0.0+1.0im, 0.0) * m).xy ≈ (Ry(π/2) * m).xy - @test (Q(π/2, 0.0+1.0im, 0.0) * m).z ≈ (Ry(π/2) * m).z - @test (Q(π/2, 0.0+0.0im, 1.0) * m).xy ≈ (Rz(π/2) * m).xy - @test (Q(π/2, 0.0+0.0im, 1.0) * m).z ≈ (Rz(π/2) * m).z - - ## Verify that Spinor rotation = matrix rotation - v = rand(3) - n = rand(3); n = n ./ sqrt(sum(n.^2)) - m = Mag([complex(v[1:2]...)], [v[3]]) - φ, θ, φ1, φ2 = rand(4) * 2π - # Rx - vx = rotx(θ) * v - mx = Rx(θ) * m - @test [real(mx.xy); imag(mx.xy); mx.z] ≈ vx - # Ry - vy = roty(θ) * v - my = Ry(θ) * m - @test [real(my.xy); imag(my.xy); my.z] ≈ vy - # Rz - vz = rotz(θ) * v - mz = Rz(θ) * m - @test [real(mz.xy); imag(mz.xy); mz.z] ≈ vz - # Rφ - vφ = Un(θ, [sin(φ); cos(φ); 0.0]) * v - mφ = Rφ(φ,θ) * m - @test [real(mφ.xy); imag(mφ.xy); mφ.z] ≈ vφ - # Rg - vg = rotz(φ2) * roty(θ) * rotz(φ1) * v - mg = Rg(φ1,θ,φ2) * m - @test [real(mg.xy); imag(mg.xy); mg.z] ≈ vg - # Rn - vq = Un(θ, n) * v - mq = Q(θ, n[1]+n[2]*1im, n[3]) * m - @test [real(mq.xy); imag(mq.xy); mq.z] ≈ vq - - ## Spinors satify that |α|^2 + |β|^2 = 1 - @test abs(Rx(θ)) ≈ [1] - @test abs(Ry(θ)) ≈ [1] - @test abs(Rz(θ)) ≈ [1] - @test abs(Rφ(φ,θ)) ≈ [1] - @test abs(Q(θ, n[1]+n[2]*1im, n[3])) ≈ [1] - - ## Checking properties of Introduction to the Shinnar-Le Roux algorithm. - # Rx = Rz(-π/2) * Ry(θ) * Rz(π/2) - @test rotx(θ) * v ≈ rotz(-π/2) * roty(θ) * rotz(π/2) * v - @test (Rx(θ) * m).xy ≈ (Rz(-π/2) * Ry(θ) * Rz(π/2) * m).xy - @test (Rx(θ) * m).z ≈ (Rz(-π/2) * Ry(θ) * Rz(π/2) * m).z - # Rφ(φ,θ) = Rz(-φ) Ry(θ) Rz(φ) - @test (Rφ(φ,θ) * m).xy ≈ (Rz(-φ) * Ry(θ) * Rz(φ) * m).xy - @test (Rφ(φ,θ) * m).z ≈ (Rz(-φ) * Ry(θ) * Rz(φ) * m).z - # Rg(φ1, θ, φ2) = Rz(φ2) Ry(θ) Rz(φ1) - @test (Rg(φ1,θ,φ2) * m).xy ≈ (Rz(φ2) * Ry(θ) * Rz(φ1) * m).xy - @test (Rg(φ1,θ,φ2) * m).z ≈ (Rz(φ2) * Ry(θ) * Rz(φ1) * m).z - # Rg(-φ, θ, φ) = Rz(-φ) Ry(θ) Rz(φ) = Rφ(φ,θ) - @test rotz(-φ) * roty(θ) * rotz(φ) * v ≈ Un(θ, [sin(φ); cos(φ); 0.0]) * v - @test (Rg(φ,θ,-φ) * m).xy ≈ (Rφ(φ,θ) * m).xy - @test (Rg(φ,θ,-φ) * m).z ≈ (Rφ(φ,θ) * m).z - - ## Verify trivial identities - # Rφ is an xy-plane rotation of θ around an axis making an angle of φ with respect to the y-axis - # Rφ φ=0 = Ry - @test (Rφ(0,θ) * m).xy ≈ (Ry(θ) * m).xy - @test (Rφ(0,θ) * m).z ≈ (Ry(θ) * m).z - # Rφ φ=π/2 = Rx - @test (Rφ(π/2,θ) * m).xy ≈ (Rx(θ) * m).xy - @test (Rφ(π/2,θ) * m).z ≈ (Rx(θ) * m).z - # General rotation Rn - # Rn n=[1,0,0] = Rx - @test Un(θ, [1,0,0]) * v ≈ rotx(θ) * v - @test (Q(θ, 1.0+0.0im, 0.0) * m).xy ≈ (Rx(θ) * m).xy - @test (Q(θ, 1.0+0.0im, 0.0) * m).z ≈ (Rx(θ) * m).z - # Rn n=[0,1,0] = Ry - @test Un(θ, [0,1,0]) * v ≈ roty(θ) * v - @test (Q(θ, 0.0+1.0im, 0.0) * m).xy ≈ (Ry(θ) * m).xy - @test (Q(θ, 0.0+1.0im, 0.0) * m).z ≈ (Ry(θ) * m).z - # Rn n=[0,0,1] = Rz - @test Un(θ, [0,0,1]) * v ≈ rotz(θ) * v - @test (Q(θ, 0.0+0.0im, 1.0) * m).xy ≈ (Rz(θ) * m).xy - @test (Q(θ, 0.0+0.0im, 1.0) * m).z ≈ (Rz(θ) * m).z - - # Associativity - # Rx - @test (((Rz(-π/2) * Ry(θ)) * Rz(π/2)) * m).xy ≈ (Rx(θ) * m).xy - @test (((Rz(-π/2) * Ry(θ)) * Rz(π/2)) * m).z ≈ (Rx(θ) * m).z - @test (Rz(-π/2) * (Ry(θ) * (Rz(π/2) * m))).xy ≈ (Rx(θ) * m).xy - @test (Rz(-π/2) * (Ry(θ) * (Rz(π/2) * m))).z ≈ (Rx(θ) * m).z - # Rφ - @test (Rφ(φ,θ) * m).xy ≈ (((Rz(-φ) * Ry(θ)) * Rz(φ)) * m).xy - @test (Rφ(φ,θ) * m).z ≈ (((Rz(-φ) * Ry(θ)) * Rz(φ)) * m).z - @test (Rφ(φ,θ) * m).xy ≈ ((Rz(-φ) * (Ry(θ) * Rz(φ))) * m).xy - @test (Rφ(φ,θ) * m).z ≈ ((Rz(-φ) * (Ry(θ) * Rz(φ))) * m).z - # Rg - @test (Rg(φ1,θ,φ2) * m).xy ≈ (((Rz(φ2) * Ry(θ)) * Rz(φ1)) * m).xy - @test (Rg(φ1,θ,φ2) * m).z ≈ (((Rz(φ2) * Ry(θ)) * Rz(φ1)) * m).z - @test (Rg(φ1,θ,φ2) * m).xy ≈ ((Rz(φ2) * (Ry(θ) * Rz(φ1))) * m).xy - @test (Rg(φ1,θ,φ2) * m).z ≈ ((Rz(φ2) * (Ry(θ) * Rz(φ1))) * m).z - - ## Other tests - # Test Spinor struct - α, β = rand(2) - s = Spinor(α, β) - @test s[1].α ≈ [Complex(α)] && s[1].β ≈ [Complex(β)] - # Just checking to ensure that show() doesn't get stuck and that it is covered - show(IOBuffer(), "text/plain", s) - @test true -end - -@testitem "ISMRMRD" tags=[:core] begin - using Suppressor - include("initialize_backend.jl") - - seq = PulseDesigner.EPI_example() - sys = Scanner() - obj = brain_phantom2D() - parts = kfoldperm(length(obj), 2) - - sim_params = KomaMRICore.default_sim_params() - sim_params["return_type"] = "raw" - sim_params["gpu"] = USE_GPU - - sig1 = @suppress simulate(obj[parts[1]], seq, sys; sim_params) - sig2 = @suppress simulate(obj[parts[2]], seq, sys; sim_params) - sig = @suppress simulate(obj, seq, sys; sim_params) - - @test isapprox(sig, sig1 + sig2; rtol=0.001) -end - -@testitem "signal_to_raw_data" tags=[:core] begin - using Suppressor - include("initialize_backend.jl") - - seq = PulseDesigner.EPI_example() - sys = Scanner() - obj = brain_phantom2D() +group = get(ENV, "TEST_GROUP", :all) |> Symbol +test_file = get(ENV, "TEST_FILE", :none) |> Symbol - sim_params = KomaMRICore.default_sim_params() - sim_params["return_type"] = "mat" - sim_params["gpu"] = USE_GPU - sig = @suppress simulate(obj, seq, sys; sim_params) - - # Test signal_to_raw_data - raw = signal_to_raw_data(sig, seq) - sig_aux = vcat([vec(profile.data) for profile in raw.profiles]...) - sig_raw = reshape(sig_aux, length(sig_aux), 1) - @test all(sig .== sig_raw) - - seq.DEF["FOV"] = [23e-2, 23e-2, 0] - raw = signal_to_raw_data(sig, seq) - sig_aux = vcat([vec(profile.data) for profile in raw.profiles]...) - sig_raw = reshape(sig_aux, length(sig_aux), 1) - @test all(sig .== sig_raw) - - # Just checking to ensure that show() doesn't get stuck and that it is covered - show(IOBuffer(), "text/plain", raw) - @test true +# if we are testing just a single file then group = :none +# to skip the full test suite +if test_file != :none + group = :none end -@testitem "Bloch" tags=[:important, :core] begin - using Suppressor - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - sig_jemris = signal_sphere_jemris() - seq = seq_epi_100x100_TE100_FOV230() - obj = phantom_sphere() - sys = Scanner() - - sim_params = Dict{String, Any}( - "gpu"=>USE_GPU, - "sim_method"=>KomaMRICore.Bloch(), - "return_type"=>"mat" - ) - sig = @suppress simulate(obj, seq, sys; sim_params) - sig = sig / prod(size(obj)) - - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - - @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% -end - -@testitem "Bloch_RF_accuracy" tags=[:important, :core] begin - using Suppressor - include("initialize_backend.jl") - - Tadc = 1e-3 - Trf = Tadc - T1 = 1000e-3 - T2 = 20e-3 - Δw = 2π * 100 - B1 = 2e-6 * (Tadc / Trf) - N = 6 - - sys = Scanner() - obj = Phantom{Float64}(x=[0.],T1=[T1],T2=[T2],Δw=[Δw]) - - rf_phase = [0, π/2] - seq = Sequence() - seq += ADC(N, Tadc) - for i=1:2 - global seq += RF(B1 .* exp(1im*rf_phase[i]), Trf) - global seq += ADC(N, Tadc) +@testset "KomaMRICore" begin + if test_file != :none + @testset "Single file test" begin + include(String(test_file)) + end end - - sim_params = Dict{String, Any}("Δt_rf"=>1e-5, "gpu"=>USE_GPU) - raw = @suppress simulate(obj, seq, sys; sim_params) - - #Mathematica-simulated Bloch equation result - res1 = [0.153592+0.46505im, - 0.208571+0.437734im, - 0.259184+0.40408im, - 0.304722+0.364744im, - 0.344571+0.320455im, - 0.378217+0.272008im] - res2 = [-0.0153894+0.142582im, - 0.00257641+0.14196im, - 0.020146+0.13912im, - 0.037051+0.134149im, - 0.0530392+0.12717im, - 0.0678774+0.11833im] - norm2(x) = sqrt.(sum(abs.(x).^2)) - error0 = norm2(raw.profiles[1].data .- 0) - error1 = norm2(raw.profiles[2].data .- res1) ./ norm2(res1) * 100 - error2 = norm2(raw.profiles[3].data .- res2) ./ norm2(res2) * 100 - - @test error0 + error1 + error2 < 0.1 #NMRSE < 0.1% -end - -@testitem "Bloch_phase_compensation" tags=[:important, :core] begin - using Suppressor - include("initialize_backend.jl") - - Tadc = 1e-3 - Trf = Tadc - T1 = 1000e-3 - T2 = 20e-3 - Δw = 2π * 100 - B1 = 2e-6 * (Tadc / Trf) - N = 6 - - sys = Scanner() - obj = Phantom{Float64}(x=[0.],T1=[T1],T2=[T2],Δw=[Δw]) - - rf_phase = 2π*rand() - seq1 = Sequence() - seq1 += RF(B1, Trf) - seq1 += ADC(N, Tadc) - - seq2 = Sequence() - seq2 += RF(B1 .* exp(1im*rf_phase), Trf) - seq2 += ADC(N, Tadc, 0, 0, rf_phase) - - sim_params = Dict{String, Any}("Δt_rf"=>1e-5, "gpu"=>USE_GPU) - raw1 = @suppress simulate(obj, seq1, sys; sim_params) - raw2 = @suppress simulate(obj, seq2, sys; sim_params) - - @test raw1.profiles[1].data ≈ raw2.profiles[1].data -end - -@testitem "Bloch SimpleAction" tags=[:important, :core, :motion] begin - using Suppressor - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - sig_jemris = signal_brain_motion_jemris() - seq = seq_epi_100x100_TE100_FOV230() - sys = Scanner() - obj = phantom_brain_simple_motion() - sim_params = Dict{String, Any}( - "gpu"=>USE_GPU, - "sim_method"=>KomaMRICore.Bloch(), - "return_type"=>"mat" - ) - sig = @suppress simulate(obj, seq, sys; sim_params) - sig = sig / prod(size(obj)) - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - println("NMRSE SimpleAction: ", NMRSE(sig, sig_jemris)) - @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% -end - -@testitem "Bloch ArbitraryAction" tags=[:important, :core, :motion] begin - using Suppressor - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - sig_jemris = signal_brain_motion_jemris() - seq = seq_epi_100x100_TE100_FOV230() - sys = Scanner() - obj = phantom_brain_arbitrary_motion() - sim_params = Dict{String, Any}( - "gpu"=>USE_GPU, - "sim_method"=>KomaMRICore.Bloch(), - "return_type"=>"mat" - ) - sig = @suppress simulate(obj, seq, sys; sim_params) - sig = sig / prod(size(obj)) - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - println("NMRSE ArbitraryAction: ", NMRSE(sig, sig_jemris)) - @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% -end - -@testitem "BlochDict" tags=[:important, :core] begin - using Suppressor - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - seq = seq_epi_100x100_TE100_FOV230() - obj = Phantom{Float64}(x=[0.], T1=[1000e-3], T2=[100e-3]) - sys = Scanner() - sim_params = Dict( - "gpu"=>USE_GPU, - "sim_method"=>KomaMRICore.Bloch(), - "return_type"=>"mat") - sig = @suppress simulate(obj, seq, sys; sim_params) - sig = sig / prod(size(obj)) - sim_params["sim_method"] = KomaMRICore.BlochDict() - sig2 = @suppress simulate(obj, seq, sys; sim_params) - sig2 = sig2 / prod(size(obj)) - @test sig ≈ sig2 - - # Just checking to ensure that show() doesn't get stuck and that it is covered - show(IOBuffer(), "text/plain", KomaMRICore.BlochDict()) - @test true -end - -@testitem "BlochSimple" tags=[:important, :core] begin - using Suppressor - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - sig_jemris = signal_sphere_jemris() - seq = seq_epi_100x100_TE100_FOV230() - obj = phantom_sphere() - sys = Scanner() - - sim_params = Dict{String, Any}( - "gpu"=>USE_GPU, - "sim_method"=>KomaMRICore.BlochSimple(), - "return_type"=>"mat" - ) - sig = @suppress simulate(obj, seq, sys; sim_params) - sig = sig / prod(size(obj)) - - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% -end - -@testitem "BlochSimple SimpleAction" tags=[:important, :core, :motion] begin - using Suppressor - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - sig_jemris = signal_brain_motion_jemris() - seq = seq_epi_100x100_TE100_FOV230() - sys = Scanner() - obj = phantom_brain_simple_motion() - - sim_params = Dict{String, Any}( - "gpu"=>USE_GPU, - "sim_method"=>KomaMRICore.BlochSimple(), - "return_type"=>"mat" - ) - sig = @suppress simulate(obj, seq, sys; sim_params) - sig = sig / prod(size(obj)) - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - println("NMRSE SimpleAction BlochSimple: ", NMRSE(sig, sig_jemris)) - @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% -end - -@testitem "BlochSimple ArbitraryAction" tags=[:important, :core, :motion] begin - using Suppressor - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - sig_jemris = signal_brain_motion_jemris() - seq = seq_epi_100x100_TE100_FOV230() - sys = Scanner() - obj = phantom_brain_arbitrary_motion() - - sim_params = Dict{String, Any}( - "gpu"=>USE_GPU, - "sim_method"=>KomaMRICore.BlochSimple(), - "return_type"=>"mat" - ) - sig = @suppress simulate(obj, seq, sys; sim_params) - sig = sig / prod(size(obj)) - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - println("NMRSE ArbitraryAction BlochSimple: ", NMRSE(sig, sig_jemris)) - @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% -end - - -@testitem "simulate_slice_profile" tags=[:core] begin - using Suppressor - include("initialize_backend.jl") - - # This is a sequence with a sinc RF 30° excitation pulse - sys = Scanner() - sys.Smax = 50 - B1 = 4.92e-6 - Trf = 3.2e-3 - zmax = 2e-2 - fmax = 5e3 - z = range(-zmax, zmax, 400) - Gz = fmax / (γ * zmax) - f = γ * Gz * z - seq = PulseDesigner.RF_sinc(B1, Trf, sys; G=[0; 0; Gz], TBP=8) - - # Simulate the slice profile - sim_params = Dict{String, Any}( - "Δt_rf" => Trf / length(seq.RF.A[1]), - "gpu" => USE_GPU) - M = @suppress simulate_slice_profile(seq; z, sim_params) + if group == :core || group == :all + @testset "Core" begin + include("test_core.jl") + end + end - # For the time being, always pass the test - @test true + if group == :motion || group == :all + @testset "Motion" begin + include("test_motion.jl") + end + end end -@testitem "GPU Functions" tags=[:core] begin - using Suppressor - import KernelAbstractions as KA - include("initialize_backend.jl") - - x = ones(Float32, 1000) +#Environment variable set by CI +const CI = get(ENV, "CI", nothing) - @suppress begin - if USE_GPU - y = x |> gpu - @test KA.get_backend(y) isa KA.GPU - y = y |> cpu - @test KA.get_backend(y) isa KA.CPU - else - # Test that gpu and cpu are no-ops - y = x |> gpu - @test y == x - y = y |> cpu - @test y == x - end - end +@run_package_tests filter=ti->(:core in ti.tags)&&(isnothing(CI) || :skipci ∉ ti.tags) #verbose=true - @suppress print_devices() - @test true -end diff --git a/KomaMRICore/test/test_core.jl b/KomaMRICore/test/test_core.jl new file mode 100644 index 000000000..4016cc34a --- /dev/null +++ b/KomaMRICore/test/test_core.jl @@ -0,0 +1,380 @@ +@testitem "Spinors×Mag" begin + using KomaMRICore: Rx, Ry, Rz, Q, rotx, roty, rotz, Un, Rφ, Rg + + ## Verifying that operators perform counter-clockwise rotations + v = [1, 2, 3] + m = Mag([complex(v[1:2]...)], [v[3]]) + # Rx + @test rotx(π/2) * v ≈ [1, -3, 2] + @test (Rx(π/2) * m).xy ≈ [1.0 - 3.0im] + @test (Rx(π/2) * m).z ≈ [2.0] + # Ry + @test roty(π/2) * v ≈ [3, 2, -1] + @test (Ry(π/2) * m).xy ≈ [3.0 + 2.0im] + @test (Ry(π/2) * m).z ≈ [-1.0] + # Rz + @test rotz(π/2) * v ≈ [-2, 1, 3] + @test (Rz(π/2) * m).xy ≈ [-2.0 + 1.0im] + @test (Rz(π/2) * m).z ≈ [3.0] + # Rn + @test Un(π/2, [1,0,0]) * v ≈ rotx(π/2) * v + @test Un(π/2, [0,1,0]) * v ≈ roty(π/2) * v + @test Un(π/2, [0,0,1]) * v ≈ rotz(π/2) * v + @test (Q(π/2, 1.0+0.0im, 0.0) * m).xy ≈ (Rx(π/2) * m).xy + @test (Q(π/2, 1.0+0.0im, 0.0) * m).z ≈ (Rx(π/2) * m).z + @test (Q(π/2, 0.0+1.0im, 0.0) * m).xy ≈ (Ry(π/2) * m).xy + @test (Q(π/2, 0.0+1.0im, 0.0) * m).z ≈ (Ry(π/2) * m).z + @test (Q(π/2, 0.0+0.0im, 1.0) * m).xy ≈ (Rz(π/2) * m).xy + @test (Q(π/2, 0.0+0.0im, 1.0) * m).z ≈ (Rz(π/2) * m).z + + ## Verify that Spinor rotation = matrix rotation + v = rand(3) + n = rand(3); n = n ./ sqrt(sum(n.^2)) + m = Mag([complex(v[1:2]...)], [v[3]]) + φ, θ, φ1, φ2 = rand(4) * 2π + # Rx + vx = rotx(θ) * v + mx = Rx(θ) * m + @test [real(mx.xy); imag(mx.xy); mx.z] ≈ vx + # Ry + vy = roty(θ) * v + my = Ry(θ) * m + @test [real(my.xy); imag(my.xy); my.z] ≈ vy + # Rz + vz = rotz(θ) * v + mz = Rz(θ) * m + @test [real(mz.xy); imag(mz.xy); mz.z] ≈ vz + # Rφ + vφ = Un(θ, [sin(φ); cos(φ); 0.0]) * v + mφ = Rφ(φ,θ) * m + @test [real(mφ.xy); imag(mφ.xy); mφ.z] ≈ vφ + # Rg + vg = rotz(φ2) * roty(θ) * rotz(φ1) * v + mg = Rg(φ1,θ,φ2) * m + @test [real(mg.xy); imag(mg.xy); mg.z] ≈ vg + # Rn + vq = Un(θ, n) * v + mq = Q(θ, n[1]+n[2]*1im, n[3]) * m + @test [real(mq.xy); imag(mq.xy); mq.z] ≈ vq + + ## Spinors satify that |α|^2 + |β|^2 = 1 + @test abs(Rx(θ)) ≈ [1] + @test abs(Ry(θ)) ≈ [1] + @test abs(Rz(θ)) ≈ [1] + @test abs(Rφ(φ,θ)) ≈ [1] + @test abs(Q(θ, n[1]+n[2]*1im, n[3])) ≈ [1] + + ## Checking properties of Introduction to the Shinnar-Le Roux algorithm. + # Rx = Rz(-π/2) * Ry(θ) * Rz(π/2) + @test rotx(θ) * v ≈ rotz(-π/2) * roty(θ) * rotz(π/2) * v + @test (Rx(θ) * m).xy ≈ (Rz(-π/2) * Ry(θ) * Rz(π/2) * m).xy + @test (Rx(θ) * m).z ≈ (Rz(-π/2) * Ry(θ) * Rz(π/2) * m).z + # Rφ(φ,θ) = Rz(-φ) Ry(θ) Rz(φ) + @test (Rφ(φ,θ) * m).xy ≈ (Rz(-φ) * Ry(θ) * Rz(φ) * m).xy + @test (Rφ(φ,θ) * m).z ≈ (Rz(-φ) * Ry(θ) * Rz(φ) * m).z + # Rg(φ1, θ, φ2) = Rz(φ2) Ry(θ) Rz(φ1) + @test (Rg(φ1,θ,φ2) * m).xy ≈ (Rz(φ2) * Ry(θ) * Rz(φ1) * m).xy + @test (Rg(φ1,θ,φ2) * m).z ≈ (Rz(φ2) * Ry(θ) * Rz(φ1) * m).z + # Rg(-φ, θ, φ) = Rz(-φ) Ry(θ) Rz(φ) = Rφ(φ,θ) + @test rotz(-φ) * roty(θ) * rotz(φ) * v ≈ Un(θ, [sin(φ); cos(φ); 0.0]) * v + @test (Rg(φ,θ,-φ) * m).xy ≈ (Rφ(φ,θ) * m).xy + @test (Rg(φ,θ,-φ) * m).z ≈ (Rφ(φ,θ) * m).z + + ## Verify trivial identities + # Rφ is an xy-plane rotation of θ around an axis making an angle of φ with respect to the y-axis + # Rφ φ=0 = Ry + @test (Rφ(0,θ) * m).xy ≈ (Ry(θ) * m).xy + @test (Rφ(0,θ) * m).z ≈ (Ry(θ) * m).z + # Rφ φ=π/2 = Rx + @test (Rφ(π/2,θ) * m).xy ≈ (Rx(θ) * m).xy + @test (Rφ(π/2,θ) * m).z ≈ (Rx(θ) * m).z + # General rotation Rn + # Rn n=[1,0,0] = Rx + @test Un(θ, [1,0,0]) * v ≈ rotx(θ) * v + @test (Q(θ, 1.0+0.0im, 0.0) * m).xy ≈ (Rx(θ) * m).xy + @test (Q(θ, 1.0+0.0im, 0.0) * m).z ≈ (Rx(θ) * m).z + # Rn n=[0,1,0] = Ry + @test Un(θ, [0,1,0]) * v ≈ roty(θ) * v + @test (Q(θ, 0.0+1.0im, 0.0) * m).xy ≈ (Ry(θ) * m).xy + @test (Q(θ, 0.0+1.0im, 0.0) * m).z ≈ (Ry(θ) * m).z + # Rn n=[0,0,1] = Rz + @test Un(θ, [0,0,1]) * v ≈ rotz(θ) * v + @test (Q(θ, 0.0+0.0im, 1.0) * m).xy ≈ (Rz(θ) * m).xy + @test (Q(θ, 0.0+0.0im, 1.0) * m).z ≈ (Rz(θ) * m).z + + # Associativity + # Rx + @test (((Rz(-π/2) * Ry(θ)) * Rz(π/2)) * m).xy ≈ (Rx(θ) * m).xy + @test (((Rz(-π/2) * Ry(θ)) * Rz(π/2)) * m).z ≈ (Rx(θ) * m).z + @test (Rz(-π/2) * (Ry(θ) * (Rz(π/2) * m))).xy ≈ (Rx(θ) * m).xy + @test (Rz(-π/2) * (Ry(θ) * (Rz(π/2) * m))).z ≈ (Rx(θ) * m).z + # Rφ + @test (Rφ(φ,θ) * m).xy ≈ (((Rz(-φ) * Ry(θ)) * Rz(φ)) * m).xy + @test (Rφ(φ,θ) * m).z ≈ (((Rz(-φ) * Ry(θ)) * Rz(φ)) * m).z + @test (Rφ(φ,θ) * m).xy ≈ ((Rz(-φ) * (Ry(θ) * Rz(φ))) * m).xy + @test (Rφ(φ,θ) * m).z ≈ ((Rz(-φ) * (Ry(θ) * Rz(φ))) * m).z + # Rg + @test (Rg(φ1,θ,φ2) * m).xy ≈ (((Rz(φ2) * Ry(θ)) * Rz(φ1)) * m).xy + @test (Rg(φ1,θ,φ2) * m).z ≈ (((Rz(φ2) * Ry(θ)) * Rz(φ1)) * m).z + @test (Rg(φ1,θ,φ2) * m).xy ≈ ((Rz(φ2) * (Ry(θ) * Rz(φ1))) * m).xy + @test (Rg(φ1,θ,φ2) * m).z ≈ ((Rz(φ2) * (Ry(θ) * Rz(φ1))) * m).z + + ## Other tests + # Test Spinor struct + α, β = rand(2) + s = Spinor(α, β) + @test s[1].α ≈ [Complex(α)] && s[1].β ≈ [Complex(β)] + # Just checking to ensure that show() doesn't get stuck and that it is covered + show(IOBuffer(), "text/plain", s) + @test true +end + +@testitem "ISMRMRD" begin + using Suppressor + include("initialize_backend.jl") + + seq = PulseDesigner.EPI_example() + sys = Scanner() + obj = brain_phantom2D() + parts = kfoldperm(length(obj), 2) + + sim_params = KomaMRICore.default_sim_params() + sim_params["return_type"] = "raw" + sim_params["gpu"] = USE_GPU + + sig1 = @suppress simulate(obj[parts[1]], seq, sys; sim_params) + sig2 = @suppress simulate(obj[parts[2]], seq, sys; sim_params) + sig = @suppress simulate(obj, seq, sys; sim_params) + + @test isapprox(sig, sig1 + sig2; rtol=0.001) +end + +@testitem "signal_to_raw_data" begin + using Suppressor + include("initialize_backend.jl") + + seq = PulseDesigner.EPI_example() + sys = Scanner() + obj = brain_phantom2D() + + sim_params = KomaMRICore.default_sim_params() + sim_params["return_type"] = "mat" + sim_params["gpu"] = USE_GPU + sig = @suppress simulate(obj, seq, sys; sim_params) + + # Test signal_to_raw_data + raw = signal_to_raw_data(sig, seq) + sig_aux = vcat([vec(profile.data) for profile in raw.profiles]...) + sig_raw = reshape(sig_aux, length(sig_aux), 1) + @test all(sig .== sig_raw) + + seq.DEF["FOV"] = [23e-2, 23e-2, 0] + raw = signal_to_raw_data(sig, seq) + sig_aux = vcat([vec(profile.data) for profile in raw.profiles]...) + sig_raw = reshape(sig_aux, length(sig_aux), 1) + @test all(sig .== sig_raw) + + # Just checking to ensure that show() doesn't get stuck and that it is covered + show(IOBuffer(), "text/plain", raw) + @test true +end + +@testitem "Bloch" begin + using Suppressor + include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) + + sig_jemris = signal_sphere_jemris() + seq = seq_epi_100x100_TE100_FOV230() + obj = phantom_sphere() + sys = Scanner() + + sim_params = Dict{String, Any}( + "gpu"=>USE_GPU, + "sim_method"=>KomaMRICore.Bloch(), + "return_type"=>"mat" + ) + sig = @suppress simulate(obj, seq, sys; sim_params) + sig = sig / prod(size(obj)) + + NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + + @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% +end + +@testitem "Bloch_RF_accuracy" begin + using Suppressor + include("initialize_backend.jl") + + Tadc = 1e-3 + Trf = Tadc + T1 = 1000e-3 + T2 = 20e-3 + Δw = 2π * 100 + B1 = 2e-6 * (Tadc / Trf) + N = 6 + + sys = Scanner() + obj = Phantom{Float64}(x=[0.],T1=[T1],T2=[T2],Δw=[Δw]) + + rf_phase = [0, π/2] + seq = Sequence() + seq += ADC(N, Tadc) + for i=1:2 + global seq += RF(B1 .* exp(1im*rf_phase[i]), Trf) + global seq += ADC(N, Tadc) + end + + sim_params = Dict{String, Any}("Δt_rf"=>1e-5, "gpu"=>USE_GPU) + raw = @suppress simulate(obj, seq, sys; sim_params) + + #Mathematica-simulated Bloch equation result + res1 = [0.153592+0.46505im, + 0.208571+0.437734im, + 0.259184+0.40408im, + 0.304722+0.364744im, + 0.344571+0.320455im, + 0.378217+0.272008im] + res2 = [-0.0153894+0.142582im, + 0.00257641+0.14196im, + 0.020146+0.13912im, + 0.037051+0.134149im, + 0.0530392+0.12717im, + 0.0678774+0.11833im] + norm2(x) = sqrt.(sum(abs.(x).^2)) + error0 = norm2(raw.profiles[1].data .- 0) + error1 = norm2(raw.profiles[2].data .- res1) ./ norm2(res1) * 100 + error2 = norm2(raw.profiles[3].data .- res2) ./ norm2(res2) * 100 + + @test error0 + error1 + error2 < 0.1 #NMRSE < 0.1% +end + +@testitem "Bloch_phase_compensation" begin + using Suppressor + include("initialize_backend.jl") + + Tadc = 1e-3 + Trf = Tadc + T1 = 1000e-3 + T2 = 20e-3 + Δw = 2π * 100 + B1 = 2e-6 * (Tadc / Trf) + N = 6 + + sys = Scanner() + obj = Phantom{Float64}(x=[0.],T1=[T1],T2=[T2],Δw=[Δw]) + + rf_phase = 2π*rand() + seq1 = Sequence() + seq1 += RF(B1, Trf) + seq1 += ADC(N, Tadc) + + seq2 = Sequence() + seq2 += RF(B1 .* exp(1im*rf_phase), Trf) + seq2 += ADC(N, Tadc, 0, 0, rf_phase) + + sim_params = Dict{String, Any}("Δt_rf"=>1e-5, "gpu"=>USE_GPU) + raw1 = @suppress simulate(obj, seq1, sys; sim_params) + raw2 = @suppress simulate(obj, seq2, sys; sim_params) + + @test raw1.profiles[1].data ≈ raw2.profiles[1].data +end + +@testitem "BlochDict" begin + using Suppressor + include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) + + seq = seq_epi_100x100_TE100_FOV230() + obj = Phantom{Float64}(x=[0.], T1=[1000e-3], T2=[100e-3]) + sys = Scanner() + sim_params = Dict( + "gpu"=>USE_GPU, + "sim_method"=>KomaMRICore.Bloch(), + "return_type"=>"mat") + sig = @suppress simulate(obj, seq, sys; sim_params) + sig = sig / prod(size(obj)) + sim_params["sim_method"] = KomaMRICore.BlochDict() + sig2 = @suppress simulate(obj, seq, sys; sim_params) + sig2 = sig2 / prod(size(obj)) + @test sig ≈ sig2 + + # Just checking to ensure that show() doesn't get stuck and that it is covered + show(IOBuffer(), "text/plain", KomaMRICore.BlochDict()) + @test true +end + +@testitem "BlochSimple" begin + using Suppressor + include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) + + sig_jemris = signal_sphere_jemris() + seq = seq_epi_100x100_TE100_FOV230() + obj = phantom_sphere() + sys = Scanner() + + sim_params = Dict{String, Any}( + "gpu"=>USE_GPU, + "sim_method"=>KomaMRICore.BlochSimple(), + "return_type"=>"mat" + ) + sig = @suppress simulate(obj, seq, sys; sim_params) + sig = sig / prod(size(obj)) + + NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + + @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% +end + +@testitem "simulate_slice_profile" begin + using Suppressor + include("initialize_backend.jl") + + # This is a sequence with a sinc RF 30° excitation pulse + sys = Scanner() + sys.Smax = 50 + B1 = 4.92e-6 + Trf = 3.2e-3 + zmax = 2e-2 + fmax = 5e3 + z = range(-zmax, zmax, 400) + Gz = fmax / (γ * zmax) + f = γ * Gz * z + seq = PulseDesigner.RF_sinc(B1, Trf, sys; G=[0; 0; Gz], TBP=8) + + # Simulate the slice profile + sim_params = Dict{String, Any}( + "Δt_rf" => Trf / length(seq.RF.A[1]), + "gpu" => USE_GPU) + M = @suppress simulate_slice_profile(seq; z, sim_params) + + # For the time being, always pass the test + @test true +end + +@testitem "GPU Functions" begin + using Suppressor + import KernelAbstractions as KA + include("initialize_backend.jl") + + x = ones(Float32, 1000) + + @suppress begin + if USE_GPU + y = x |> gpu + @test KA.get_backend(y) isa KA.GPU + y = y |> cpu + @test KA.get_backend(y) isa KA.CPU + else + # Test that gpu and cpu are no-ops + y = x |> gpu + @test y == x + y = y |> cpu + @test y == x + end + end + + @suppress print_devices() + @test true +end diff --git a/KomaMRICore/test/test_motion.jl b/KomaMRICore/test/test_motion.jl new file mode 100644 index 000000000..3a856b6d1 --- /dev/null +++ b/KomaMRICore/test/test_motion.jl @@ -0,0 +1,85 @@ +@testitem "Bloch SimpleAction" begin + using Suppressor + include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) + + sig_jemris = signal_brain_motion_jemris() + seq = seq_epi_100x100_TE100_FOV230() + sys = Scanner() + obj = phantom_brain_simple_motion() + sim_params = Dict{String, Any}( + "gpu"=>USE_GPU, + "sim_method"=>KomaMRICore.Bloch(), + "return_type"=>"mat" + ) + sig = @suppress simulate(obj, seq, sys; sim_params) + sig = sig / prod(size(obj)) + NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + println("NMRSE SimpleAction: ", NMRSE(sig, sig_jemris)) + @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% +end + +@testitem "Bloch ArbitraryAction" begin + using Suppressor + include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) + + sig_jemris = signal_brain_motion_jemris() + seq = seq_epi_100x100_TE100_FOV230() + sys = Scanner() + obj = phantom_brain_arbitrary_motion() + sim_params = Dict{String, Any}( + "gpu"=>USE_GPU, + "sim_method"=>KomaMRICore.Bloch(), + "return_type"=>"mat" + ) + sig = @suppress simulate(obj, seq, sys; sim_params) + sig = sig / prod(size(obj)) + NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + println("NMRSE ArbitraryAction: ", NMRSE(sig, sig_jemris)) + @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% +end + +@testitem "BlochSimple SimpleAction" begin + using Suppressor + include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) + + sig_jemris = signal_brain_motion_jemris() + seq = seq_epi_100x100_TE100_FOV230() + sys = Scanner() + obj = phantom_brain_simple_motion() + + sim_params = Dict{String, Any}( + "gpu"=>USE_GPU, + "sim_method"=>KomaMRICore.BlochSimple(), + "return_type"=>"mat" + ) + sig = @suppress simulate(obj, seq, sys; sim_params) + sig = sig / prod(size(obj)) + NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + println("NMRSE SimpleAction BlochSimple: ", NMRSE(sig, sig_jemris)) + @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% +end + +@testitem "BlochSimple ArbitraryAction" begin + using Suppressor + include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) + + sig_jemris = signal_brain_motion_jemris() + seq = seq_epi_100x100_TE100_FOV230() + sys = Scanner() + obj = phantom_brain_arbitrary_motion() + + sim_params = Dict{String, Any}( + "gpu"=>USE_GPU, + "sim_method"=>KomaMRICore.BlochSimple(), + "return_type"=>"mat" + ) + sig = @suppress simulate(obj, seq, sys; sim_params) + sig = sig / prod(size(obj)) + NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + println("NMRSE ArbitraryAction BlochSimple: ", NMRSE(sig, sig_jemris)) + @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% +end \ No newline at end of file From e29418c34dbd04d5770384aaf281d2be82507f71 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 29 Aug 2024 12:19:02 +0200 Subject: [PATCH 50/91] Try to fix error on runtests.yml --- .buildkite/runtests.yml | 310 ++++++++++++++++++++-------------------- 1 file changed, 155 insertions(+), 155 deletions(-) diff --git a/.buildkite/runtests.yml b/.buildkite/runtests.yml index 96de30cf3..1b11fff90 100644 --- a/.buildkite/runtests.yml +++ b/.buildkite/runtests.yml @@ -1,164 +1,164 @@ + steps: - steps: - - label: "CPU: Run tests on v{{matrix.version}}" - matrix: - setup: - version: - - "1.9" - - "1" - plugins: - - JuliaCI/julia#v1: - version: "{{matrix.version}}" - - JuliaCI/julia-coverage#v1: - codecov: true - dirs: - - KomaMRICore/src - - KomaMRICore/ext - command: | - julia -e 'println("--- :julia: Instantiating project") - using Pkg - Pkg.develop([ - PackageSpec(path=pwd(), subdir="KomaMRIBase"), - PackageSpec(path=pwd(), subdir="KomaMRICore"), - ])' + - label: "CPU: Run tests on v{{matrix.version}}" + matrix: + setup: + version: + - "1.9" + - "1" + plugins: + - JuliaCI/julia#v1: + version: "{{matrix.version}}" + - JuliaCI/julia-coverage#v1: + codecov: true + dirs: + - KomaMRICore/src + - KomaMRICore/ext + command: | + julia -e 'println("--- :julia: Instantiating project") + using Pkg + Pkg.develop([ + PackageSpec(path=pwd(), subdir="KomaMRIBase"), + PackageSpec(path=pwd(), subdir="KomaMRICore"), + ])' - julia -e 'println("--- :julia: Running tests") - using Pkg - Pkg.test("KomaMRICore"; coverage=true, julia_args=`--threads=auto`)' - agents: - queue: "juliagpu" - timeout_in_minutes: 60 - - - label: "AMDGPU: Run tests on v{{matrix.version}}" - matrix: - setup: - version: - - "1" - plugins: - - JuliaCI/julia#v1: - version: "{{matrix.version}}" - - JuliaCI/julia-coverage#v1: - codecov: true - dirs: - - KomaMRICore/src - - KomaMRICore/ext - command: | - julia -e 'println("--- :julia: Instantiating project") - using Pkg - Pkg.develop([ - PackageSpec(path=pwd(), subdir="KomaMRIBase"), - PackageSpec(path=pwd(), subdir="KomaMRICore"), - ])' - - julia --project=KomaMRICore/test -e 'println("--- :julia: Add AMDGPU to test environment") - using Pkg - Pkg.add("AMDGPU")' - - julia -e 'println("--- :julia: Running tests") - using Pkg - Pkg.test("KomaMRICore"; coverage=true, test_args=["AMDGPU"])' - agents: - queue: "juliagpu" - rocm: "*" - timeout_in_minutes: 60 + julia -e 'println("--- :julia: Running tests") + using Pkg + Pkg.test("KomaMRICore"; coverage=true, julia_args=`--threads=auto`)' + agents: + queue: "juliagpu" + timeout_in_minutes: 60 + + - label: "AMDGPU: Run tests on v{{matrix.version}}" + matrix: + setup: + version: + - "1" + plugins: + - JuliaCI/julia#v1: + version: "{{matrix.version}}" + - JuliaCI/julia-coverage#v1: + codecov: true + dirs: + - KomaMRICore/src + - KomaMRICore/ext + command: | + julia -e 'println("--- :julia: Instantiating project") + using Pkg + Pkg.develop([ + PackageSpec(path=pwd(), subdir="KomaMRIBase"), + PackageSpec(path=pwd(), subdir="KomaMRICore"), + ])' + + julia --project=KomaMRICore/test -e 'println("--- :julia: Add AMDGPU to test environment") + using Pkg + Pkg.add("AMDGPU")' + + julia -e 'println("--- :julia: Running tests") + using Pkg + Pkg.test("KomaMRICore"; coverage=true, test_args=["AMDGPU"])' + agents: + queue: "juliagpu" + rocm: "*" + timeout_in_minutes: 60 - - label: "CUDA: Run tests on v{{matrix.version}}" - matrix: - setup: - version: - - "1.9" - - "1" - plugins: - - JuliaCI/julia#v1: - version: "{{matrix.version}}" - - JuliaCI/julia-coverage#v1: - codecov: true - dirs: - - KomaMRICore/src - - KomaMRICore/ext - command: | - julia -e 'println("--- :julia: Instantiating project") - using Pkg - Pkg.develop([ - PackageSpec(path=pwd(), subdir="KomaMRIBase"), - PackageSpec(path=pwd(), subdir="KomaMRICore"), - ])' - - julia --project=KomaMRICore/test -e 'println("--- :julia: Add CUDA to test environment") - using Pkg - Pkg.add("CUDA")' - - julia -e 'println("--- :julia: Running tests") - using Pkg - Pkg.test("KomaMRICore"; coverage=true, test_args=["CUDA"])' - agents: - queue: "juliagpu" - cuda: "*" - timeout_in_minutes: 60 + - label: "CUDA: Run tests on v{{matrix.version}}" + matrix: + setup: + version: + - "1.9" + - "1" + plugins: + - JuliaCI/julia#v1: + version: "{{matrix.version}}" + - JuliaCI/julia-coverage#v1: + codecov: true + dirs: + - KomaMRICore/src + - KomaMRICore/ext + command: | + julia -e 'println("--- :julia: Instantiating project") + using Pkg + Pkg.develop([ + PackageSpec(path=pwd(), subdir="KomaMRIBase"), + PackageSpec(path=pwd(), subdir="KomaMRICore"), + ])' + + julia --project=KomaMRICore/test -e 'println("--- :julia: Add CUDA to test environment") + using Pkg + Pkg.add("CUDA")' + + julia -e 'println("--- :julia: Running tests") + using Pkg + Pkg.test("KomaMRICore"; coverage=true, test_args=["CUDA"])' + agents: + queue: "juliagpu" + cuda: "*" + timeout_in_minutes: 60 - - label: "Metal: Run tests on v{{matrix.version}}" - matrix: - setup: - version: - - "1.9" - - "1" - plugins: - - JuliaCI/julia#v1: - version: "{{matrix.version}}" - command: | - julia -e 'println("--- :julia: Instantiating project") - using Pkg - Pkg.develop([ - PackageSpec(path=pwd(), subdir="KomaMRIBase"), - PackageSpec(path=pwd(), subdir="KomaMRICore"), - ])' - - julia --project=KomaMRICore/test -e 'println("--- :julia: Add Metal to test environment") - using Pkg - Pkg.add("Metal")' + - label: "Metal: Run tests on v{{matrix.version}}" + matrix: + setup: + version: + - "1.9" + - "1" + plugins: + - JuliaCI/julia#v1: + version: "{{matrix.version}}" + command: | + julia -e 'println("--- :julia: Instantiating project") + using Pkg + Pkg.develop([ + PackageSpec(path=pwd(), subdir="KomaMRIBase"), + PackageSpec(path=pwd(), subdir="KomaMRICore"), + ])' + + julia --project=KomaMRICore/test -e 'println("--- :julia: Add Metal to test environment") + using Pkg + Pkg.add("Metal")' - julia -e 'println("--- :julia: Running tests") - using Pkg - Pkg.test("KomaMRICore"; test_args=["Metal"])' - agents: - queue: "juliaecosystem" - os: "macos" - arch: "aarch64" - timeout_in_minutes: 60 + julia -e 'println("--- :julia: Running tests") + using Pkg + Pkg.test("KomaMRICore"; test_args=["Metal"])' + agents: + queue: "juliaecosystem" + os: "macos" + arch: "aarch64" + timeout_in_minutes: 60 - - label: "oneAPI: Run tests on v{{matrix.version}}" - matrix: - setup: - version: - - "1.9" - - "1" - plugins: - - JuliaCI/julia#v1: - version: "{{matrix.version}}" - - JuliaCI/julia-coverage#v1: - codecov: true - dirs: - - KomaMRICore/src - - KomaMRICore/ext - command: | - julia -e 'println("--- :julia: Instantiating project") - using Pkg - Pkg.develop([ - PackageSpec(path=pwd(), subdir="KomaMRIBase"), - PackageSpec(path=pwd(), subdir="KomaMRICore"), - ])' - - julia --project=KomaMRICore/test -e 'println("--- :julia: Add oneAPI to test environment") - using Pkg - Pkg.add("oneAPI")' - - julia -e 'println("--- :julia: Running tests") - using Pkg - Pkg.test("KomaMRICore"; coverage=true, test_args=["oneAPI"])' - agents: - queue: "juliagpu" - intel: "*" - timeout_in_minutes: 60 + - label: "oneAPI: Run tests on v{{matrix.version}}" + matrix: + setup: + version: + - "1.9" + - "1" + plugins: + - JuliaCI/julia#v1: + version: "{{matrix.version}}" + - JuliaCI/julia-coverage#v1: + codecov: true + dirs: + - KomaMRICore/src + - KomaMRICore/ext + command: | + julia -e 'println("--- :julia: Instantiating project") + using Pkg + Pkg.develop([ + PackageSpec(path=pwd(), subdir="KomaMRIBase"), + PackageSpec(path=pwd(), subdir="KomaMRICore"), + ])' + + julia --project=KomaMRICore/test -e 'println("--- :julia: Add oneAPI to test environment") + using Pkg + Pkg.add("oneAPI")' + + julia -e 'println("--- :julia: Running tests") + using Pkg + Pkg.test("KomaMRICore"; coverage=true, test_args=["oneAPI"])' + agents: + queue: "juliagpu" + intel: "*" + timeout_in_minutes: 60 env: CI: BUILDKITE From 911544f1b341f3dc167e8dd78367bc96658fdc69 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 29 Aug 2024 17:33:18 +0200 Subject: [PATCH 51/91] Try to fix buildkite tests --- .buildkite/pipeline.yml | 4 +- .buildkite/runtests.yml | 311 ++++++++++---------- KomaMRICore/test/runtests.jl | 487 ++++++++++++++++++++++++++++++-- KomaMRICore/test/test_core.jl | 380 ------------------------- KomaMRICore/test/test_motion.jl | 85 ------ 5 files changed, 621 insertions(+), 646 deletions(-) delete mode 100644 KomaMRICore/test/test_core.jl delete mode 100644 KomaMRICore/test/test_motion.jl diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index d5d9f3e02..7e8e614d9 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -1,7 +1,7 @@ steps: - - label: ":pipeline: Core Tests" + - label: ":pipeline: NoMotion Tests" env: - TEST_GROUP: "core" + TEST_GROUP: "nomotion" command: buildkite-agent pipeline upload .buildkite/runtests.yml agents: queue: "juliagpu" diff --git a/.buildkite/runtests.yml b/.buildkite/runtests.yml index 1b11fff90..b1e0c89ad 100644 --- a/.buildkite/runtests.yml +++ b/.buildkite/runtests.yml @@ -1,164 +1,165 @@ - steps: - - label: "CPU: Run tests on v{{matrix.version}}" - matrix: - setup: - version: - - "1.9" - - "1" - plugins: - - JuliaCI/julia#v1: - version: "{{matrix.version}}" - - JuliaCI/julia-coverage#v1: - codecov: true - dirs: - - KomaMRICore/src - - KomaMRICore/ext - command: | - julia -e 'println("--- :julia: Instantiating project") - using Pkg - Pkg.develop([ - PackageSpec(path=pwd(), subdir="KomaMRIBase"), - PackageSpec(path=pwd(), subdir="KomaMRICore"), - ])' + - group: ":julia: Tests" + steps: + - label: "CPU: Run tests on v{{matrix.version}}" + matrix: + setup: + version: + - "1.9" + - "1" + plugins: + - JuliaCI/julia#v1: + version: "{{matrix.version}}" + - JuliaCI/julia-coverage#v1: + codecov: true + dirs: + - KomaMRICore/src + - KomaMRICore/ext + command: | + julia -e 'println("--- :julia: Instantiating project") + using Pkg + Pkg.develop([ + PackageSpec(path=pwd(), subdir="KomaMRIBase"), + PackageSpec(path=pwd(), subdir="KomaMRICore"), + ])' - julia -e 'println("--- :julia: Running tests") - using Pkg - Pkg.test("KomaMRICore"; coverage=true, julia_args=`--threads=auto`)' - agents: - queue: "juliagpu" - timeout_in_minutes: 60 - - - label: "AMDGPU: Run tests on v{{matrix.version}}" - matrix: - setup: - version: - - "1" - plugins: - - JuliaCI/julia#v1: - version: "{{matrix.version}}" - - JuliaCI/julia-coverage#v1: - codecov: true - dirs: - - KomaMRICore/src - - KomaMRICore/ext - command: | - julia -e 'println("--- :julia: Instantiating project") - using Pkg - Pkg.develop([ - PackageSpec(path=pwd(), subdir="KomaMRIBase"), - PackageSpec(path=pwd(), subdir="KomaMRICore"), - ])' - - julia --project=KomaMRICore/test -e 'println("--- :julia: Add AMDGPU to test environment") - using Pkg - Pkg.add("AMDGPU")' - - julia -e 'println("--- :julia: Running tests") - using Pkg - Pkg.test("KomaMRICore"; coverage=true, test_args=["AMDGPU"])' - agents: - queue: "juliagpu" - rocm: "*" - timeout_in_minutes: 60 + julia -e 'println("--- :julia: Running tests") + using Pkg + Pkg.test("KomaMRICore"; coverage=true, julia_args=`--threads=auto`)' + agents: + queue: "juliagpu" + timeout_in_minutes: 60 + + - label: "AMDGPU: Run tests on v{{matrix.version}}" + matrix: + setup: + version: + - "1" + plugins: + - JuliaCI/julia#v1: + version: "{{matrix.version}}" + - JuliaCI/julia-coverage#v1: + codecov: true + dirs: + - KomaMRICore/src + - KomaMRICore/ext + command: | + julia -e 'println("--- :julia: Instantiating project") + using Pkg + Pkg.develop([ + PackageSpec(path=pwd(), subdir="KomaMRIBase"), + PackageSpec(path=pwd(), subdir="KomaMRICore"), + ])' + + julia --project=KomaMRICore/test -e 'println("--- :julia: Add AMDGPU to test environment") + using Pkg + Pkg.add("AMDGPU")' + + julia -e 'println("--- :julia: Running tests") + using Pkg + Pkg.test("KomaMRICore"; coverage=true, test_args=["AMDGPU", ])' + agents: + queue: "juliagpu" + rocm: "*" + timeout_in_minutes: 60 - - label: "CUDA: Run tests on v{{matrix.version}}" - matrix: - setup: - version: - - "1.9" - - "1" - plugins: - - JuliaCI/julia#v1: - version: "{{matrix.version}}" - - JuliaCI/julia-coverage#v1: - codecov: true - dirs: - - KomaMRICore/src - - KomaMRICore/ext - command: | - julia -e 'println("--- :julia: Instantiating project") - using Pkg - Pkg.develop([ - PackageSpec(path=pwd(), subdir="KomaMRIBase"), - PackageSpec(path=pwd(), subdir="KomaMRICore"), - ])' - - julia --project=KomaMRICore/test -e 'println("--- :julia: Add CUDA to test environment") - using Pkg - Pkg.add("CUDA")' - - julia -e 'println("--- :julia: Running tests") - using Pkg - Pkg.test("KomaMRICore"; coverage=true, test_args=["CUDA"])' - agents: - queue: "juliagpu" - cuda: "*" - timeout_in_minutes: 60 + - label: "CUDA: Run tests on v{{matrix.version}}" + matrix: + setup: + version: + - "1.9" + - "1" + plugins: + - JuliaCI/julia#v1: + version: "{{matrix.version}}" + - JuliaCI/julia-coverage#v1: + codecov: true + dirs: + - KomaMRICore/src + - KomaMRICore/ext + command: | + julia -e 'println("--- :julia: Instantiating project") + using Pkg + Pkg.develop([ + PackageSpec(path=pwd(), subdir="KomaMRIBase"), + PackageSpec(path=pwd(), subdir="KomaMRICore"), + ])' + + julia --project=KomaMRICore/test -e 'println("--- :julia: Add CUDA to test environment") + using Pkg + Pkg.add("CUDA")' + + julia -e 'println("--- :julia: Running tests") + using Pkg + Pkg.test("KomaMRICore"; coverage=true, test_args=["CUDA"])' + agents: + queue: "juliagpu" + cuda: "*" + timeout_in_minutes: 60 - - label: "Metal: Run tests on v{{matrix.version}}" - matrix: - setup: - version: - - "1.9" - - "1" - plugins: - - JuliaCI/julia#v1: - version: "{{matrix.version}}" - command: | - julia -e 'println("--- :julia: Instantiating project") - using Pkg - Pkg.develop([ - PackageSpec(path=pwd(), subdir="KomaMRIBase"), - PackageSpec(path=pwd(), subdir="KomaMRICore"), - ])' - - julia --project=KomaMRICore/test -e 'println("--- :julia: Add Metal to test environment") - using Pkg - Pkg.add("Metal")' + - label: "Metal: Run tests on v{{matrix.version}}" + matrix: + setup: + version: + - "1.9" + - "1" + plugins: + - JuliaCI/julia#v1: + version: "{{matrix.version}}" + command: | + julia -e 'println("--- :julia: Instantiating project") + using Pkg + Pkg.develop([ + PackageSpec(path=pwd(), subdir="KomaMRIBase"), + PackageSpec(path=pwd(), subdir="KomaMRICore"), + ])' + + julia --project=KomaMRICore/test -e 'println("--- :julia: Add Metal to test environment") + using Pkg + Pkg.add("Metal")' - julia -e 'println("--- :julia: Running tests") - using Pkg - Pkg.test("KomaMRICore"; test_args=["Metal"])' - agents: - queue: "juliaecosystem" - os: "macos" - arch: "aarch64" - timeout_in_minutes: 60 + julia -e 'println("--- :julia: Running tests") + using Pkg + Pkg.test("KomaMRICore"; test_args=["Metal"])' + agents: + queue: "juliaecosystem" + os: "macos" + arch: "aarch64" + timeout_in_minutes: 60 - - label: "oneAPI: Run tests on v{{matrix.version}}" - matrix: - setup: - version: - - "1.9" - - "1" - plugins: - - JuliaCI/julia#v1: - version: "{{matrix.version}}" - - JuliaCI/julia-coverage#v1: - codecov: true - dirs: - - KomaMRICore/src - - KomaMRICore/ext - command: | - julia -e 'println("--- :julia: Instantiating project") - using Pkg - Pkg.develop([ - PackageSpec(path=pwd(), subdir="KomaMRIBase"), - PackageSpec(path=pwd(), subdir="KomaMRICore"), - ])' - - julia --project=KomaMRICore/test -e 'println("--- :julia: Add oneAPI to test environment") - using Pkg - Pkg.add("oneAPI")' - - julia -e 'println("--- :julia: Running tests") - using Pkg - Pkg.test("KomaMRICore"; coverage=true, test_args=["oneAPI"])' - agents: - queue: "juliagpu" - intel: "*" - timeout_in_minutes: 60 + - label: "oneAPI: Run tests on v{{matrix.version}}" + matrix: + setup: + version: + - "1.9" + - "1" + plugins: + - JuliaCI/julia#v1: + version: "{{matrix.version}}" + - JuliaCI/julia-coverage#v1: + codecov: true + dirs: + - KomaMRICore/src + - KomaMRICore/ext + command: | + julia -e 'println("--- :julia: Instantiating project") + using Pkg + Pkg.develop([ + PackageSpec(path=pwd(), subdir="KomaMRIBase"), + PackageSpec(path=pwd(), subdir="KomaMRICore"), + ])' + + julia --project=KomaMRICore/test -e 'println("--- :julia: Add oneAPI to test environment") + using Pkg + Pkg.add("oneAPI")' + + julia -e 'println("--- :julia: Running tests") + using Pkg + Pkg.test("KomaMRICore"; coverage=true, test_args=["oneAPI"])' + agents: + queue: "juliagpu" + intel: "*" + timeout_in_minutes: 60 env: CI: BUILDKITE diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index 24d5b4a81..93e9599c0 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -33,37 +33,476 @@ using TestItems, TestItemRunner # ### -group = get(ENV, "TEST_GROUP", :all) |> Symbol -test_file = get(ENV, "TEST_FILE", :none) |> Symbol +#Environment variable set by CI +const CI = get(ENV, "CI", nothing) +const group = get(ENV, "TEST_GROUP", :core) |> Symbol + +@run_package_tests filter=ti->(group in ti.tags)&&(isnothing(CI) || :skipci ∉ ti.tags) #verbose=true + +@testitem "Spinors×Mag" tags=[:core, :nomotion] begin + using KomaMRICore: Rx, Ry, Rz, Q, rotx, roty, rotz, Un, Rφ, Rg + + ## Verifying that operators perform counter-clockwise rotations + v = [1, 2, 3] + m = Mag([complex(v[1:2]...)], [v[3]]) + # Rx + @test rotx(π/2) * v ≈ [1, -3, 2] + @test (Rx(π/2) * m).xy ≈ [1.0 - 3.0im] + @test (Rx(π/2) * m).z ≈ [2.0] + # Ry + @test roty(π/2) * v ≈ [3, 2, -1] + @test (Ry(π/2) * m).xy ≈ [3.0 + 2.0im] + @test (Ry(π/2) * m).z ≈ [-1.0] + # Rz + @test rotz(π/2) * v ≈ [-2, 1, 3] + @test (Rz(π/2) * m).xy ≈ [-2.0 + 1.0im] + @test (Rz(π/2) * m).z ≈ [3.0] + # Rn + @test Un(π/2, [1,0,0]) * v ≈ rotx(π/2) * v + @test Un(π/2, [0,1,0]) * v ≈ roty(π/2) * v + @test Un(π/2, [0,0,1]) * v ≈ rotz(π/2) * v + @test (Q(π/2, 1.0+0.0im, 0.0) * m).xy ≈ (Rx(π/2) * m).xy + @test (Q(π/2, 1.0+0.0im, 0.0) * m).z ≈ (Rx(π/2) * m).z + @test (Q(π/2, 0.0+1.0im, 0.0) * m).xy ≈ (Ry(π/2) * m).xy + @test (Q(π/2, 0.0+1.0im, 0.0) * m).z ≈ (Ry(π/2) * m).z + @test (Q(π/2, 0.0+0.0im, 1.0) * m).xy ≈ (Rz(π/2) * m).xy + @test (Q(π/2, 0.0+0.0im, 1.0) * m).z ≈ (Rz(π/2) * m).z + + ## Verify that Spinor rotation = matrix rotation + v = rand(3) + n = rand(3); n = n ./ sqrt(sum(n.^2)) + m = Mag([complex(v[1:2]...)], [v[3]]) + φ, θ, φ1, φ2 = rand(4) * 2π + # Rx + vx = rotx(θ) * v + mx = Rx(θ) * m + @test [real(mx.xy); imag(mx.xy); mx.z] ≈ vx + # Ry + vy = roty(θ) * v + my = Ry(θ) * m + @test [real(my.xy); imag(my.xy); my.z] ≈ vy + # Rz + vz = rotz(θ) * v + mz = Rz(θ) * m + @test [real(mz.xy); imag(mz.xy); mz.z] ≈ vz + # Rφ + vφ = Un(θ, [sin(φ); cos(φ); 0.0]) * v + mφ = Rφ(φ,θ) * m + @test [real(mφ.xy); imag(mφ.xy); mφ.z] ≈ vφ + # Rg + vg = rotz(φ2) * roty(θ) * rotz(φ1) * v + mg = Rg(φ1,θ,φ2) * m + @test [real(mg.xy); imag(mg.xy); mg.z] ≈ vg + # Rn + vq = Un(θ, n) * v + mq = Q(θ, n[1]+n[2]*1im, n[3]) * m + @test [real(mq.xy); imag(mq.xy); mq.z] ≈ vq -# if we are testing just a single file then group = :none -# to skip the full test suite -if test_file != :none - group = :none + ## Spinors satify that |α|^2 + |β|^2 = 1 + @test abs(Rx(θ)) ≈ [1] + @test abs(Ry(θ)) ≈ [1] + @test abs(Rz(θ)) ≈ [1] + @test abs(Rφ(φ,θ)) ≈ [1] + @test abs(Q(θ, n[1]+n[2]*1im, n[3])) ≈ [1] + + ## Checking properties of Introduction to the Shinnar-Le Roux algorithm. + # Rx = Rz(-π/2) * Ry(θ) * Rz(π/2) + @test rotx(θ) * v ≈ rotz(-π/2) * roty(θ) * rotz(π/2) * v + @test (Rx(θ) * m).xy ≈ (Rz(-π/2) * Ry(θ) * Rz(π/2) * m).xy + @test (Rx(θ) * m).z ≈ (Rz(-π/2) * Ry(θ) * Rz(π/2) * m).z + # Rφ(φ,θ) = Rz(-φ) Ry(θ) Rz(φ) + @test (Rφ(φ,θ) * m).xy ≈ (Rz(-φ) * Ry(θ) * Rz(φ) * m).xy + @test (Rφ(φ,θ) * m).z ≈ (Rz(-φ) * Ry(θ) * Rz(φ) * m).z + # Rg(φ1, θ, φ2) = Rz(φ2) Ry(θ) Rz(φ1) + @test (Rg(φ1,θ,φ2) * m).xy ≈ (Rz(φ2) * Ry(θ) * Rz(φ1) * m).xy + @test (Rg(φ1,θ,φ2) * m).z ≈ (Rz(φ2) * Ry(θ) * Rz(φ1) * m).z + # Rg(-φ, θ, φ) = Rz(-φ) Ry(θ) Rz(φ) = Rφ(φ,θ) + @test rotz(-φ) * roty(θ) * rotz(φ) * v ≈ Un(θ, [sin(φ); cos(φ); 0.0]) * v + @test (Rg(φ,θ,-φ) * m).xy ≈ (Rφ(φ,θ) * m).xy + @test (Rg(φ,θ,-φ) * m).z ≈ (Rφ(φ,θ) * m).z + + ## Verify trivial identities + # Rφ is an xy-plane rotation of θ around an axis making an angle of φ with respect to the y-axis + # Rφ φ=0 = Ry + @test (Rφ(0,θ) * m).xy ≈ (Ry(θ) * m).xy + @test (Rφ(0,θ) * m).z ≈ (Ry(θ) * m).z + # Rφ φ=π/2 = Rx + @test (Rφ(π/2,θ) * m).xy ≈ (Rx(θ) * m).xy + @test (Rφ(π/2,θ) * m).z ≈ (Rx(θ) * m).z + # General rotation Rn + # Rn n=[1,0,0] = Rx + @test Un(θ, [1,0,0]) * v ≈ rotx(θ) * v + @test (Q(θ, 1.0+0.0im, 0.0) * m).xy ≈ (Rx(θ) * m).xy + @test (Q(θ, 1.0+0.0im, 0.0) * m).z ≈ (Rx(θ) * m).z + # Rn n=[0,1,0] = Ry + @test Un(θ, [0,1,0]) * v ≈ roty(θ) * v + @test (Q(θ, 0.0+1.0im, 0.0) * m).xy ≈ (Ry(θ) * m).xy + @test (Q(θ, 0.0+1.0im, 0.0) * m).z ≈ (Ry(θ) * m).z + # Rn n=[0,0,1] = Rz + @test Un(θ, [0,0,1]) * v ≈ rotz(θ) * v + @test (Q(θ, 0.0+0.0im, 1.0) * m).xy ≈ (Rz(θ) * m).xy + @test (Q(θ, 0.0+0.0im, 1.0) * m).z ≈ (Rz(θ) * m).z + + # Associativity + # Rx + @test (((Rz(-π/2) * Ry(θ)) * Rz(π/2)) * m).xy ≈ (Rx(θ) * m).xy + @test (((Rz(-π/2) * Ry(θ)) * Rz(π/2)) * m).z ≈ (Rx(θ) * m).z + @test (Rz(-π/2) * (Ry(θ) * (Rz(π/2) * m))).xy ≈ (Rx(θ) * m).xy + @test (Rz(-π/2) * (Ry(θ) * (Rz(π/2) * m))).z ≈ (Rx(θ) * m).z + # Rφ + @test (Rφ(φ,θ) * m).xy ≈ (((Rz(-φ) * Ry(θ)) * Rz(φ)) * m).xy + @test (Rφ(φ,θ) * m).z ≈ (((Rz(-φ) * Ry(θ)) * Rz(φ)) * m).z + @test (Rφ(φ,θ) * m).xy ≈ ((Rz(-φ) * (Ry(θ) * Rz(φ))) * m).xy + @test (Rφ(φ,θ) * m).z ≈ ((Rz(-φ) * (Ry(θ) * Rz(φ))) * m).z + # Rg + @test (Rg(φ1,θ,φ2) * m).xy ≈ (((Rz(φ2) * Ry(θ)) * Rz(φ1)) * m).xy + @test (Rg(φ1,θ,φ2) * m).z ≈ (((Rz(φ2) * Ry(θ)) * Rz(φ1)) * m).z + @test (Rg(φ1,θ,φ2) * m).xy ≈ ((Rz(φ2) * (Ry(θ) * Rz(φ1))) * m).xy + @test (Rg(φ1,θ,φ2) * m).z ≈ ((Rz(φ2) * (Ry(θ) * Rz(φ1))) * m).z + + ## Other tests + # Test Spinor struct + α, β = rand(2) + s = Spinor(α, β) + @test s[1].α ≈ [Complex(α)] && s[1].β ≈ [Complex(β)] + # Just checking to ensure that show() doesn't get stuck and that it is covered + show(IOBuffer(), "text/plain", s) + @test true end -@testset "KomaMRICore" begin - if test_file != :none - @testset "Single file test" begin - include(String(test_file)) - end +@testitem "ISMRMRD" tags=[:core, :nomotion] begin + using Suppressor + include("initialize_backend.jl") + + seq = PulseDesigner.EPI_example() + sys = Scanner() + obj = brain_phantom2D() + parts = kfoldperm(length(obj), 2) + + sim_params = KomaMRICore.default_sim_params() + sim_params["return_type"] = "raw" + sim_params["gpu"] = USE_GPU + + sig1 = @suppress simulate(obj[parts[1]], seq, sys; sim_params) + sig2 = @suppress simulate(obj[parts[2]], seq, sys; sim_params) + sig = @suppress simulate(obj, seq, sys; sim_params) + + @test isapprox(sig, sig1 + sig2; rtol=0.001) +end + +@testitem "signal_to_raw_data" tags=[:core, :nomotion] begin + using Suppressor + include("initialize_backend.jl") + + seq = PulseDesigner.EPI_example() + sys = Scanner() + obj = brain_phantom2D() + + sim_params = KomaMRICore.default_sim_params() + sim_params["return_type"] = "mat" + sim_params["gpu"] = USE_GPU + sig = @suppress simulate(obj, seq, sys; sim_params) + + # Test signal_to_raw_data + raw = signal_to_raw_data(sig, seq) + sig_aux = vcat([vec(profile.data) for profile in raw.profiles]...) + sig_raw = reshape(sig_aux, length(sig_aux), 1) + @test all(sig .== sig_raw) + + seq.DEF["FOV"] = [23e-2, 23e-2, 0] + raw = signal_to_raw_data(sig, seq) + sig_aux = vcat([vec(profile.data) for profile in raw.profiles]...) + sig_raw = reshape(sig_aux, length(sig_aux), 1) + @test all(sig .== sig_raw) + + # Just checking to ensure that show() doesn't get stuck and that it is covered + show(IOBuffer(), "text/plain", raw) + @test true +end + +@testitem "Bloch" tags=[:important, :core, :nomotion] begin + using Suppressor + include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) + + sig_jemris = signal_sphere_jemris() + seq = seq_epi_100x100_TE100_FOV230() + obj = phantom_sphere() + sys = Scanner() + + sim_params = Dict{String, Any}( + "gpu"=>USE_GPU, + "sim_method"=>KomaMRICore.Bloch(), + "return_type"=>"mat" + ) + sig = @suppress simulate(obj, seq, sys; sim_params) + sig = sig / prod(size(obj)) + + NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + + @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% +end + +@testitem "Bloch_RF_accuracy" tags=[:important, :core, :nomotion] begin + using Suppressor + include("initialize_backend.jl") + + Tadc = 1e-3 + Trf = Tadc + T1 = 1000e-3 + T2 = 20e-3 + Δw = 2π * 100 + B1 = 2e-6 * (Tadc / Trf) + N = 6 + + sys = Scanner() + obj = Phantom{Float64}(x=[0.],T1=[T1],T2=[T2],Δw=[Δw]) + + rf_phase = [0, π/2] + seq = Sequence() + seq += ADC(N, Tadc) + for i=1:2 + global seq += RF(B1 .* exp(1im*rf_phase[i]), Trf) + global seq += ADC(N, Tadc) end + + sim_params = Dict{String, Any}("Δt_rf"=>1e-5, "gpu"=>USE_GPU) + raw = @suppress simulate(obj, seq, sys; sim_params) + + #Mathematica-simulated Bloch equation result + res1 = [0.153592+0.46505im, + 0.208571+0.437734im, + 0.259184+0.40408im, + 0.304722+0.364744im, + 0.344571+0.320455im, + 0.378217+0.272008im] + res2 = [-0.0153894+0.142582im, + 0.00257641+0.14196im, + 0.020146+0.13912im, + 0.037051+0.134149im, + 0.0530392+0.12717im, + 0.0678774+0.11833im] + norm2(x) = sqrt.(sum(abs.(x).^2)) + error0 = norm2(raw.profiles[1].data .- 0) + error1 = norm2(raw.profiles[2].data .- res1) ./ norm2(res1) * 100 + error2 = norm2(raw.profiles[3].data .- res2) ./ norm2(res2) * 100 + + @test error0 + error1 + error2 < 0.1 #NMRSE < 0.1% +end + +@testitem "Bloch_phase_compensation" tags=[:important, :core, :nomotion] begin + using Suppressor + include("initialize_backend.jl") + + Tadc = 1e-3 + Trf = Tadc + T1 = 1000e-3 + T2 = 20e-3 + Δw = 2π * 100 + B1 = 2e-6 * (Tadc / Trf) + N = 6 + + sys = Scanner() + obj = Phantom{Float64}(x=[0.],T1=[T1],T2=[T2],Δw=[Δw]) + + rf_phase = 2π*rand() + seq1 = Sequence() + seq1 += RF(B1, Trf) + seq1 += ADC(N, Tadc) + + seq2 = Sequence() + seq2 += RF(B1 .* exp(1im*rf_phase), Trf) + seq2 += ADC(N, Tadc, 0, 0, rf_phase) + + sim_params = Dict{String, Any}("Δt_rf"=>1e-5, "gpu"=>USE_GPU) + raw1 = @suppress simulate(obj, seq1, sys; sim_params) + raw2 = @suppress simulate(obj, seq2, sys; sim_params) + + @test raw1.profiles[1].data ≈ raw2.profiles[1].data +end + +@testitem "Bloch SimpleAction" tags=[:important, :core, :motion] begin + using Suppressor + include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) + + sig_jemris = signal_brain_motion_jemris() + seq = seq_epi_100x100_TE100_FOV230() + sys = Scanner() + obj = phantom_brain_simple_motion() + sim_params = Dict{String, Any}( + "gpu"=>USE_GPU, + "sim_method"=>KomaMRICore.Bloch(), + "return_type"=>"mat" + ) + sig = @suppress simulate(obj, seq, sys; sim_params) + sig = sig / prod(size(obj)) + NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + println("NMRSE SimpleAction: ", NMRSE(sig, sig_jemris)) + @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% +end + +@testitem "Bloch ArbitraryAction" tags=[:important, :core, :motion] begin + using Suppressor + include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) + + sig_jemris = signal_brain_motion_jemris() + seq = seq_epi_100x100_TE100_FOV230() + sys = Scanner() + obj = phantom_brain_arbitrary_motion() + sim_params = Dict{String, Any}( + "gpu"=>USE_GPU, + "sim_method"=>KomaMRICore.Bloch(), + "return_type"=>"mat" + ) + sig = @suppress simulate(obj, seq, sys; sim_params) + sig = sig / prod(size(obj)) + NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + println("NMRSE ArbitraryAction: ", NMRSE(sig, sig_jemris)) + @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% +end + +@testitem "BlochDict" tags=[:important, :core, :nomotion] begin + using Suppressor + include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) + + seq = seq_epi_100x100_TE100_FOV230() + obj = Phantom{Float64}(x=[0.], T1=[1000e-3], T2=[100e-3]) + sys = Scanner() + sim_params = Dict( + "gpu"=>USE_GPU, + "sim_method"=>KomaMRICore.Bloch(), + "return_type"=>"mat") + sig = @suppress simulate(obj, seq, sys; sim_params) + sig = sig / prod(size(obj)) + sim_params["sim_method"] = KomaMRICore.BlochDict() + sig2 = @suppress simulate(obj, seq, sys; sim_params) + sig2 = sig2 / prod(size(obj)) + @test sig ≈ sig2 + + # Just checking to ensure that show() doesn't get stuck and that it is covered + show(IOBuffer(), "text/plain", KomaMRICore.BlochDict()) + @test true +end + +@testitem "BlochSimple" tags=[:important, :core, :nomotion] begin + using Suppressor + include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) + + sig_jemris = signal_sphere_jemris() + seq = seq_epi_100x100_TE100_FOV230() + obj = phantom_sphere() + sys = Scanner() + + sim_params = Dict{String, Any}( + "gpu"=>USE_GPU, + "sim_method"=>KomaMRICore.BlochSimple(), + "return_type"=>"mat" + ) + sig = @suppress simulate(obj, seq, sys; sim_params) + sig = sig / prod(size(obj)) + + NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - if group == :core || group == :all - @testset "Core" begin - include("test_core.jl") - end - end + @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% +end - if group == :motion || group == :all - @testset "Motion" begin - include("test_motion.jl") - end - end +@testitem "BlochSimple SimpleAction" tags=[:important, :core, :motion] begin + using Suppressor + include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) + + sig_jemris = signal_brain_motion_jemris() + seq = seq_epi_100x100_TE100_FOV230() + sys = Scanner() + obj = phantom_brain_simple_motion() + + sim_params = Dict{String, Any}( + "gpu"=>USE_GPU, + "sim_method"=>KomaMRICore.BlochSimple(), + "return_type"=>"mat" + ) + sig = @suppress simulate(obj, seq, sys; sim_params) + sig = sig / prod(size(obj)) + NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + println("NMRSE SimpleAction BlochSimple: ", NMRSE(sig, sig_jemris)) + @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% +end + +@testitem "BlochSimple ArbitraryAction" tags=[:important, :core, :motion] begin + using Suppressor + include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) + + sig_jemris = signal_brain_motion_jemris() + seq = seq_epi_100x100_TE100_FOV230() + sys = Scanner() + obj = phantom_brain_arbitrary_motion() + + sim_params = Dict{String, Any}( + "gpu"=>USE_GPU, + "sim_method"=>KomaMRICore.BlochSimple(), + "return_type"=>"mat" + ) + sig = @suppress simulate(obj, seq, sys; sim_params) + sig = sig / prod(size(obj)) + NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + println("NMRSE ArbitraryAction BlochSimple: ", NMRSE(sig, sig_jemris)) + @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end -#Environment variable set by CI -const CI = get(ENV, "CI", nothing) -@run_package_tests filter=ti->(:core in ti.tags)&&(isnothing(CI) || :skipci ∉ ti.tags) #verbose=true +@testitem "simulate_slice_profile" tags=[:core, :nomotion] begin + using Suppressor + include("initialize_backend.jl") + # This is a sequence with a sinc RF 30° excitation pulse + sys = Scanner() + sys.Smax = 50 + B1 = 4.92e-6 + Trf = 3.2e-3 + zmax = 2e-2 + fmax = 5e3 + z = range(-zmax, zmax, 400) + Gz = fmax / (γ * zmax) + f = γ * Gz * z + seq = PulseDesigner.RF_sinc(B1, Trf, sys; G=[0; 0; Gz], TBP=8) + + # Simulate the slice profile + sim_params = Dict{String, Any}( + "Δt_rf" => Trf / length(seq.RF.A[1]), + "gpu" => USE_GPU) + M = @suppress simulate_slice_profile(seq; z, sim_params) + + # For the time being, always pass the test + @test true +end + +@testitem "GPU Functions" tags=[:core, :nomotion] begin + using Suppressor + import KernelAbstractions as KA + include("initialize_backend.jl") + + x = ones(Float32, 1000) + + @suppress begin + if USE_GPU + y = x |> gpu + @test KA.get_backend(y) isa KA.GPU + y = y |> cpu + @test KA.get_backend(y) isa KA.CPU + else + # Test that gpu and cpu are no-ops + y = x |> gpu + @test y == x + y = y |> cpu + @test y == x + end + end + + @suppress print_devices() + @test true +end diff --git a/KomaMRICore/test/test_core.jl b/KomaMRICore/test/test_core.jl deleted file mode 100644 index 4016cc34a..000000000 --- a/KomaMRICore/test/test_core.jl +++ /dev/null @@ -1,380 +0,0 @@ -@testitem "Spinors×Mag" begin - using KomaMRICore: Rx, Ry, Rz, Q, rotx, roty, rotz, Un, Rφ, Rg - - ## Verifying that operators perform counter-clockwise rotations - v = [1, 2, 3] - m = Mag([complex(v[1:2]...)], [v[3]]) - # Rx - @test rotx(π/2) * v ≈ [1, -3, 2] - @test (Rx(π/2) * m).xy ≈ [1.0 - 3.0im] - @test (Rx(π/2) * m).z ≈ [2.0] - # Ry - @test roty(π/2) * v ≈ [3, 2, -1] - @test (Ry(π/2) * m).xy ≈ [3.0 + 2.0im] - @test (Ry(π/2) * m).z ≈ [-1.0] - # Rz - @test rotz(π/2) * v ≈ [-2, 1, 3] - @test (Rz(π/2) * m).xy ≈ [-2.0 + 1.0im] - @test (Rz(π/2) * m).z ≈ [3.0] - # Rn - @test Un(π/2, [1,0,0]) * v ≈ rotx(π/2) * v - @test Un(π/2, [0,1,0]) * v ≈ roty(π/2) * v - @test Un(π/2, [0,0,1]) * v ≈ rotz(π/2) * v - @test (Q(π/2, 1.0+0.0im, 0.0) * m).xy ≈ (Rx(π/2) * m).xy - @test (Q(π/2, 1.0+0.0im, 0.0) * m).z ≈ (Rx(π/2) * m).z - @test (Q(π/2, 0.0+1.0im, 0.0) * m).xy ≈ (Ry(π/2) * m).xy - @test (Q(π/2, 0.0+1.0im, 0.0) * m).z ≈ (Ry(π/2) * m).z - @test (Q(π/2, 0.0+0.0im, 1.0) * m).xy ≈ (Rz(π/2) * m).xy - @test (Q(π/2, 0.0+0.0im, 1.0) * m).z ≈ (Rz(π/2) * m).z - - ## Verify that Spinor rotation = matrix rotation - v = rand(3) - n = rand(3); n = n ./ sqrt(sum(n.^2)) - m = Mag([complex(v[1:2]...)], [v[3]]) - φ, θ, φ1, φ2 = rand(4) * 2π - # Rx - vx = rotx(θ) * v - mx = Rx(θ) * m - @test [real(mx.xy); imag(mx.xy); mx.z] ≈ vx - # Ry - vy = roty(θ) * v - my = Ry(θ) * m - @test [real(my.xy); imag(my.xy); my.z] ≈ vy - # Rz - vz = rotz(θ) * v - mz = Rz(θ) * m - @test [real(mz.xy); imag(mz.xy); mz.z] ≈ vz - # Rφ - vφ = Un(θ, [sin(φ); cos(φ); 0.0]) * v - mφ = Rφ(φ,θ) * m - @test [real(mφ.xy); imag(mφ.xy); mφ.z] ≈ vφ - # Rg - vg = rotz(φ2) * roty(θ) * rotz(φ1) * v - mg = Rg(φ1,θ,φ2) * m - @test [real(mg.xy); imag(mg.xy); mg.z] ≈ vg - # Rn - vq = Un(θ, n) * v - mq = Q(θ, n[1]+n[2]*1im, n[3]) * m - @test [real(mq.xy); imag(mq.xy); mq.z] ≈ vq - - ## Spinors satify that |α|^2 + |β|^2 = 1 - @test abs(Rx(θ)) ≈ [1] - @test abs(Ry(θ)) ≈ [1] - @test abs(Rz(θ)) ≈ [1] - @test abs(Rφ(φ,θ)) ≈ [1] - @test abs(Q(θ, n[1]+n[2]*1im, n[3])) ≈ [1] - - ## Checking properties of Introduction to the Shinnar-Le Roux algorithm. - # Rx = Rz(-π/2) * Ry(θ) * Rz(π/2) - @test rotx(θ) * v ≈ rotz(-π/2) * roty(θ) * rotz(π/2) * v - @test (Rx(θ) * m).xy ≈ (Rz(-π/2) * Ry(θ) * Rz(π/2) * m).xy - @test (Rx(θ) * m).z ≈ (Rz(-π/2) * Ry(θ) * Rz(π/2) * m).z - # Rφ(φ,θ) = Rz(-φ) Ry(θ) Rz(φ) - @test (Rφ(φ,θ) * m).xy ≈ (Rz(-φ) * Ry(θ) * Rz(φ) * m).xy - @test (Rφ(φ,θ) * m).z ≈ (Rz(-φ) * Ry(θ) * Rz(φ) * m).z - # Rg(φ1, θ, φ2) = Rz(φ2) Ry(θ) Rz(φ1) - @test (Rg(φ1,θ,φ2) * m).xy ≈ (Rz(φ2) * Ry(θ) * Rz(φ1) * m).xy - @test (Rg(φ1,θ,φ2) * m).z ≈ (Rz(φ2) * Ry(θ) * Rz(φ1) * m).z - # Rg(-φ, θ, φ) = Rz(-φ) Ry(θ) Rz(φ) = Rφ(φ,θ) - @test rotz(-φ) * roty(θ) * rotz(φ) * v ≈ Un(θ, [sin(φ); cos(φ); 0.0]) * v - @test (Rg(φ,θ,-φ) * m).xy ≈ (Rφ(φ,θ) * m).xy - @test (Rg(φ,θ,-φ) * m).z ≈ (Rφ(φ,θ) * m).z - - ## Verify trivial identities - # Rφ is an xy-plane rotation of θ around an axis making an angle of φ with respect to the y-axis - # Rφ φ=0 = Ry - @test (Rφ(0,θ) * m).xy ≈ (Ry(θ) * m).xy - @test (Rφ(0,θ) * m).z ≈ (Ry(θ) * m).z - # Rφ φ=π/2 = Rx - @test (Rφ(π/2,θ) * m).xy ≈ (Rx(θ) * m).xy - @test (Rφ(π/2,θ) * m).z ≈ (Rx(θ) * m).z - # General rotation Rn - # Rn n=[1,0,0] = Rx - @test Un(θ, [1,0,0]) * v ≈ rotx(θ) * v - @test (Q(θ, 1.0+0.0im, 0.0) * m).xy ≈ (Rx(θ) * m).xy - @test (Q(θ, 1.0+0.0im, 0.0) * m).z ≈ (Rx(θ) * m).z - # Rn n=[0,1,0] = Ry - @test Un(θ, [0,1,0]) * v ≈ roty(θ) * v - @test (Q(θ, 0.0+1.0im, 0.0) * m).xy ≈ (Ry(θ) * m).xy - @test (Q(θ, 0.0+1.0im, 0.0) * m).z ≈ (Ry(θ) * m).z - # Rn n=[0,0,1] = Rz - @test Un(θ, [0,0,1]) * v ≈ rotz(θ) * v - @test (Q(θ, 0.0+0.0im, 1.0) * m).xy ≈ (Rz(θ) * m).xy - @test (Q(θ, 0.0+0.0im, 1.0) * m).z ≈ (Rz(θ) * m).z - - # Associativity - # Rx - @test (((Rz(-π/2) * Ry(θ)) * Rz(π/2)) * m).xy ≈ (Rx(θ) * m).xy - @test (((Rz(-π/2) * Ry(θ)) * Rz(π/2)) * m).z ≈ (Rx(θ) * m).z - @test (Rz(-π/2) * (Ry(θ) * (Rz(π/2) * m))).xy ≈ (Rx(θ) * m).xy - @test (Rz(-π/2) * (Ry(θ) * (Rz(π/2) * m))).z ≈ (Rx(θ) * m).z - # Rφ - @test (Rφ(φ,θ) * m).xy ≈ (((Rz(-φ) * Ry(θ)) * Rz(φ)) * m).xy - @test (Rφ(φ,θ) * m).z ≈ (((Rz(-φ) * Ry(θ)) * Rz(φ)) * m).z - @test (Rφ(φ,θ) * m).xy ≈ ((Rz(-φ) * (Ry(θ) * Rz(φ))) * m).xy - @test (Rφ(φ,θ) * m).z ≈ ((Rz(-φ) * (Ry(θ) * Rz(φ))) * m).z - # Rg - @test (Rg(φ1,θ,φ2) * m).xy ≈ (((Rz(φ2) * Ry(θ)) * Rz(φ1)) * m).xy - @test (Rg(φ1,θ,φ2) * m).z ≈ (((Rz(φ2) * Ry(θ)) * Rz(φ1)) * m).z - @test (Rg(φ1,θ,φ2) * m).xy ≈ ((Rz(φ2) * (Ry(θ) * Rz(φ1))) * m).xy - @test (Rg(φ1,θ,φ2) * m).z ≈ ((Rz(φ2) * (Ry(θ) * Rz(φ1))) * m).z - - ## Other tests - # Test Spinor struct - α, β = rand(2) - s = Spinor(α, β) - @test s[1].α ≈ [Complex(α)] && s[1].β ≈ [Complex(β)] - # Just checking to ensure that show() doesn't get stuck and that it is covered - show(IOBuffer(), "text/plain", s) - @test true -end - -@testitem "ISMRMRD" begin - using Suppressor - include("initialize_backend.jl") - - seq = PulseDesigner.EPI_example() - sys = Scanner() - obj = brain_phantom2D() - parts = kfoldperm(length(obj), 2) - - sim_params = KomaMRICore.default_sim_params() - sim_params["return_type"] = "raw" - sim_params["gpu"] = USE_GPU - - sig1 = @suppress simulate(obj[parts[1]], seq, sys; sim_params) - sig2 = @suppress simulate(obj[parts[2]], seq, sys; sim_params) - sig = @suppress simulate(obj, seq, sys; sim_params) - - @test isapprox(sig, sig1 + sig2; rtol=0.001) -end - -@testitem "signal_to_raw_data" begin - using Suppressor - include("initialize_backend.jl") - - seq = PulseDesigner.EPI_example() - sys = Scanner() - obj = brain_phantom2D() - - sim_params = KomaMRICore.default_sim_params() - sim_params["return_type"] = "mat" - sim_params["gpu"] = USE_GPU - sig = @suppress simulate(obj, seq, sys; sim_params) - - # Test signal_to_raw_data - raw = signal_to_raw_data(sig, seq) - sig_aux = vcat([vec(profile.data) for profile in raw.profiles]...) - sig_raw = reshape(sig_aux, length(sig_aux), 1) - @test all(sig .== sig_raw) - - seq.DEF["FOV"] = [23e-2, 23e-2, 0] - raw = signal_to_raw_data(sig, seq) - sig_aux = vcat([vec(profile.data) for profile in raw.profiles]...) - sig_raw = reshape(sig_aux, length(sig_aux), 1) - @test all(sig .== sig_raw) - - # Just checking to ensure that show() doesn't get stuck and that it is covered - show(IOBuffer(), "text/plain", raw) - @test true -end - -@testitem "Bloch" begin - using Suppressor - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - sig_jemris = signal_sphere_jemris() - seq = seq_epi_100x100_TE100_FOV230() - obj = phantom_sphere() - sys = Scanner() - - sim_params = Dict{String, Any}( - "gpu"=>USE_GPU, - "sim_method"=>KomaMRICore.Bloch(), - "return_type"=>"mat" - ) - sig = @suppress simulate(obj, seq, sys; sim_params) - sig = sig / prod(size(obj)) - - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - - @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% -end - -@testitem "Bloch_RF_accuracy" begin - using Suppressor - include("initialize_backend.jl") - - Tadc = 1e-3 - Trf = Tadc - T1 = 1000e-3 - T2 = 20e-3 - Δw = 2π * 100 - B1 = 2e-6 * (Tadc / Trf) - N = 6 - - sys = Scanner() - obj = Phantom{Float64}(x=[0.],T1=[T1],T2=[T2],Δw=[Δw]) - - rf_phase = [0, π/2] - seq = Sequence() - seq += ADC(N, Tadc) - for i=1:2 - global seq += RF(B1 .* exp(1im*rf_phase[i]), Trf) - global seq += ADC(N, Tadc) - end - - sim_params = Dict{String, Any}("Δt_rf"=>1e-5, "gpu"=>USE_GPU) - raw = @suppress simulate(obj, seq, sys; sim_params) - - #Mathematica-simulated Bloch equation result - res1 = [0.153592+0.46505im, - 0.208571+0.437734im, - 0.259184+0.40408im, - 0.304722+0.364744im, - 0.344571+0.320455im, - 0.378217+0.272008im] - res2 = [-0.0153894+0.142582im, - 0.00257641+0.14196im, - 0.020146+0.13912im, - 0.037051+0.134149im, - 0.0530392+0.12717im, - 0.0678774+0.11833im] - norm2(x) = sqrt.(sum(abs.(x).^2)) - error0 = norm2(raw.profiles[1].data .- 0) - error1 = norm2(raw.profiles[2].data .- res1) ./ norm2(res1) * 100 - error2 = norm2(raw.profiles[3].data .- res2) ./ norm2(res2) * 100 - - @test error0 + error1 + error2 < 0.1 #NMRSE < 0.1% -end - -@testitem "Bloch_phase_compensation" begin - using Suppressor - include("initialize_backend.jl") - - Tadc = 1e-3 - Trf = Tadc - T1 = 1000e-3 - T2 = 20e-3 - Δw = 2π * 100 - B1 = 2e-6 * (Tadc / Trf) - N = 6 - - sys = Scanner() - obj = Phantom{Float64}(x=[0.],T1=[T1],T2=[T2],Δw=[Δw]) - - rf_phase = 2π*rand() - seq1 = Sequence() - seq1 += RF(B1, Trf) - seq1 += ADC(N, Tadc) - - seq2 = Sequence() - seq2 += RF(B1 .* exp(1im*rf_phase), Trf) - seq2 += ADC(N, Tadc, 0, 0, rf_phase) - - sim_params = Dict{String, Any}("Δt_rf"=>1e-5, "gpu"=>USE_GPU) - raw1 = @suppress simulate(obj, seq1, sys; sim_params) - raw2 = @suppress simulate(obj, seq2, sys; sim_params) - - @test raw1.profiles[1].data ≈ raw2.profiles[1].data -end - -@testitem "BlochDict" begin - using Suppressor - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - seq = seq_epi_100x100_TE100_FOV230() - obj = Phantom{Float64}(x=[0.], T1=[1000e-3], T2=[100e-3]) - sys = Scanner() - sim_params = Dict( - "gpu"=>USE_GPU, - "sim_method"=>KomaMRICore.Bloch(), - "return_type"=>"mat") - sig = @suppress simulate(obj, seq, sys; sim_params) - sig = sig / prod(size(obj)) - sim_params["sim_method"] = KomaMRICore.BlochDict() - sig2 = @suppress simulate(obj, seq, sys; sim_params) - sig2 = sig2 / prod(size(obj)) - @test sig ≈ sig2 - - # Just checking to ensure that show() doesn't get stuck and that it is covered - show(IOBuffer(), "text/plain", KomaMRICore.BlochDict()) - @test true -end - -@testitem "BlochSimple" begin - using Suppressor - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - sig_jemris = signal_sphere_jemris() - seq = seq_epi_100x100_TE100_FOV230() - obj = phantom_sphere() - sys = Scanner() - - sim_params = Dict{String, Any}( - "gpu"=>USE_GPU, - "sim_method"=>KomaMRICore.BlochSimple(), - "return_type"=>"mat" - ) - sig = @suppress simulate(obj, seq, sys; sim_params) - sig = sig / prod(size(obj)) - - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - - @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% -end - -@testitem "simulate_slice_profile" begin - using Suppressor - include("initialize_backend.jl") - - # This is a sequence with a sinc RF 30° excitation pulse - sys = Scanner() - sys.Smax = 50 - B1 = 4.92e-6 - Trf = 3.2e-3 - zmax = 2e-2 - fmax = 5e3 - z = range(-zmax, zmax, 400) - Gz = fmax / (γ * zmax) - f = γ * Gz * z - seq = PulseDesigner.RF_sinc(B1, Trf, sys; G=[0; 0; Gz], TBP=8) - - # Simulate the slice profile - sim_params = Dict{String, Any}( - "Δt_rf" => Trf / length(seq.RF.A[1]), - "gpu" => USE_GPU) - M = @suppress simulate_slice_profile(seq; z, sim_params) - - # For the time being, always pass the test - @test true -end - -@testitem "GPU Functions" begin - using Suppressor - import KernelAbstractions as KA - include("initialize_backend.jl") - - x = ones(Float32, 1000) - - @suppress begin - if USE_GPU - y = x |> gpu - @test KA.get_backend(y) isa KA.GPU - y = y |> cpu - @test KA.get_backend(y) isa KA.CPU - else - # Test that gpu and cpu are no-ops - y = x |> gpu - @test y == x - y = y |> cpu - @test y == x - end - end - - @suppress print_devices() - @test true -end diff --git a/KomaMRICore/test/test_motion.jl b/KomaMRICore/test/test_motion.jl deleted file mode 100644 index 3a856b6d1..000000000 --- a/KomaMRICore/test/test_motion.jl +++ /dev/null @@ -1,85 +0,0 @@ -@testitem "Bloch SimpleAction" begin - using Suppressor - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - sig_jemris = signal_brain_motion_jemris() - seq = seq_epi_100x100_TE100_FOV230() - sys = Scanner() - obj = phantom_brain_simple_motion() - sim_params = Dict{String, Any}( - "gpu"=>USE_GPU, - "sim_method"=>KomaMRICore.Bloch(), - "return_type"=>"mat" - ) - sig = @suppress simulate(obj, seq, sys; sim_params) - sig = sig / prod(size(obj)) - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - println("NMRSE SimpleAction: ", NMRSE(sig, sig_jemris)) - @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% -end - -@testitem "Bloch ArbitraryAction" begin - using Suppressor - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - sig_jemris = signal_brain_motion_jemris() - seq = seq_epi_100x100_TE100_FOV230() - sys = Scanner() - obj = phantom_brain_arbitrary_motion() - sim_params = Dict{String, Any}( - "gpu"=>USE_GPU, - "sim_method"=>KomaMRICore.Bloch(), - "return_type"=>"mat" - ) - sig = @suppress simulate(obj, seq, sys; sim_params) - sig = sig / prod(size(obj)) - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - println("NMRSE ArbitraryAction: ", NMRSE(sig, sig_jemris)) - @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% -end - -@testitem "BlochSimple SimpleAction" begin - using Suppressor - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - sig_jemris = signal_brain_motion_jemris() - seq = seq_epi_100x100_TE100_FOV230() - sys = Scanner() - obj = phantom_brain_simple_motion() - - sim_params = Dict{String, Any}( - "gpu"=>USE_GPU, - "sim_method"=>KomaMRICore.BlochSimple(), - "return_type"=>"mat" - ) - sig = @suppress simulate(obj, seq, sys; sim_params) - sig = sig / prod(size(obj)) - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - println("NMRSE SimpleAction BlochSimple: ", NMRSE(sig, sig_jemris)) - @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% -end - -@testitem "BlochSimple ArbitraryAction" begin - using Suppressor - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - sig_jemris = signal_brain_motion_jemris() - seq = seq_epi_100x100_TE100_FOV230() - sys = Scanner() - obj = phantom_brain_arbitrary_motion() - - sim_params = Dict{String, Any}( - "gpu"=>USE_GPU, - "sim_method"=>KomaMRICore.BlochSimple(), - "return_type"=>"mat" - ) - sig = @suppress simulate(obj, seq, sys; sim_params) - sig = sig / prod(size(obj)) - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - println("NMRSE ArbitraryAction BlochSimple: ", NMRSE(sig, sig_jemris)) - @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% -end \ No newline at end of file From e792affe93e23152572f693a51c8c627f49606e5 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 29 Aug 2024 17:44:36 +0200 Subject: [PATCH 52/91] Change test group title for buildkite --- .buildkite/runtests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildkite/runtests.yml b/.buildkite/runtests.yml index b1e0c89ad..04bf4b2b5 100644 --- a/.buildkite/runtests.yml +++ b/.buildkite/runtests.yml @@ -1,5 +1,5 @@ steps: - - group: ":julia: Tests" + - group: ":julia: ({{env.TEST_GROUP}}) Tests" steps: - label: "CPU: Run tests on v{{matrix.version}}" matrix: From 007ee258697c8fc59ceb011c4a52b6ffa564c45e Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 29 Aug 2024 18:31:35 +0200 Subject: [PATCH 53/91] Try to interpolate env variable with $$ --- .buildkite/runtests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildkite/runtests.yml b/.buildkite/runtests.yml index 04bf4b2b5..bd7836a89 100644 --- a/.buildkite/runtests.yml +++ b/.buildkite/runtests.yml @@ -1,5 +1,5 @@ steps: - - group: ":julia: ({{env.TEST_GROUP}}) Tests" + - group: ":julia: ($$TEST_GROUP) Tests" steps: - label: "CPU: Run tests on v{{matrix.version}}" matrix: From 316fabffe6d113237dcaeabe1f25263db382e07b Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 29 Aug 2024 18:33:47 +0200 Subject: [PATCH 54/91] Try with only one $ --- .buildkite/runtests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildkite/runtests.yml b/.buildkite/runtests.yml index bd7836a89..d135225eb 100644 --- a/.buildkite/runtests.yml +++ b/.buildkite/runtests.yml @@ -1,5 +1,5 @@ steps: - - group: ":julia: ($$TEST_GROUP) Tests" + - group: ":julia: ($TEST_GROUP) Tests" steps: - label: "CPU: Run tests on v{{matrix.version}}" matrix: From 7049c60f91d8e50ec4710ee6f8e2fdab9ec68023 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 29 Aug 2024 18:41:23 +0200 Subject: [PATCH 55/91] Define env for each buildkite step --- .buildkite/runtests.yml | 10 ++++++++++ KomaMRICore/test/runtests.jl | 2 ++ 2 files changed, 12 insertions(+) diff --git a/.buildkite/runtests.yml b/.buildkite/runtests.yml index d135225eb..b88fd8163 100644 --- a/.buildkite/runtests.yml +++ b/.buildkite/runtests.yml @@ -15,6 +15,8 @@ steps: dirs: - KomaMRICore/src - KomaMRICore/ext + env: + TEST_GROUP: $TEST_GROUP command: | julia -e 'println("--- :julia: Instantiating project") using Pkg @@ -43,6 +45,8 @@ steps: dirs: - KomaMRICore/src - KomaMRICore/ext + env: + TEST_GROUP: $TEST_GROUP command: | julia -e 'println("--- :julia: Instantiating project") using Pkg @@ -77,6 +81,8 @@ steps: dirs: - KomaMRICore/src - KomaMRICore/ext + env: + TEST_GROUP: $TEST_GROUP command: | julia -e 'println("--- :julia: Instantiating project") using Pkg @@ -106,6 +112,8 @@ steps: plugins: - JuliaCI/julia#v1: version: "{{matrix.version}}" + env: + TEST_GROUP: $TEST_GROUP command: | julia -e 'println("--- :julia: Instantiating project") using Pkg @@ -141,6 +149,8 @@ steps: dirs: - KomaMRICore/src - KomaMRICore/ext + env: + TEST_GROUP: $TEST_GROUP command: | julia -e 'println("--- :julia: Instantiating project") using Pkg diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index 93e9599c0..cc02a0a1d 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -37,6 +37,8 @@ using TestItems, TestItemRunner const CI = get(ENV, "CI", nothing) const group = get(ENV, "TEST_GROUP", :core) |> Symbol +println("GROUP: ", group) + @run_package_tests filter=ti->(group in ti.tags)&&(isnothing(CI) || :skipci ∉ ti.tags) #verbose=true @testitem "Spinors×Mag" tags=[:core, :nomotion] begin From d88ecbaa3ffac18560da8170776d176e489c3528 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 29 Aug 2024 18:51:48 +0200 Subject: [PATCH 56/91] Motion and NoMotion tests separated for buildkite --- .buildkite/pipeline.yml | 4 ++-- KomaMRICore/test/runtests.jl | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 7e8e614d9..25aada17f 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -1,12 +1,12 @@ steps: - - label: ":pipeline: NoMotion Tests" + - label: ":pipeline: Upload NoMotion Tests" env: TEST_GROUP: "nomotion" command: buildkite-agent pipeline upload .buildkite/runtests.yml agents: queue: "juliagpu" - - label: ":pipeline: Motion Tests" + - label: ":pipeline: Upload Motion Tests" env: TEST_GROUP: "motion" command: buildkite-agent pipeline upload .buildkite/runtests.yml diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index cc02a0a1d..93e9599c0 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -37,8 +37,6 @@ using TestItems, TestItemRunner const CI = get(ENV, "CI", nothing) const group = get(ENV, "TEST_GROUP", :core) |> Symbol -println("GROUP: ", group) - @run_package_tests filter=ti->(group in ti.tags)&&(isnothing(CI) || :skipci ∉ ti.tags) #verbose=true @testitem "Spinors×Mag" tags=[:core, :nomotion] begin From 9913be1e8cadbce86dbfb91f2aae3645b5bb8033 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Fri, 30 Aug 2024 09:57:36 +0200 Subject: [PATCH 57/91] Only test `get_spin_coords` for ArbitraryAction in gpu --- .buildkite/pipeline.yml | 12 +-- KomaMRICore/test/runtests.jl | 190 +++++++++++++++++++---------------- 2 files changed, 109 insertions(+), 93 deletions(-) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 25aada17f..ba2b1f309 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -1,10 +1,10 @@ steps: - - label: ":pipeline: Upload NoMotion Tests" - env: - TEST_GROUP: "nomotion" - command: buildkite-agent pipeline upload .buildkite/runtests.yml - agents: - queue: "juliagpu" + # - label: ":pipeline: Upload NoMotion Tests" + # env: + # TEST_GROUP: "nomotion" + # command: buildkite-agent pipeline upload .buildkite/runtests.yml + # agents: + # queue: "juliagpu" - label: ":pipeline: Upload Motion Tests" env: diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index 93e9599c0..365ad9a00 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -321,48 +321,6 @@ end @test raw1.profiles[1].data ≈ raw2.profiles[1].data end -@testitem "Bloch SimpleAction" tags=[:important, :core, :motion] begin - using Suppressor - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - sig_jemris = signal_brain_motion_jemris() - seq = seq_epi_100x100_TE100_FOV230() - sys = Scanner() - obj = phantom_brain_simple_motion() - sim_params = Dict{String, Any}( - "gpu"=>USE_GPU, - "sim_method"=>KomaMRICore.Bloch(), - "return_type"=>"mat" - ) - sig = @suppress simulate(obj, seq, sys; sim_params) - sig = sig / prod(size(obj)) - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - println("NMRSE SimpleAction: ", NMRSE(sig, sig_jemris)) - @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% -end - -@testitem "Bloch ArbitraryAction" tags=[:important, :core, :motion] begin - using Suppressor - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - sig_jemris = signal_brain_motion_jemris() - seq = seq_epi_100x100_TE100_FOV230() - sys = Scanner() - obj = phantom_brain_arbitrary_motion() - sim_params = Dict{String, Any}( - "gpu"=>USE_GPU, - "sim_method"=>KomaMRICore.Bloch(), - "return_type"=>"mat" - ) - sig = @suppress simulate(obj, seq, sys; sim_params) - sig = sig / prod(size(obj)) - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - println("NMRSE ArbitraryAction: ", NMRSE(sig, sig_jemris)) - @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% -end - @testitem "BlochDict" tags=[:important, :core, :nomotion] begin using Suppressor include("initialize_backend.jl") @@ -410,51 +368,6 @@ end @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end -@testitem "BlochSimple SimpleAction" tags=[:important, :core, :motion] begin - using Suppressor - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - sig_jemris = signal_brain_motion_jemris() - seq = seq_epi_100x100_TE100_FOV230() - sys = Scanner() - obj = phantom_brain_simple_motion() - - sim_params = Dict{String, Any}( - "gpu"=>USE_GPU, - "sim_method"=>KomaMRICore.BlochSimple(), - "return_type"=>"mat" - ) - sig = @suppress simulate(obj, seq, sys; sim_params) - sig = sig / prod(size(obj)) - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - println("NMRSE SimpleAction BlochSimple: ", NMRSE(sig, sig_jemris)) - @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% -end - -@testitem "BlochSimple ArbitraryAction" tags=[:important, :core, :motion] begin - using Suppressor - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - sig_jemris = signal_brain_motion_jemris() - seq = seq_epi_100x100_TE100_FOV230() - sys = Scanner() - obj = phantom_brain_arbitrary_motion() - - sim_params = Dict{String, Any}( - "gpu"=>USE_GPU, - "sim_method"=>KomaMRICore.BlochSimple(), - "return_type"=>"mat" - ) - sig = @suppress simulate(obj, seq, sys; sim_params) - sig = sig / prod(size(obj)) - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - println("NMRSE ArbitraryAction BlochSimple: ", NMRSE(sig, sig_jemris)) - @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% -end - - @testitem "simulate_slice_profile" tags=[:core, :nomotion] begin using Suppressor include("initialize_backend.jl") @@ -506,3 +419,106 @@ end @suppress print_devices() @test true end + +# --------- Motion-related tests ------------- +@testitem "Bloch SimpleAction" tags=[:core] begin + using Suppressor + include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) + + sig_jemris = signal_brain_motion_jemris() + seq = seq_epi_100x100_TE100_FOV230() + sys = Scanner() + obj = phantom_brain_simple_motion() + sim_params = Dict{String, Any}( + "gpu"=>USE_GPU, + "sim_method"=>KomaMRICore.Bloch(), + "return_type"=>"mat" + ) + sig = @suppress simulate(obj, seq, sys; sim_params) + sig = sig / prod(size(obj)) + NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + println("NMRSE SimpleAction: ", NMRSE(sig, sig_jemris)) + @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% +end + +@testitem "BlochSimple SimpleAction" tags=[:core] begin + using Suppressor + include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) + + sig_jemris = signal_brain_motion_jemris() + seq = seq_epi_100x100_TE100_FOV230() + sys = Scanner() + obj = phantom_brain_simple_motion() + + sim_params = Dict{String, Any}( + "gpu"=>USE_GPU, + "sim_method"=>KomaMRICore.BlochSimple(), + "return_type"=>"mat" + ) + sig = @suppress simulate(obj, seq, sys; sim_params) + sig = sig / prod(size(obj)) + NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + println("NMRSE SimpleAction BlochSimple: ", NMRSE(sig, sig_jemris)) + @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% +end + +@testitem "Bloch ArbitraryAction" tags=[:core, :motion] begin + using Suppressor + include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) + + sig_jemris = signal_brain_motion_jemris() + seq = seq_epi_100x100_TE100_FOV230() + sys = Scanner() + obj = phantom_brain_arbitrary_motion() + + vx = 0.0 + vy = 0.1 + vz = 0.0 + t = collect(0:0.1:10) + + obj = obj |> f32 |> gpu + t = t |> f32 |> gpu + + x, y, z = get_spin_coords(obj.motion, obj.x, obj.y, obj.z, t') + + @test x ≈ obj.x .+ vx .* t' + @test y ≈ obj.y .+ vy .* t' + @test z ≈ obj.z .+ vz .* t' + + # sim_params = Dict{String, Any}( + # "gpu"=>USE_GPU, + # "sim_method"=>KomaMRICore.Bloch(), + # "return_type"=>"mat" + # ) + # sig = simulate(obj, seq, sys; sim_params) + # sig = sig / prod(size(obj)) + # NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + # println("NMRSE ArbitraryAction: ", NMRSE(sig, sig_jemris)) + # @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% +end + +@testitem "BlochSimple ArbitraryAction" tags=[:core] begin + using Suppressor + include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) + + sig_jemris = signal_brain_motion_jemris() + seq = seq_epi_100x100_TE100_FOV230() + sys = Scanner() + obj = phantom_brain_arbitrary_motion() + + sim_params = Dict{String, Any}( + "gpu"=>USE_GPU, + "sim_method"=>KomaMRICore.BlochSimple(), + "return_type"=>"mat" + ) + sig = simulate(obj, seq, sys; sim_params) + sig = sig / prod(size(obj)) + NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + println("NMRSE ArbitraryAction BlochSimple: ", NMRSE(sig, sig_jemris)) + @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% +end + From a1565536a31beda09e48f22eaa9517abbe53d148 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Fri, 30 Aug 2024 10:01:48 +0200 Subject: [PATCH 58/91] Fix test to use Float32 --- KomaMRICore/test/runtests.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index 365ad9a00..8c4b35515 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -474,9 +474,9 @@ end sys = Scanner() obj = phantom_brain_arbitrary_motion() - vx = 0.0 - vy = 0.1 - vz = 0.0 + vx = 0.0f0 + vy = 0.1f0 + vz = 0.0f0 t = collect(0:0.1:10) obj = obj |> f32 |> gpu From 2655f0dd66f28295bebfabf4f1b84e7133ff9f90 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Fri, 30 Aug 2024 10:04:39 +0200 Subject: [PATCH 59/91] Disable benchmarks temporarily --- .buildkite/pipeline.yml | 46 ++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index ba2b1f309..1b29e8731 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -13,27 +13,27 @@ steps: agents: queue: "juliagpu" - - label: ":pipeline: Launch Benchmarks" - if: build.message !~ /skip benchmarks/ - agents: - queue: "juliagpu" - plugins: - - monorepo-diff#v1.0.1: - diff: "git diff --name-only HEAD~1" - interpolation: false - watch: - - path: - - "KomaMRICore/src/**/*" - - "KomaMRICore/ext/**/*" - - "KomaMRICore/Project.toml" - - "KomaMRIBase/src/**/*" - - "KomaMRIBase/Project.toml" - - "benchmarks/**/*" - - ".buildkite/**/*" - - ".github/workflows/Benchmark.yml" - - "Project.toml" - config: - command: "buildkite-agent pipeline upload .buildkite/runbenchmarks.yml" - agents: - queue: "juliagpu" + # - label: ":pipeline: Launch Benchmarks" + # if: build.message !~ /skip benchmarks/ + # agents: + # queue: "juliagpu" + # plugins: + # - monorepo-diff#v1.0.1: + # diff: "git diff --name-only HEAD~1" + # interpolation: false + # watch: + # - path: + # - "KomaMRICore/src/**/*" + # - "KomaMRICore/ext/**/*" + # - "KomaMRICore/Project.toml" + # - "KomaMRIBase/src/**/*" + # - "KomaMRIBase/Project.toml" + # - "benchmarks/**/*" + # - ".buildkite/**/*" + # - ".github/workflows/Benchmark.yml" + # - "Project.toml" + # config: + # command: "buildkite-agent pipeline upload .buildkite/runbenchmarks.yml" + # agents: + # queue: "juliagpu" From 44debddc8e526be7f3ec22cb36eab0f099644467 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Fri, 30 Aug 2024 10:39:09 +0200 Subject: [PATCH 60/91] Only test ArbitraryMotion for oneAPI --- .buildkite/runtests.yml | 246 ++++++++++++++++++++-------------------- 1 file changed, 123 insertions(+), 123 deletions(-) diff --git a/.buildkite/runtests.yml b/.buildkite/runtests.yml index b88fd8163..f16e82d65 100644 --- a/.buildkite/runtests.yml +++ b/.buildkite/runtests.yml @@ -1,139 +1,139 @@ steps: - group: ":julia: ($TEST_GROUP) Tests" steps: - - label: "CPU: Run tests on v{{matrix.version}}" - matrix: - setup: - version: - - "1.9" - - "1" - plugins: - - JuliaCI/julia#v1: - version: "{{matrix.version}}" - - JuliaCI/julia-coverage#v1: - codecov: true - dirs: - - KomaMRICore/src - - KomaMRICore/ext - env: - TEST_GROUP: $TEST_GROUP - command: | - julia -e 'println("--- :julia: Instantiating project") - using Pkg - Pkg.develop([ - PackageSpec(path=pwd(), subdir="KomaMRIBase"), - PackageSpec(path=pwd(), subdir="KomaMRICore"), - ])' + # - label: "CPU: Run tests on v{{matrix.version}}" + # matrix: + # setup: + # version: + # - "1.9" + # - "1" + # plugins: + # - JuliaCI/julia#v1: + # version: "{{matrix.version}}" + # - JuliaCI/julia-coverage#v1: + # codecov: true + # dirs: + # - KomaMRICore/src + # - KomaMRICore/ext + # env: + # TEST_GROUP: $TEST_GROUP + # command: | + # julia -e 'println("--- :julia: Instantiating project") + # using Pkg + # Pkg.develop([ + # PackageSpec(path=pwd(), subdir="KomaMRIBase"), + # PackageSpec(path=pwd(), subdir="KomaMRICore"), + # ])' - julia -e 'println("--- :julia: Running tests") - using Pkg - Pkg.test("KomaMRICore"; coverage=true, julia_args=`--threads=auto`)' - agents: - queue: "juliagpu" - timeout_in_minutes: 60 + # julia -e 'println("--- :julia: Running tests") + # using Pkg + # Pkg.test("KomaMRICore"; coverage=true, julia_args=`--threads=auto`)' + # agents: + # queue: "juliagpu" + # timeout_in_minutes: 60 - - label: "AMDGPU: Run tests on v{{matrix.version}}" - matrix: - setup: - version: - - "1" - plugins: - - JuliaCI/julia#v1: - version: "{{matrix.version}}" - - JuliaCI/julia-coverage#v1: - codecov: true - dirs: - - KomaMRICore/src - - KomaMRICore/ext - env: - TEST_GROUP: $TEST_GROUP - command: | - julia -e 'println("--- :julia: Instantiating project") - using Pkg - Pkg.develop([ - PackageSpec(path=pwd(), subdir="KomaMRIBase"), - PackageSpec(path=pwd(), subdir="KomaMRICore"), - ])' + # - label: "AMDGPU: Run tests on v{{matrix.version}}" + # matrix: + # setup: + # version: + # - "1" + # plugins: + # - JuliaCI/julia#v1: + # version: "{{matrix.version}}" + # - JuliaCI/julia-coverage#v1: + # codecov: true + # dirs: + # - KomaMRICore/src + # - KomaMRICore/ext + # env: + # TEST_GROUP: $TEST_GROUP + # command: | + # julia -e 'println("--- :julia: Instantiating project") + # using Pkg + # Pkg.develop([ + # PackageSpec(path=pwd(), subdir="KomaMRIBase"), + # PackageSpec(path=pwd(), subdir="KomaMRICore"), + # ])' - julia --project=KomaMRICore/test -e 'println("--- :julia: Add AMDGPU to test environment") - using Pkg - Pkg.add("AMDGPU")' + # julia --project=KomaMRICore/test -e 'println("--- :julia: Add AMDGPU to test environment") + # using Pkg + # Pkg.add("AMDGPU")' - julia -e 'println("--- :julia: Running tests") - using Pkg - Pkg.test("KomaMRICore"; coverage=true, test_args=["AMDGPU", ])' - agents: - queue: "juliagpu" - rocm: "*" - timeout_in_minutes: 60 + # julia -e 'println("--- :julia: Running tests") + # using Pkg + # Pkg.test("KomaMRICore"; coverage=true, test_args=["AMDGPU", ])' + # agents: + # queue: "juliagpu" + # rocm: "*" + # timeout_in_minutes: 60 - - label: "CUDA: Run tests on v{{matrix.version}}" - matrix: - setup: - version: - - "1.9" - - "1" - plugins: - - JuliaCI/julia#v1: - version: "{{matrix.version}}" - - JuliaCI/julia-coverage#v1: - codecov: true - dirs: - - KomaMRICore/src - - KomaMRICore/ext - env: - TEST_GROUP: $TEST_GROUP - command: | - julia -e 'println("--- :julia: Instantiating project") - using Pkg - Pkg.develop([ - PackageSpec(path=pwd(), subdir="KomaMRIBase"), - PackageSpec(path=pwd(), subdir="KomaMRICore"), - ])' + # - label: "CUDA: Run tests on v{{matrix.version}}" + # matrix: + # setup: + # version: + # - "1.9" + # - "1" + # plugins: + # - JuliaCI/julia#v1: + # version: "{{matrix.version}}" + # - JuliaCI/julia-coverage#v1: + # codecov: true + # dirs: + # - KomaMRICore/src + # - KomaMRICore/ext + # env: + # TEST_GROUP: $TEST_GROUP + # command: | + # julia -e 'println("--- :julia: Instantiating project") + # using Pkg + # Pkg.develop([ + # PackageSpec(path=pwd(), subdir="KomaMRIBase"), + # PackageSpec(path=pwd(), subdir="KomaMRICore"), + # ])' - julia --project=KomaMRICore/test -e 'println("--- :julia: Add CUDA to test environment") - using Pkg - Pkg.add("CUDA")' + # julia --project=KomaMRICore/test -e 'println("--- :julia: Add CUDA to test environment") + # using Pkg + # Pkg.add("CUDA")' - julia -e 'println("--- :julia: Running tests") - using Pkg - Pkg.test("KomaMRICore"; coverage=true, test_args=["CUDA"])' - agents: - queue: "juliagpu" - cuda: "*" - timeout_in_minutes: 60 + # julia -e 'println("--- :julia: Running tests") + # using Pkg + # Pkg.test("KomaMRICore"; coverage=true, test_args=["CUDA"])' + # agents: + # queue: "juliagpu" + # cuda: "*" + # timeout_in_minutes: 60 - - label: "Metal: Run tests on v{{matrix.version}}" - matrix: - setup: - version: - - "1.9" - - "1" - plugins: - - JuliaCI/julia#v1: - version: "{{matrix.version}}" - env: - TEST_GROUP: $TEST_GROUP - command: | - julia -e 'println("--- :julia: Instantiating project") - using Pkg - Pkg.develop([ - PackageSpec(path=pwd(), subdir="KomaMRIBase"), - PackageSpec(path=pwd(), subdir="KomaMRICore"), - ])' + # - label: "Metal: Run tests on v{{matrix.version}}" + # matrix: + # setup: + # version: + # - "1.9" + # - "1" + # plugins: + # - JuliaCI/julia#v1: + # version: "{{matrix.version}}" + # env: + # TEST_GROUP: $TEST_GROUP + # command: | + # julia -e 'println("--- :julia: Instantiating project") + # using Pkg + # Pkg.develop([ + # PackageSpec(path=pwd(), subdir="KomaMRIBase"), + # PackageSpec(path=pwd(), subdir="KomaMRICore"), + # ])' - julia --project=KomaMRICore/test -e 'println("--- :julia: Add Metal to test environment") - using Pkg - Pkg.add("Metal")' + # julia --project=KomaMRICore/test -e 'println("--- :julia: Add Metal to test environment") + # using Pkg + # Pkg.add("Metal")' - julia -e 'println("--- :julia: Running tests") - using Pkg - Pkg.test("KomaMRICore"; test_args=["Metal"])' - agents: - queue: "juliaecosystem" - os: "macos" - arch: "aarch64" - timeout_in_minutes: 60 + # julia -e 'println("--- :julia: Running tests") + # using Pkg + # Pkg.test("KomaMRICore"; test_args=["Metal"])' + # agents: + # queue: "juliaecosystem" + # os: "macos" + # arch: "aarch64" + # timeout_in_minutes: 60 - label: "oneAPI: Run tests on v{{matrix.version}}" matrix: From 67f2f33e56649893ee4f9a809c58b771b7f04148 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Fri, 30 Aug 2024 12:35:31 +0200 Subject: [PATCH 61/91] New example phantoms --- examples/2.phantoms/artery.phantom | Bin 0 -> 24815032 bytes examples/2.phantoms/brain_motion.phantom | Bin 432088 -> 429552 bytes examples/2.phantoms/contracting_ring.phantom | Bin 1047608 -> 1044144 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 examples/2.phantoms/artery.phantom diff --git a/examples/2.phantoms/artery.phantom b/examples/2.phantoms/artery.phantom new file mode 100644 index 0000000000000000000000000000000000000000..62b1f3b4b6ba1771b1ad9386933240270affa904 GIT binary patch literal 24815032 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 z9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_ zzyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb< z0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q z9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_ zzyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb< z0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q z9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_ zzyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb< z0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q z9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_ zzyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb< z0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q z9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_ zzyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb< z0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q z9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_ zzyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb< z0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q9KZn_zyTb<0UW>q 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 z1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+fC3bt00k&O0SZun0u-PC1t>rP3Q&Lo z6rcbFC_n)UP=Epypa2CZKmiI+fC3bt00k&O0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)U zP=Epypa2CZKmiI+fC3bt00k&O0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZ zKmiI+fC3bt00k&O0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+fC3bt z00k&O0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+fC3bt00k&O0SZun z0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+fC3bt00k&O0SZun0u-PC1t>rP z3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+fC3bt00k&O0SZun0u-PC1t>rP3Q&Lo6rcbF zC_n)UP=Epypa2CZKmiI+fC3bt00k&O0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epy zpa2CZKmiI+fC3bt00k&O0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+ zfC3bt00k&O0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+fC3bt00k&O z0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+fC3bt00k&O0SZun0u-PC z1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+fC3bt00k&O0SZun0u-PC1t>rP3Q&Lo z6rcbFC_n)UP=Epypa2CZKmiI+fC3bt00k&O0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)U zP=Epypa2CZKmiI+fC3bt00k&O0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZ zKmiI+fC3bt00k&O0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+fC3bt z00k&O0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+fC3bt00k&O0SZun z0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+fC3bt00k&O0SZun0u-PC1t>rP z3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+fC3bt00k&O0SZun0u-PC1t>rP3Q&Lo6rcbF zC_n)UP=Epypa2CZKmiI+fC3bt00k&O0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epy zpa2CZKmiI+fC3bt00k&O0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+ zfC3bt00k&O0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+fC3bt00k&O z0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+fC3bt00k&O0SZun0u-PC z1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+fC3bt00k&O0SZun0u-PC1t>rP3Q&Lo z6rcbFC_n)UP=Epypa2CZKmiI+fC3bt00k&O0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)U zP=Epypa2CZKmiI+fC3bt00k&O0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZ zKmiI+fC3bt00k&O0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+fC3bt z00k&O0SZun0u-PC1t>rP3Q&Lo6rcbFC_sTz6!`XUec*vx$9;P8zf<1pnDG_3cJUP# z*Dl+9_MKMmy}0-GfBocN{p#EC!8^}8kM0N|d`<-(eBdPy-Fo51omcP4QFqRxJiK+T z-TL3{gRlPc^ZNgHedf%a0PfRQX}|2^p4;bl2FdyT+8_PF+fRLCUbqwU`o&-W?DPLC zF23&e5qDpF@ny!#oNxG{*M85%Z@yeCw>nK%f9O@O`N4<2>kZ#`>zQXApL) z_S5x~Z@a2wTe_Zc3Jpa83Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+fC3bt00k&O0SZun z0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+fC3bt00k&O0SZun0u-PC1t>rP z3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+fC3bt00k&O0SZun0u-PC1t>rP3Q&Lo6rcbF zC_n)UP=Epypa2CZKmiI+fC3bt00k&O0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epy zpa2CZKmiI+fC3bt00k&O0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+ zfC3bt00k&O0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+fC3bt00k&O z0SZun0u-PC1t>rP3Q&Lo6rcbFC_n)UP=Epypa2CZKmiI+fC3bt00k&O0SZun0u-PC z1t>rP3Q&Lo6rcbFC_n)UP=Epypuqpf4jl*p00000`M)+of&&K*95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^` zz<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!K zaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB) z95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c z2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*= zfddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede z;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQq zIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n? z4jede;J|?c2M!!KaNxj!0|yQqIB?*=fddB)95`^`z<~n?4jede;J|?c2M!!KaNxj! z0|yQqIB?(qcIuudiXaRC@Z6msBE|Vpkl1K!D2*Y84lPJ16*aLl1WvgExWFZHHe&qv z4OG@vMq}eoFeVmuCjJ3RN;=$`ot1-d(FCb@w%D1S?|EkSB|v}x0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNA}N(GvsnRY(4 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 z-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$ zfCC)h00%h00S<70103K02RJZg2mbP@6Wey(@X*{U2R$$c9N+*4IKTl8aDW3G-~b0W zzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h z00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70 z103K02ROh14sd`29N+*4IKTl8aDW4A&4GVB`t;tF(;u5z<)8=VfCC)h00%h00S<70 z103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh1 z4sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4 zIKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G z-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$ zfCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h0 z0S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K0 z2ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02UgpGuO7`g zv&td7(E~lu19QNE+75iJ-Pf)8Im$r}abME|Joot#Z%+1t#Z%X zIKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G z-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$ zfCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h0 z0S<70103K02ROh14sd`29N+*4IKTl8aDW3Gc%=jP9pAHczI*S?DhE9<2OQu42ROh1 z4sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4 zIKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G z-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$ zfCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h0 z0S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K0 z2ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`2 z9N+*4IKY9$95{G9|GZTW;i(?zfgYFx4%Bww)9t=)&CgK|a)|qy9_WD{n1iYg*q7Db zm-+mBe$Lm<*Bsyg2ROh14sd`29GIp9Ri87s?(Cx+OAf|&VjY*z%OpuaO&jd2j*5e=z%%l00%h00S<70103K02RJYu2QF{tb!*P09OMu_ z>46^TfjOw^fPJ=owte<^_SxpqJeo)I$N>&;fCC)h00%hGIZ*XEgX>Pm z56nU5KzDz)+V9)%+wXIL101ODKzDz)+V9)%+wXIL103K02ROh14sd`29N+*4IKTl8 zaDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0W zzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$V09h% z>b^~PTyg2XnN<#YU=BFI0S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$ zfCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h0 z0S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K0 z2ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`2 z9N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8 zaDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0W zzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h z00%h00S<70103K02ROh14sd`29N+*4IKTl8aNvh^;HPi?%5#^0=rePx9Q42(aDW3G z-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$ zfCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h0 z0S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S;7k;EnCy zX|<+fa*#uKt_OOc2j-xv0~fXXx-}h>gB-$hJ>Py=70kn-~b0WzyS_$ zfCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h0 z0S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K0 z2ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`2 z9N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8 zaDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0W zzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h z00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b24>cDsI+J0>3fkShv9Q42( zaDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0W zzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h z00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70 z103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh1 z4sd`29N+*4IKTl8aDW3G-~b0Wz=0p#fuFhO+7~al>fV`E4tih?IKTl8aDW3G-~b0W zzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h z00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70 z103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh1 z4sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4 zIKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G z-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$ zfCC)h00%h00S<70103K02ROh14sc*<4t(?QMW6rDm+zlj<)8=VfCC)h00%h00S<70 z103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh1 z4sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4 zIKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aA0T$ zzR~{ub!&Pg2RVexdY}h-U=BFI0S<70103K02ROh14sd`29N+*4`f}h~?K!+PU6F$v z!dE@e13fSY9N+*4IKTl8aDW3G-~b0WzyS_$fCGIw(AV>PuahpxK@Q=w9_WD{n1i7l zc>9jW&pWnm|I8`}Jun9x-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4 zIKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G z-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$ zfCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h0 z0S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K0 z2ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`2 z9N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROiibl@M3KD~G4^uf7R z4tih?IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8 zaDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0W zzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h z00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70 z103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70100xw16yx= z`QqE2-#@d;K@ZFU2ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70 z103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh1 z4sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4 zIKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G z-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$ zfCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h0 z0S<70103K02ROh14sd`29N+*4IKTl8a9}Y9zIbBWt{dKbaBh`@9+(3TaDW3G-~b0W zzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h z00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70 z103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROii zp&fXt{rl_I^hgeJ2$%Ih5A?tsaDW3G-~b0WzyS^n?Z98P^SU)Xl7k$=Wj)XXJun9x z-~b0WzyS_$fCC)h00%h00S<701ARF#^!EVXKRuI!9Kv-y&;va%2OQu42ROh14sd`2 z9N+*4IKTl8aA0~4^z}U7>!c5IkV81B2YR3f=3poXZocxSjh7ucFtf@*56l4vIKTl8 zaDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0W zzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h z00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70 z103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh1 z4sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4 zIKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8a9|h* z?mM%x?a+1i&8>3K19QLu4sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70 z103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh1 z4sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4 zIKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G z-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$ zfCC)h00%h00S<70103K02ROh14sd`29N@reIB@5izIe^I?)>!3DhE9<2OQu42ROh1 z4sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4 zIKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G z-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$ zfCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h0 z0S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K0 z2ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h0fm#my>6^ZHy>qJ^{@-ss zFb~)0fjMwLa6jMx2ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70 z103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh1 z4sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4 zIKTl8aDW3G-~b0Wz=0th_>=bUOVCv^J5M=J zIZttb0~~m@1HakM>(-nPImjWL&;vct19QLu4sd`29N+*4I54yWRlh%Q-RY+st<)&hwzBVfjQs+2ROh14sd`29N+*4IKTl8aDW3G z-~b0WzyS_$fCC&jmjj!2JbvD>b^GU5Ip~2o-~b0WzyS_$fCC)h00%h00S<70103K0 z2ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`2 z9N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8 zaDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`292nYxjqTrGx28vOkVCkv z2YR3f=AfzrL+>lRzw?drjq?o$IKYAa9JsQb*R45Ma*#v#q6d1Q2j-xv1NO=G$@a

GRStS!4miL84sd`29N+*4 zIKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G z-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$ zfCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h0 z0S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K0 z2ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC(uf&&-dbM1>4Ty^iWO*U|ww$RRw`13l0Kb5PxZ|GI1Yv7HAF&8%|J19QLu4sd`29N+*4 zIKTl8aDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G z-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$ zfCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h00%h0 z0S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b1v>%f2CmHoELA?92U^gs{H0S9V3 zaHieYt@%01K@M?W(*r%w19MQ-0ng*oFFD8|9M=Oq&;xV80S<70103K02RJYm2mbfS zOV3_^%ctg6Ip~2o-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8 zaDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0W zzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h z00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G=*xj0dm`t_Du-}I5A;9}%mD{D zzyS_$fCC)h00+k5Kwsy6ualpr9OMx9H$Bh;JunADIq*M6a{jDx2#54Q5A?tsaG=LTk34VSz&IT6`-uD;D0S<70103K02ROiiS36MkIfLuYd6t75!Zkh613fSYoddtnzF)Vd3v!S{ z_^1bZpa4_ZV5U%Qh9_WEN z-~b0WzyS_$fCC&D+JQyS`~G}7CI>l$=X#(AdSDKQc3|l5_q~5QC{9e1STXUY}Act^G5A;9}%mD{DzyS_$fCC)h00%h0 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 z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FoHzU>Yys z90(2^O28bL19M;xx^^Jj>qvkA0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7csf#m|zcnRk~aNtk^=D-}71AEZ51KD0j0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PE*=FpVGW90(2^O28bL z19M;xbU+7mKnHX{2XsIOw(dZ-(2)QE0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk+X+nLM>_|C1BVhY2j;*W*n_Se$o4uCAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV6Tbz%*XMIS?E; zlz=%f2j;*YbnQU4*O34L0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0?P%a@e_OKKWP2S65FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FoHzU>Yys90(2^O28bL19M;xx^^Jj>qvkA0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5;&?F6Rrqn!i6fkO$H19M;w z?12vGfDY(@4(Nam=)l$;$QC*hAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyKwvw8Y5ZvCKyctt0_MORm;-yzwFB8+M*;*05FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXFEEkx@OE?FD1BVhY z2j;*W*n_Se$o4uCAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV6Tbz%*XMIS?E;lz=%f2j;*YbnQU4*O34L0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0?P%a@e_OKKWP2S65FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAh4alG=8*mAUJR+0drsu%z-`7 z0Ugi*9nb+C&;cFTx&zrlM*;*05FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5(bCoqj4?HmXW97@0(m;-ZQ54v_B+v`Yx009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+0D(`zL_fB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs 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 z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pkKU`pNTj&4; z2M$97%z-&D2hPEd^&S{Fy;cGQ2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72>ft?!EK=f5F9uR5ikelz#KRSKh}F-;PhGv5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk 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

m&8q`dod! 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 literal 0 HcmV?d00001 diff --git a/examples/2.phantoms/brain_motion.phantom b/examples/2.phantoms/brain_motion.phantom index c3412d7f0512fb9577809ae51710eb9eea2043f8..89f67f4df813b80cbaf0f7203f953d3206284f68 100644 GIT binary patch delta 6096 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 Vo*m8`N!knRb~wKb>+dB+{}0KlHC6xs diff --git a/examples/2.phantoms/contracting_ring.phantom b/examples/2.phantoms/contracting_ring.phantom index 9934c78bc741206c60db0332ceb8e49f658fad66..0bdabaf095df36d7ff159c2090b764c04d69b18f 100644 GIT binary patch delta 1359 zcmaiyO=uHA7>0LevOhE1x~atyEHo*-lm;Odgq}<)6biirz2u-YK>~^NC)z+O;vtB{ zOQAKE2^9}|F(*xsk{mqb*pu3W7;TGzYK<+et+sB&lDfMy+cs8k53|X9@4VmpJv)V} zH@obe>IB)%TvrVOkNl#`_6)$xj+9yy&ons%z!{IvT=gZ@j<3TBnIn^e=!CUL3*@;V z=I%=}s&G{5xsy=x35AbpsV*FWB?;7N6Db#zQ72$xvW-BqaI>_}Q|q36ST85VJ){tb z!D;B=0f}MYLJ^Uk?$0H>02FOx7J(pnf7PQKlMxbDH>6Z69*5Oksjirq z&N`A64DuBWgIcl|+w#3w(0^r+n9m?7sCC#my9uG^n`FK{42iWru|=%|8>rse7#J3| zB{G1-R@k{InehsrlzN#l=n0q=0a@5XEmDNGA#H*<1RYC_$Hfi8>qI~><;~)&2jiJ7 ziL42>Hgs>7CX5+)pnpdqw{{zE&vM?Lak4{6EOp>4|D(B?k z(Rg%pWbkZdhx!;wk#m>_csp;^yGvziVic14e%{WbfJR)rEZ>dcxtOB=@&AHya$ZyN z^IAhtBV|poCHr)`eOS1xKQ&X z@R?yfe9vZm46A+V3*E1N{SW*IGobKG8%N=1naMHuv32VH7yLBaF6b`$jdSBe_UuYCZ9NwLs+uhmAF=rEEs^G27gNIUyk;1AEIV-|Gw6KVv6xt+6;I70bF;`9B zE)=|g3JGgFdGy}$q&bRSszURm*N0Z~P`vXG3Q9?>(rbI!z5Hp;*_nx6+SG+zc4l_I z-}imLZ+7}o*G#QzJPxdt{LWPcJVD>>gqML5l??ZR`u@I429Sa)!Jcm(vIHfW4%@_3 zREn}UE6gXH{Ad?Y#*jpo@8NIIREWcLkh!`%1jfSddCgRAJ^kLjqUFitL)@gF?(P4w zZ#ZsD7x+UJUGzy@Zc`7sM9lOaeBJjLDQei^r-nt;eKPmk-6 zc^YmrIC2Lqb>lhSr5_#{>`$FYrQO|nC-W3>I#aSrF8qSqQ3 zg$n09^`w8+)2?~?ai2szO>bUPHz-tF8&XZuGw>!eIZ~`@O@iul4RK2=!XM5xxq?_M zEO2zs2;%LLBvqUZIVvkhJBOtu&XdmaGh?sviH!{_`r!r^>kPC0LFA`0ZERB5l3|T~ zvdUSQ;Ld0zv=NJ+FP@pCMc*D9`_Bk@RMpy1r5a(D+9X%i_JcZ_$hBvCUZO(JOM^O^ z@Dx2e^DJDLdDhh97cw;UGzc`=aX-?)TqL_1dEL!$X@LVX;UyevHE5lnmBID#Ksq(> z#Yd^(v>SzOsOSk2Ok!ps!hbA|kJ;A=@?#nAK*hiKu*kj|^!QiW_}^@r{7}a4W*r)2 zrw(lzWHUczg5Yl6BziGb;Q$WZN)Of5%?-_i>D0--IOjqBDB<7TcVH`WmiP{yfSQK` zeaYS7J>7O+@2T{MeZ6T*=%h*D|4W(#qqFG-B2LJ#7 From 2a9aac7880a266d4d525537f043325f4874a4821 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Fri, 30 Aug 2024 12:39:47 +0200 Subject: [PATCH 62/91] Test ArbitraryMotion with all vendors again --- .buildkite/runtests.yml | 246 ++++++++++++++++++++-------------------- 1 file changed, 123 insertions(+), 123 deletions(-) diff --git a/.buildkite/runtests.yml b/.buildkite/runtests.yml index f16e82d65..b88fd8163 100644 --- a/.buildkite/runtests.yml +++ b/.buildkite/runtests.yml @@ -1,139 +1,139 @@ steps: - group: ":julia: ($TEST_GROUP) Tests" steps: - # - label: "CPU: Run tests on v{{matrix.version}}" - # matrix: - # setup: - # version: - # - "1.9" - # - "1" - # plugins: - # - JuliaCI/julia#v1: - # version: "{{matrix.version}}" - # - JuliaCI/julia-coverage#v1: - # codecov: true - # dirs: - # - KomaMRICore/src - # - KomaMRICore/ext - # env: - # TEST_GROUP: $TEST_GROUP - # command: | - # julia -e 'println("--- :julia: Instantiating project") - # using Pkg - # Pkg.develop([ - # PackageSpec(path=pwd(), subdir="KomaMRIBase"), - # PackageSpec(path=pwd(), subdir="KomaMRICore"), - # ])' + - label: "CPU: Run tests on v{{matrix.version}}" + matrix: + setup: + version: + - "1.9" + - "1" + plugins: + - JuliaCI/julia#v1: + version: "{{matrix.version}}" + - JuliaCI/julia-coverage#v1: + codecov: true + dirs: + - KomaMRICore/src + - KomaMRICore/ext + env: + TEST_GROUP: $TEST_GROUP + command: | + julia -e 'println("--- :julia: Instantiating project") + using Pkg + Pkg.develop([ + PackageSpec(path=pwd(), subdir="KomaMRIBase"), + PackageSpec(path=pwd(), subdir="KomaMRICore"), + ])' - # julia -e 'println("--- :julia: Running tests") - # using Pkg - # Pkg.test("KomaMRICore"; coverage=true, julia_args=`--threads=auto`)' - # agents: - # queue: "juliagpu" - # timeout_in_minutes: 60 + julia -e 'println("--- :julia: Running tests") + using Pkg + Pkg.test("KomaMRICore"; coverage=true, julia_args=`--threads=auto`)' + agents: + queue: "juliagpu" + timeout_in_minutes: 60 - # - label: "AMDGPU: Run tests on v{{matrix.version}}" - # matrix: - # setup: - # version: - # - "1" - # plugins: - # - JuliaCI/julia#v1: - # version: "{{matrix.version}}" - # - JuliaCI/julia-coverage#v1: - # codecov: true - # dirs: - # - KomaMRICore/src - # - KomaMRICore/ext - # env: - # TEST_GROUP: $TEST_GROUP - # command: | - # julia -e 'println("--- :julia: Instantiating project") - # using Pkg - # Pkg.develop([ - # PackageSpec(path=pwd(), subdir="KomaMRIBase"), - # PackageSpec(path=pwd(), subdir="KomaMRICore"), - # ])' + - label: "AMDGPU: Run tests on v{{matrix.version}}" + matrix: + setup: + version: + - "1" + plugins: + - JuliaCI/julia#v1: + version: "{{matrix.version}}" + - JuliaCI/julia-coverage#v1: + codecov: true + dirs: + - KomaMRICore/src + - KomaMRICore/ext + env: + TEST_GROUP: $TEST_GROUP + command: | + julia -e 'println("--- :julia: Instantiating project") + using Pkg + Pkg.develop([ + PackageSpec(path=pwd(), subdir="KomaMRIBase"), + PackageSpec(path=pwd(), subdir="KomaMRICore"), + ])' - # julia --project=KomaMRICore/test -e 'println("--- :julia: Add AMDGPU to test environment") - # using Pkg - # Pkg.add("AMDGPU")' + julia --project=KomaMRICore/test -e 'println("--- :julia: Add AMDGPU to test environment") + using Pkg + Pkg.add("AMDGPU")' - # julia -e 'println("--- :julia: Running tests") - # using Pkg - # Pkg.test("KomaMRICore"; coverage=true, test_args=["AMDGPU", ])' - # agents: - # queue: "juliagpu" - # rocm: "*" - # timeout_in_minutes: 60 + julia -e 'println("--- :julia: Running tests") + using Pkg + Pkg.test("KomaMRICore"; coverage=true, test_args=["AMDGPU", ])' + agents: + queue: "juliagpu" + rocm: "*" + timeout_in_minutes: 60 - # - label: "CUDA: Run tests on v{{matrix.version}}" - # matrix: - # setup: - # version: - # - "1.9" - # - "1" - # plugins: - # - JuliaCI/julia#v1: - # version: "{{matrix.version}}" - # - JuliaCI/julia-coverage#v1: - # codecov: true - # dirs: - # - KomaMRICore/src - # - KomaMRICore/ext - # env: - # TEST_GROUP: $TEST_GROUP - # command: | - # julia -e 'println("--- :julia: Instantiating project") - # using Pkg - # Pkg.develop([ - # PackageSpec(path=pwd(), subdir="KomaMRIBase"), - # PackageSpec(path=pwd(), subdir="KomaMRICore"), - # ])' + - label: "CUDA: Run tests on v{{matrix.version}}" + matrix: + setup: + version: + - "1.9" + - "1" + plugins: + - JuliaCI/julia#v1: + version: "{{matrix.version}}" + - JuliaCI/julia-coverage#v1: + codecov: true + dirs: + - KomaMRICore/src + - KomaMRICore/ext + env: + TEST_GROUP: $TEST_GROUP + command: | + julia -e 'println("--- :julia: Instantiating project") + using Pkg + Pkg.develop([ + PackageSpec(path=pwd(), subdir="KomaMRIBase"), + PackageSpec(path=pwd(), subdir="KomaMRICore"), + ])' - # julia --project=KomaMRICore/test -e 'println("--- :julia: Add CUDA to test environment") - # using Pkg - # Pkg.add("CUDA")' + julia --project=KomaMRICore/test -e 'println("--- :julia: Add CUDA to test environment") + using Pkg + Pkg.add("CUDA")' - # julia -e 'println("--- :julia: Running tests") - # using Pkg - # Pkg.test("KomaMRICore"; coverage=true, test_args=["CUDA"])' - # agents: - # queue: "juliagpu" - # cuda: "*" - # timeout_in_minutes: 60 + julia -e 'println("--- :julia: Running tests") + using Pkg + Pkg.test("KomaMRICore"; coverage=true, test_args=["CUDA"])' + agents: + queue: "juliagpu" + cuda: "*" + timeout_in_minutes: 60 - # - label: "Metal: Run tests on v{{matrix.version}}" - # matrix: - # setup: - # version: - # - "1.9" - # - "1" - # plugins: - # - JuliaCI/julia#v1: - # version: "{{matrix.version}}" - # env: - # TEST_GROUP: $TEST_GROUP - # command: | - # julia -e 'println("--- :julia: Instantiating project") - # using Pkg - # Pkg.develop([ - # PackageSpec(path=pwd(), subdir="KomaMRIBase"), - # PackageSpec(path=pwd(), subdir="KomaMRICore"), - # ])' + - label: "Metal: Run tests on v{{matrix.version}}" + matrix: + setup: + version: + - "1.9" + - "1" + plugins: + - JuliaCI/julia#v1: + version: "{{matrix.version}}" + env: + TEST_GROUP: $TEST_GROUP + command: | + julia -e 'println("--- :julia: Instantiating project") + using Pkg + Pkg.develop([ + PackageSpec(path=pwd(), subdir="KomaMRIBase"), + PackageSpec(path=pwd(), subdir="KomaMRICore"), + ])' - # julia --project=KomaMRICore/test -e 'println("--- :julia: Add Metal to test environment") - # using Pkg - # Pkg.add("Metal")' + julia --project=KomaMRICore/test -e 'println("--- :julia: Add Metal to test environment") + using Pkg + Pkg.add("Metal")' - # julia -e 'println("--- :julia: Running tests") - # using Pkg - # Pkg.test("KomaMRICore"; test_args=["Metal"])' - # agents: - # queue: "juliaecosystem" - # os: "macos" - # arch: "aarch64" - # timeout_in_minutes: 60 + julia -e 'println("--- :julia: Running tests") + using Pkg + Pkg.test("KomaMRICore"; test_args=["Metal"])' + agents: + queue: "juliaecosystem" + os: "macos" + arch: "aarch64" + timeout_in_minutes: 60 - label: "oneAPI: Run tests on v{{matrix.version}}" matrix: From d7ea6b71e8d45c3928dda73c142f6a6aba9af260 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Fri, 30 Aug 2024 12:58:51 +0200 Subject: [PATCH 63/91] Trying to find out where the error produces for oneAPI --- .buildkite/runtests.yml | 246 +++++++++++++++++------------------ KomaMRICore/test/runtests.jl | 8 +- 2 files changed, 128 insertions(+), 126 deletions(-) diff --git a/.buildkite/runtests.yml b/.buildkite/runtests.yml index b88fd8163..f16e82d65 100644 --- a/.buildkite/runtests.yml +++ b/.buildkite/runtests.yml @@ -1,139 +1,139 @@ steps: - group: ":julia: ($TEST_GROUP) Tests" steps: - - label: "CPU: Run tests on v{{matrix.version}}" - matrix: - setup: - version: - - "1.9" - - "1" - plugins: - - JuliaCI/julia#v1: - version: "{{matrix.version}}" - - JuliaCI/julia-coverage#v1: - codecov: true - dirs: - - KomaMRICore/src - - KomaMRICore/ext - env: - TEST_GROUP: $TEST_GROUP - command: | - julia -e 'println("--- :julia: Instantiating project") - using Pkg - Pkg.develop([ - PackageSpec(path=pwd(), subdir="KomaMRIBase"), - PackageSpec(path=pwd(), subdir="KomaMRICore"), - ])' + # - label: "CPU: Run tests on v{{matrix.version}}" + # matrix: + # setup: + # version: + # - "1.9" + # - "1" + # plugins: + # - JuliaCI/julia#v1: + # version: "{{matrix.version}}" + # - JuliaCI/julia-coverage#v1: + # codecov: true + # dirs: + # - KomaMRICore/src + # - KomaMRICore/ext + # env: + # TEST_GROUP: $TEST_GROUP + # command: | + # julia -e 'println("--- :julia: Instantiating project") + # using Pkg + # Pkg.develop([ + # PackageSpec(path=pwd(), subdir="KomaMRIBase"), + # PackageSpec(path=pwd(), subdir="KomaMRICore"), + # ])' - julia -e 'println("--- :julia: Running tests") - using Pkg - Pkg.test("KomaMRICore"; coverage=true, julia_args=`--threads=auto`)' - agents: - queue: "juliagpu" - timeout_in_minutes: 60 + # julia -e 'println("--- :julia: Running tests") + # using Pkg + # Pkg.test("KomaMRICore"; coverage=true, julia_args=`--threads=auto`)' + # agents: + # queue: "juliagpu" + # timeout_in_minutes: 60 - - label: "AMDGPU: Run tests on v{{matrix.version}}" - matrix: - setup: - version: - - "1" - plugins: - - JuliaCI/julia#v1: - version: "{{matrix.version}}" - - JuliaCI/julia-coverage#v1: - codecov: true - dirs: - - KomaMRICore/src - - KomaMRICore/ext - env: - TEST_GROUP: $TEST_GROUP - command: | - julia -e 'println("--- :julia: Instantiating project") - using Pkg - Pkg.develop([ - PackageSpec(path=pwd(), subdir="KomaMRIBase"), - PackageSpec(path=pwd(), subdir="KomaMRICore"), - ])' + # - label: "AMDGPU: Run tests on v{{matrix.version}}" + # matrix: + # setup: + # version: + # - "1" + # plugins: + # - JuliaCI/julia#v1: + # version: "{{matrix.version}}" + # - JuliaCI/julia-coverage#v1: + # codecov: true + # dirs: + # - KomaMRICore/src + # - KomaMRICore/ext + # env: + # TEST_GROUP: $TEST_GROUP + # command: | + # julia -e 'println("--- :julia: Instantiating project") + # using Pkg + # Pkg.develop([ + # PackageSpec(path=pwd(), subdir="KomaMRIBase"), + # PackageSpec(path=pwd(), subdir="KomaMRICore"), + # ])' - julia --project=KomaMRICore/test -e 'println("--- :julia: Add AMDGPU to test environment") - using Pkg - Pkg.add("AMDGPU")' + # julia --project=KomaMRICore/test -e 'println("--- :julia: Add AMDGPU to test environment") + # using Pkg + # Pkg.add("AMDGPU")' - julia -e 'println("--- :julia: Running tests") - using Pkg - Pkg.test("KomaMRICore"; coverage=true, test_args=["AMDGPU", ])' - agents: - queue: "juliagpu" - rocm: "*" - timeout_in_minutes: 60 + # julia -e 'println("--- :julia: Running tests") + # using Pkg + # Pkg.test("KomaMRICore"; coverage=true, test_args=["AMDGPU", ])' + # agents: + # queue: "juliagpu" + # rocm: "*" + # timeout_in_minutes: 60 - - label: "CUDA: Run tests on v{{matrix.version}}" - matrix: - setup: - version: - - "1.9" - - "1" - plugins: - - JuliaCI/julia#v1: - version: "{{matrix.version}}" - - JuliaCI/julia-coverage#v1: - codecov: true - dirs: - - KomaMRICore/src - - KomaMRICore/ext - env: - TEST_GROUP: $TEST_GROUP - command: | - julia -e 'println("--- :julia: Instantiating project") - using Pkg - Pkg.develop([ - PackageSpec(path=pwd(), subdir="KomaMRIBase"), - PackageSpec(path=pwd(), subdir="KomaMRICore"), - ])' + # - label: "CUDA: Run tests on v{{matrix.version}}" + # matrix: + # setup: + # version: + # - "1.9" + # - "1" + # plugins: + # - JuliaCI/julia#v1: + # version: "{{matrix.version}}" + # - JuliaCI/julia-coverage#v1: + # codecov: true + # dirs: + # - KomaMRICore/src + # - KomaMRICore/ext + # env: + # TEST_GROUP: $TEST_GROUP + # command: | + # julia -e 'println("--- :julia: Instantiating project") + # using Pkg + # Pkg.develop([ + # PackageSpec(path=pwd(), subdir="KomaMRIBase"), + # PackageSpec(path=pwd(), subdir="KomaMRICore"), + # ])' - julia --project=KomaMRICore/test -e 'println("--- :julia: Add CUDA to test environment") - using Pkg - Pkg.add("CUDA")' + # julia --project=KomaMRICore/test -e 'println("--- :julia: Add CUDA to test environment") + # using Pkg + # Pkg.add("CUDA")' - julia -e 'println("--- :julia: Running tests") - using Pkg - Pkg.test("KomaMRICore"; coverage=true, test_args=["CUDA"])' - agents: - queue: "juliagpu" - cuda: "*" - timeout_in_minutes: 60 + # julia -e 'println("--- :julia: Running tests") + # using Pkg + # Pkg.test("KomaMRICore"; coverage=true, test_args=["CUDA"])' + # agents: + # queue: "juliagpu" + # cuda: "*" + # timeout_in_minutes: 60 - - label: "Metal: Run tests on v{{matrix.version}}" - matrix: - setup: - version: - - "1.9" - - "1" - plugins: - - JuliaCI/julia#v1: - version: "{{matrix.version}}" - env: - TEST_GROUP: $TEST_GROUP - command: | - julia -e 'println("--- :julia: Instantiating project") - using Pkg - Pkg.develop([ - PackageSpec(path=pwd(), subdir="KomaMRIBase"), - PackageSpec(path=pwd(), subdir="KomaMRICore"), - ])' + # - label: "Metal: Run tests on v{{matrix.version}}" + # matrix: + # setup: + # version: + # - "1.9" + # - "1" + # plugins: + # - JuliaCI/julia#v1: + # version: "{{matrix.version}}" + # env: + # TEST_GROUP: $TEST_GROUP + # command: | + # julia -e 'println("--- :julia: Instantiating project") + # using Pkg + # Pkg.develop([ + # PackageSpec(path=pwd(), subdir="KomaMRIBase"), + # PackageSpec(path=pwd(), subdir="KomaMRICore"), + # ])' - julia --project=KomaMRICore/test -e 'println("--- :julia: Add Metal to test environment") - using Pkg - Pkg.add("Metal")' + # julia --project=KomaMRICore/test -e 'println("--- :julia: Add Metal to test environment") + # using Pkg + # Pkg.add("Metal")' - julia -e 'println("--- :julia: Running tests") - using Pkg - Pkg.test("KomaMRICore"; test_args=["Metal"])' - agents: - queue: "juliaecosystem" - os: "macos" - arch: "aarch64" - timeout_in_minutes: 60 + # julia -e 'println("--- :julia: Running tests") + # using Pkg + # Pkg.test("KomaMRICore"; test_args=["Metal"])' + # agents: + # queue: "juliaecosystem" + # os: "macos" + # arch: "aarch64" + # timeout_in_minutes: 60 - label: "oneAPI: Run tests on v{{matrix.version}}" matrix: diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index 8c4b35515..938f29f44 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -484,9 +484,11 @@ end x, y, z = get_spin_coords(obj.motion, obj.x, obj.y, obj.z, t') - @test x ≈ obj.x .+ vx .* t' - @test y ≈ obj.y .+ vy .* t' - @test z ≈ obj.z .+ vz .* t' + # @test x ≈ obj.x .+ vx .* t' + # @test y ≈ obj.y .+ vy .* t' + # @test z ≈ obj.z .+ vz .* t' + + @test true # sim_params = Dict{String, Any}( # "gpu"=>USE_GPU, From 42c21d23c55357b2cefa4489e84cf64c9c6885ed Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Fri, 30 Aug 2024 13:06:57 +0200 Subject: [PATCH 64/91] Keep trying --- KomaMRICore/test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index 938f29f44..4eff45a29 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -484,7 +484,7 @@ end x, y, z = get_spin_coords(obj.motion, obj.x, obj.y, obj.z, t') - # @test x ≈ obj.x .+ vx .* t' + @test x ≈ obj.x .+ vx .* t' # @test y ≈ obj.y .+ vy .* t' # @test z ≈ obj.z .+ vz .* t' From 0210669ac4bc8aea4cc99687ba2e4da66c24de11 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Tue, 3 Sep 2024 14:15:55 +0200 Subject: [PATCH 65/91] Merge branch `new-motion-debug` into `new-motion` --- .buildkite/pipeline.yml | 58 +++--- .buildkite/runtests.yml | 246 ++++++++++++------------- KomaMRICore/Project.toml | 1 + KomaMRICore/ext/KomaoneAPIExt.jl | 16 +- KomaMRICore/src/simulation/Functors.jl | 16 +- 5 files changed, 178 insertions(+), 159 deletions(-) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 1b29e8731..25aada17f 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -1,10 +1,10 @@ steps: - # - label: ":pipeline: Upload NoMotion Tests" - # env: - # TEST_GROUP: "nomotion" - # command: buildkite-agent pipeline upload .buildkite/runtests.yml - # agents: - # queue: "juliagpu" + - label: ":pipeline: Upload NoMotion Tests" + env: + TEST_GROUP: "nomotion" + command: buildkite-agent pipeline upload .buildkite/runtests.yml + agents: + queue: "juliagpu" - label: ":pipeline: Upload Motion Tests" env: @@ -13,27 +13,27 @@ steps: agents: queue: "juliagpu" - # - label: ":pipeline: Launch Benchmarks" - # if: build.message !~ /skip benchmarks/ - # agents: - # queue: "juliagpu" - # plugins: - # - monorepo-diff#v1.0.1: - # diff: "git diff --name-only HEAD~1" - # interpolation: false - # watch: - # - path: - # - "KomaMRICore/src/**/*" - # - "KomaMRICore/ext/**/*" - # - "KomaMRICore/Project.toml" - # - "KomaMRIBase/src/**/*" - # - "KomaMRIBase/Project.toml" - # - "benchmarks/**/*" - # - ".buildkite/**/*" - # - ".github/workflows/Benchmark.yml" - # - "Project.toml" - # config: - # command: "buildkite-agent pipeline upload .buildkite/runbenchmarks.yml" - # agents: - # queue: "juliagpu" + - label: ":pipeline: Launch Benchmarks" + if: build.message !~ /skip benchmarks/ + agents: + queue: "juliagpu" + plugins: + - monorepo-diff#v1.0.1: + diff: "git diff --name-only HEAD~1" + interpolation: false + watch: + - path: + - "KomaMRICore/src/**/*" + - "KomaMRICore/ext/**/*" + - "KomaMRICore/Project.toml" + - "KomaMRIBase/src/**/*" + - "KomaMRIBase/Project.toml" + - "benchmarks/**/*" + - ".buildkite/**/*" + - ".github/workflows/Benchmark.yml" + - "Project.toml" + config: + command: "buildkite-agent pipeline upload .buildkite/runbenchmarks.yml" + agents: + queue: "juliagpu" diff --git a/.buildkite/runtests.yml b/.buildkite/runtests.yml index f16e82d65..b88fd8163 100644 --- a/.buildkite/runtests.yml +++ b/.buildkite/runtests.yml @@ -1,139 +1,139 @@ steps: - group: ":julia: ($TEST_GROUP) Tests" steps: - # - label: "CPU: Run tests on v{{matrix.version}}" - # matrix: - # setup: - # version: - # - "1.9" - # - "1" - # plugins: - # - JuliaCI/julia#v1: - # version: "{{matrix.version}}" - # - JuliaCI/julia-coverage#v1: - # codecov: true - # dirs: - # - KomaMRICore/src - # - KomaMRICore/ext - # env: - # TEST_GROUP: $TEST_GROUP - # command: | - # julia -e 'println("--- :julia: Instantiating project") - # using Pkg - # Pkg.develop([ - # PackageSpec(path=pwd(), subdir="KomaMRIBase"), - # PackageSpec(path=pwd(), subdir="KomaMRICore"), - # ])' + - label: "CPU: Run tests on v{{matrix.version}}" + matrix: + setup: + version: + - "1.9" + - "1" + plugins: + - JuliaCI/julia#v1: + version: "{{matrix.version}}" + - JuliaCI/julia-coverage#v1: + codecov: true + dirs: + - KomaMRICore/src + - KomaMRICore/ext + env: + TEST_GROUP: $TEST_GROUP + command: | + julia -e 'println("--- :julia: Instantiating project") + using Pkg + Pkg.develop([ + PackageSpec(path=pwd(), subdir="KomaMRIBase"), + PackageSpec(path=pwd(), subdir="KomaMRICore"), + ])' - # julia -e 'println("--- :julia: Running tests") - # using Pkg - # Pkg.test("KomaMRICore"; coverage=true, julia_args=`--threads=auto`)' - # agents: - # queue: "juliagpu" - # timeout_in_minutes: 60 + julia -e 'println("--- :julia: Running tests") + using Pkg + Pkg.test("KomaMRICore"; coverage=true, julia_args=`--threads=auto`)' + agents: + queue: "juliagpu" + timeout_in_minutes: 60 - # - label: "AMDGPU: Run tests on v{{matrix.version}}" - # matrix: - # setup: - # version: - # - "1" - # plugins: - # - JuliaCI/julia#v1: - # version: "{{matrix.version}}" - # - JuliaCI/julia-coverage#v1: - # codecov: true - # dirs: - # - KomaMRICore/src - # - KomaMRICore/ext - # env: - # TEST_GROUP: $TEST_GROUP - # command: | - # julia -e 'println("--- :julia: Instantiating project") - # using Pkg - # Pkg.develop([ - # PackageSpec(path=pwd(), subdir="KomaMRIBase"), - # PackageSpec(path=pwd(), subdir="KomaMRICore"), - # ])' + - label: "AMDGPU: Run tests on v{{matrix.version}}" + matrix: + setup: + version: + - "1" + plugins: + - JuliaCI/julia#v1: + version: "{{matrix.version}}" + - JuliaCI/julia-coverage#v1: + codecov: true + dirs: + - KomaMRICore/src + - KomaMRICore/ext + env: + TEST_GROUP: $TEST_GROUP + command: | + julia -e 'println("--- :julia: Instantiating project") + using Pkg + Pkg.develop([ + PackageSpec(path=pwd(), subdir="KomaMRIBase"), + PackageSpec(path=pwd(), subdir="KomaMRICore"), + ])' - # julia --project=KomaMRICore/test -e 'println("--- :julia: Add AMDGPU to test environment") - # using Pkg - # Pkg.add("AMDGPU")' + julia --project=KomaMRICore/test -e 'println("--- :julia: Add AMDGPU to test environment") + using Pkg + Pkg.add("AMDGPU")' - # julia -e 'println("--- :julia: Running tests") - # using Pkg - # Pkg.test("KomaMRICore"; coverage=true, test_args=["AMDGPU", ])' - # agents: - # queue: "juliagpu" - # rocm: "*" - # timeout_in_minutes: 60 + julia -e 'println("--- :julia: Running tests") + using Pkg + Pkg.test("KomaMRICore"; coverage=true, test_args=["AMDGPU", ])' + agents: + queue: "juliagpu" + rocm: "*" + timeout_in_minutes: 60 - # - label: "CUDA: Run tests on v{{matrix.version}}" - # matrix: - # setup: - # version: - # - "1.9" - # - "1" - # plugins: - # - JuliaCI/julia#v1: - # version: "{{matrix.version}}" - # - JuliaCI/julia-coverage#v1: - # codecov: true - # dirs: - # - KomaMRICore/src - # - KomaMRICore/ext - # env: - # TEST_GROUP: $TEST_GROUP - # command: | - # julia -e 'println("--- :julia: Instantiating project") - # using Pkg - # Pkg.develop([ - # PackageSpec(path=pwd(), subdir="KomaMRIBase"), - # PackageSpec(path=pwd(), subdir="KomaMRICore"), - # ])' + - label: "CUDA: Run tests on v{{matrix.version}}" + matrix: + setup: + version: + - "1.9" + - "1" + plugins: + - JuliaCI/julia#v1: + version: "{{matrix.version}}" + - JuliaCI/julia-coverage#v1: + codecov: true + dirs: + - KomaMRICore/src + - KomaMRICore/ext + env: + TEST_GROUP: $TEST_GROUP + command: | + julia -e 'println("--- :julia: Instantiating project") + using Pkg + Pkg.develop([ + PackageSpec(path=pwd(), subdir="KomaMRIBase"), + PackageSpec(path=pwd(), subdir="KomaMRICore"), + ])' - # julia --project=KomaMRICore/test -e 'println("--- :julia: Add CUDA to test environment") - # using Pkg - # Pkg.add("CUDA")' + julia --project=KomaMRICore/test -e 'println("--- :julia: Add CUDA to test environment") + using Pkg + Pkg.add("CUDA")' - # julia -e 'println("--- :julia: Running tests") - # using Pkg - # Pkg.test("KomaMRICore"; coverage=true, test_args=["CUDA"])' - # agents: - # queue: "juliagpu" - # cuda: "*" - # timeout_in_minutes: 60 + julia -e 'println("--- :julia: Running tests") + using Pkg + Pkg.test("KomaMRICore"; coverage=true, test_args=["CUDA"])' + agents: + queue: "juliagpu" + cuda: "*" + timeout_in_minutes: 60 - # - label: "Metal: Run tests on v{{matrix.version}}" - # matrix: - # setup: - # version: - # - "1.9" - # - "1" - # plugins: - # - JuliaCI/julia#v1: - # version: "{{matrix.version}}" - # env: - # TEST_GROUP: $TEST_GROUP - # command: | - # julia -e 'println("--- :julia: Instantiating project") - # using Pkg - # Pkg.develop([ - # PackageSpec(path=pwd(), subdir="KomaMRIBase"), - # PackageSpec(path=pwd(), subdir="KomaMRICore"), - # ])' + - label: "Metal: Run tests on v{{matrix.version}}" + matrix: + setup: + version: + - "1.9" + - "1" + plugins: + - JuliaCI/julia#v1: + version: "{{matrix.version}}" + env: + TEST_GROUP: $TEST_GROUP + command: | + julia -e 'println("--- :julia: Instantiating project") + using Pkg + Pkg.develop([ + PackageSpec(path=pwd(), subdir="KomaMRIBase"), + PackageSpec(path=pwd(), subdir="KomaMRICore"), + ])' - # julia --project=KomaMRICore/test -e 'println("--- :julia: Add Metal to test environment") - # using Pkg - # Pkg.add("Metal")' + julia --project=KomaMRICore/test -e 'println("--- :julia: Add Metal to test environment") + using Pkg + Pkg.add("Metal")' - # julia -e 'println("--- :julia: Running tests") - # using Pkg - # Pkg.test("KomaMRICore"; test_args=["Metal"])' - # agents: - # queue: "juliaecosystem" - # os: "macos" - # arch: "aarch64" - # timeout_in_minutes: 60 + julia -e 'println("--- :julia: Running tests") + using Pkg + Pkg.test("KomaMRICore"; test_args=["Metal"])' + agents: + queue: "juliaecosystem" + os: "macos" + arch: "aarch64" + timeout_in_minutes: 60 - label: "oneAPI: Run tests on v{{matrix.version}}" matrix: diff --git a/KomaMRICore/Project.toml b/KomaMRICore/Project.toml index 36afa8fb8..c915a6ab1 100644 --- a/KomaMRICore/Project.toml +++ b/KomaMRICore/Project.toml @@ -8,6 +8,7 @@ Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" Functors = "d9f16b24-f501-4c13-a1f2-28368ffc5196" KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" KomaMRIBase = "d0bc0b20-b151-4d03-b2a4-6ca51751cb9c" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" ThreadsX = "ac1d9e8a-700a-412c-b207-f0111f4b6c0d" diff --git a/KomaMRICore/ext/KomaoneAPIExt.jl b/KomaMRICore/ext/KomaoneAPIExt.jl index c58469421..0baa3e341 100644 --- a/KomaMRICore/ext/KomaoneAPIExt.jl +++ b/KomaMRICore/ext/KomaoneAPIExt.jl @@ -1,8 +1,9 @@ module KomaoneAPIExt using oneAPI -import KomaMRICore +import KomaMRICore, KomaMRIBase import Adapt +import LinearAlgebra KomaMRICore.name(::oneAPIBackend) = "oneAPI" KomaMRICore.isfunctional(::oneAPIBackend) = oneAPI.functional() @@ -61,4 +62,17 @@ function __init__() @warn "oneAPI does not support all array operations used by KomaMRI. GPU performance may be slower than expected" end +const AdjointOneArray{T, N, M} = LinearAlgebra.Adjoint{T, oneArray{T, N, M}} where {T<:Real, N, M} +## Extend KomaMRIBase.unit_time (until bug with oneAPI is solved) +function KomaMRIBase.unit_time(t::AdjointOneArray{T, N, M}, ts::KomaMRIBase.TimeRange{T}) where {T<:Real, N, M} + if ts.t_start == ts.t_end + return (t .>= ts.t_start) .* oneunit(T) + else + tmp = max.((t .- ts.t_start) ./ (ts.t_end - ts.t_start), zero(T)) + t = min.(tmp, oneunit(T)) + _ = t + return t + end +end + end \ No newline at end of file diff --git a/KomaMRICore/src/simulation/Functors.jl b/KomaMRICore/src/simulation/Functors.jl index 13c523fe5..753b0d3af 100644 --- a/KomaMRICore/src/simulation/Functors.jl +++ b/KomaMRICore/src/simulation/Functors.jl @@ -51,9 +51,9 @@ See also [`f32`](@ref) and [`f64`](@ref) to change element type only. x = gpu(x, CUDABackend()) ``` """ -gpu(x, backend::KA.GPU) = fmap(x -> adapt(backend, x), x; exclude=_isleaf) -adapt_storage(backend::KA.GPU, xs::MotionList) = MotionList(gpu.(xs.motions, Ref(backend))) -adapt_storage(backend::KA.GPU, xs::Motion) = Motion(gpu(xs.action, backend), gpu(xs.time, backend), xs.spins) +function gpu(x, backend::KA.GPU) + fmap(x -> adapt(backend, x), x; exclude=_isleaf) +end # To CPU """ @@ -76,9 +76,6 @@ adapt_storage(T::Type{<:Real}, xs::Real) = convert(T, xs) adapt_storage(T::Type{<:Real}, xs::AbstractArray{<:Real}) = convert.(T, xs) adapt_storage(T::Type{<:Real}, xs::AbstractArray{<:Complex}) = convert.(Complex{T}, xs) adapt_storage(T::Type{<:Real}, xs::AbstractArray{<:Bool}) = xs -adapt_storage(T::Type{<:Real}, xs::NoMotion) = NoMotion{T}() -adapt_storage(T::Type{<:Real}, xs::MotionList) = MotionList(paramtype.(T, xs.motions)) -adapt_storage(T::Type{<:Real}, xs::Motion) = Motion(paramtype(T, xs.action), paramtype(T, xs.time), xs.spins) """ f32(m) @@ -100,6 +97,13 @@ See also [`f32`](@ref). """ f64(m) = paramtype(Float64, m) +# Koma motion-related adapts +adapt_storage(backend::KA.GPU, xs::MotionList) = MotionList(gpu.(xs.motions, Ref(backend))) +adapt_storage(backend::KA.GPU, xs::Motion) = Motion(gpu(xs.action, backend), gpu(xs.time, backend), xs.spins) +adapt_storage(T::Type{<:Real}, xs::NoMotion) = NoMotion{T}() +adapt_storage(T::Type{<:Real}, xs::MotionList) = MotionList(paramtype.(T, xs.motions)) +adapt_storage(T::Type{<:Real}, xs::Motion) = Motion(paramtype(T, xs.action), paramtype(T, xs.time), xs.spins) + #The functor macro makes it easier to call a function in all the parameters # Phantom @functor Phantom From fb7a740f10ad7b13aad415a0ed18a5e037b64154 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Tue, 3 Sep 2024 16:08:12 +0200 Subject: [PATCH 66/91] Leave core tests as they were before debugging --- KomaMRICore/test/runtests.jl | 44 ++++++++++-------------------------- 1 file changed, 12 insertions(+), 32 deletions(-) diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index 4eff45a29..62ea3f5b8 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -421,7 +421,7 @@ end end # --------- Motion-related tests ------------- -@testitem "Bloch SimpleAction" tags=[:core] begin +@testitem "Bloch SimpleAction" tags=[:core, :motion] begin using Suppressor include("initialize_backend.jl") include(joinpath(@__DIR__, "test_files", "utils.jl")) @@ -438,11 +438,10 @@ end sig = @suppress simulate(obj, seq, sys; sim_params) sig = sig / prod(size(obj)) NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - println("NMRSE SimpleAction: ", NMRSE(sig, sig_jemris)) @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end -@testitem "BlochSimple SimpleAction" tags=[:core] begin +@testitem "BlochSimple SimpleAction" tags=[:core, :motion] begin using Suppressor include("initialize_backend.jl") include(joinpath(@__DIR__, "test_files", "utils.jl")) @@ -460,7 +459,6 @@ end sig = @suppress simulate(obj, seq, sys; sim_params) sig = sig / prod(size(obj)) NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - println("NMRSE SimpleAction BlochSimple: ", NMRSE(sig, sig_jemris)) @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end @@ -474,35 +472,18 @@ end sys = Scanner() obj = phantom_brain_arbitrary_motion() - vx = 0.0f0 - vy = 0.1f0 - vz = 0.0f0 - t = collect(0:0.1:10) - - obj = obj |> f32 |> gpu - t = t |> f32 |> gpu - - x, y, z = get_spin_coords(obj.motion, obj.x, obj.y, obj.z, t') - - @test x ≈ obj.x .+ vx .* t' - # @test y ≈ obj.y .+ vy .* t' - # @test z ≈ obj.z .+ vz .* t' - - @test true - - # sim_params = Dict{String, Any}( - # "gpu"=>USE_GPU, - # "sim_method"=>KomaMRICore.Bloch(), - # "return_type"=>"mat" - # ) - # sig = simulate(obj, seq, sys; sim_params) - # sig = sig / prod(size(obj)) - # NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - # println("NMRSE ArbitraryAction: ", NMRSE(sig, sig_jemris)) - # @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% + sim_params = Dict{String, Any}( + "gpu"=>USE_GPU, + "sim_method"=>KomaMRICore.Bloch(), + "return_type"=>"mat" + ) + sig = @suppress simulate(obj, seq, sys; sim_params) + sig = sig / prod(size(obj)) + NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end -@testitem "BlochSimple ArbitraryAction" tags=[:core] begin +@testitem "BlochSimple ArbitraryAction" tags=[:core, :motion] begin using Suppressor include("initialize_backend.jl") include(joinpath(@__DIR__, "test_files", "utils.jl")) @@ -520,7 +501,6 @@ end sig = simulate(obj, seq, sys; sim_params) sig = sig / prod(size(obj)) NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - println("NMRSE ArbitraryAction BlochSimple: ", NMRSE(sig, sig_jemris)) @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% end From 51c71cca2d12297ede3c722c8d03cb731d0460cc Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Tue, 3 Sep 2024 16:09:36 +0200 Subject: [PATCH 67/91] `_ = sum(t)` --- KomaMRICore/ext/KomaoneAPIExt.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KomaMRICore/ext/KomaoneAPIExt.jl b/KomaMRICore/ext/KomaoneAPIExt.jl index 0baa3e341..2ffdfa967 100644 --- a/KomaMRICore/ext/KomaoneAPIExt.jl +++ b/KomaMRICore/ext/KomaoneAPIExt.jl @@ -70,7 +70,7 @@ function KomaMRIBase.unit_time(t::AdjointOneArray{T, N, M}, ts::KomaMRIBase.Time else tmp = max.((t .- ts.t_start) ./ (ts.t_end - ts.t_start), zero(T)) t = min.(tmp, oneunit(T)) - _ = t + _ = sum(t) return t end end From 7d1ddc054b9d7ebef49e59e69fc19b748e0ec340 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Tue, 3 Sep 2024 19:56:33 +0200 Subject: [PATCH 68/91] New function `_unit_time` --- KomaMRIBase/src/motion/motionlist/TimeSpan.jl | 4 +++- KomaMRICore/ext/KomaoneAPIExt.jl | 11 +++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/KomaMRIBase/src/motion/motionlist/TimeSpan.jl b/KomaMRIBase/src/motion/motionlist/TimeSpan.jl index 46f482c33..f91621cdd 100644 --- a/KomaMRIBase/src/motion/motionlist/TimeSpan.jl +++ b/KomaMRIBase/src/motion/motionlist/TimeSpan.jl @@ -60,6 +60,9 @@ julia> t_unit = KomaMRIBase.unit_time([0.0, 1.0, 2.0, 3.0, 4.0, 5.0], TimeRange( ``` """ function unit_time(t::AbstractArray{T}, ts::TimeRange{T}) where {T<:Real} + return _unit_time(t, ts) +end +function _unit_time(t::AbstractArray{T}, ts::TimeRange{T}) where {T<:Real} if ts.t_start == ts.t_end return (t .>= ts.t_start) .* oneunit(T) else @@ -69,7 +72,6 @@ function unit_time(t::AbstractArray{T}, ts::TimeRange{T}) where {T<:Real} end - """ periodic = Periodic(period, asymmetry) diff --git a/KomaMRICore/ext/KomaoneAPIExt.jl b/KomaMRICore/ext/KomaoneAPIExt.jl index 2ffdfa967..8bbb9d6e1 100644 --- a/KomaMRICore/ext/KomaoneAPIExt.jl +++ b/KomaMRICore/ext/KomaoneAPIExt.jl @@ -65,14 +65,9 @@ end const AdjointOneArray{T, N, M} = LinearAlgebra.Adjoint{T, oneArray{T, N, M}} where {T<:Real, N, M} ## Extend KomaMRIBase.unit_time (until bug with oneAPI is solved) function KomaMRIBase.unit_time(t::AdjointOneArray{T, N, M}, ts::KomaMRIBase.TimeRange{T}) where {T<:Real, N, M} - if ts.t_start == ts.t_end - return (t .>= ts.t_start) .* oneunit(T) - else - tmp = max.((t .- ts.t_start) ./ (ts.t_end - ts.t_start), zero(T)) - t = min.(tmp, oneunit(T)) - _ = sum(t) - return t - end + t_unit = KomaMRIBase._unit_time(t, ts) + _ = sum(t_unit) + return t_unit end end \ No newline at end of file From f0707aa05da4e034642bfd2e098b97afa84d6f7d Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 12 Sep 2024 12:14:36 +0200 Subject: [PATCH 69/91] Squashed commit of the following: commit cc46bcd7520d5f612be1f2823af1c30897004165 Author: Pablo Villacorta Aylagas Date: Thu Sep 12 12:09:14 2024 +0200 Add test with BlochDict commit 84d4a74d9b1c7b7666c643419691106d566c9e52 Author: Pablo Villacorta Aylagas Date: Thu Sep 12 12:01:18 2024 +0200 Change issue reference number commit cd07c402d644a310a96be72e3483734be92f69fa Author: Pablo Villacorta Date: Thu Sep 12 09:57:24 2024 +0200 Add OrdinaryDiffEqTsit5 package to the core tests commit 3b941b45774df241eb032d27c6ca229437069c2c Author: Pablo Villacorta Date: Thu Sep 12 09:56:42 2024 +0200 Add @suppress before the for loop commit 7c1e3983efc74ee72d05c6d84bf3168b5895b639 Author: Pablo Villacorta Date: Thu Sep 12 01:15:37 2024 +0200 Solve bug with FlowPath (spin_reset is now AbstractArray{T}) commit 36c01749a39f0f331dcbfd9adcd6564b5d2ff670 Author: Pablo Villacorta Date: Wed Sep 11 18:04:43 2024 +0200 Activate tests for all vendors in runtests.yml commit 27d074f5b69da2d1ef0c705e122de3a56ee7840e Author: Pablo Villacorta Date: Wed Sep 11 18:03:37 2024 +0200 Update core tests for motion commit 014e547b147fabd2e70e8047b62cb810acd04b72 Author: Pablo Villacorta Date: Wed Sep 11 17:50:04 2024 +0200 Solve bug with `reset_magnetization!` commit 2679e3ec95141a70c0cdb850979f08185015aae5 Author: Pablo Villacorta Date: Wed Sep 11 17:01:07 2024 +0200 Remove type restriction in `resample` commit d9f0018e9956512df730ec1756dd6b1fd45340bb Author: Pablo Villacorta Date: Wed Sep 11 16:11:38 2024 +0200 Add some type restrictions removed before commit fa1944d12019cbbd22352ab3c5caa20e186aa958 Author: Pablo Villacorta Date: Wed Sep 11 16:04:44 2024 +0200 Remove more type restrictions commit c416bc48f7b670318d9e083f60a6a89ffba8b831 Author: Pablo Villacorta Date: Wed Sep 11 16:04:28 2024 +0200 Temporary change `sind` and `cosd` for `sin` and `cos` commit 27ae31e7e45b3ae527b18688ee9d32b6c373c729 Merge: d145dc70 4862b405 Author: Pablo Villacorta Date: Wed Sep 11 13:28:03 2024 +0200 Merge branch 'new-motion-debug' of https://github.com/pvillacorta/KomaMRI.jl into new-motion-debug commit d145dc707366d8066e87a9751331f215079edf25 Author: Pablo Villacorta Date: Wed Sep 11 13:23:04 2024 +0200 workgroupsize set to 256 commit 69fa0897ec50ae393239739279c08df831363e89 Author: Pablo Villacorta Date: Wed Sep 11 13:22:50 2024 +0200 Remove `unit_time` extend commit 4aeef1843b6d652ae6cb47ca796ec269aac887dc Author: Pablo Villacorta Date: Wed Sep 11 13:22:02 2024 +0200 Remove type restrictions commit 170d3ab58d7e0a8b14e5df0e7f7f0f48850efb27 Author: Pablo Villacorta Aylagas Date: Wed Sep 4 12:08:09 2024 +0200 Timeout 30 minutes commit fad91b3c9c7d4d1f11722c4a12f6faac839a527f Author: Pablo Villacorta Aylagas Date: Wed Sep 4 11:50:37 2024 +0200 Timeout 15 minutes, 12 iterations commit 16c13ecd36faf4a8ad274409cd71c3f094f7a486 Author: Pablo Villacorta Aylagas Date: Wed Sep 4 11:27:24 2024 +0200 Print u and t inside resample commit 1aa52ab85490e00b0ed558cc5994cb006a6c2a8f Author: Pablo Villacorta Aylagas Date: Wed Sep 4 11:19:30 2024 +0200 Print time commit 14f48b164ac757b039096e7e748a09846569f026 Author: Pablo Villacorta Aylagas Date: Wed Sep 4 11:12:44 2024 +0200 Correct bug in `minimum` function commit 9de93c90e9fdbc35de283d8e0a3f3dedf789763a Author: Pablo Villacorta Aylagas Date: Wed Sep 4 11:08:34 2024 +0200 print only uy again commit b82351300cd61d6ff82f14b859c54ad5a5501cd1 Author: Pablo Villacorta Aylagas Date: Wed Sep 4 10:59:59 2024 +0200 Try to reproduce the error commit 23502d468ade297cc04959379f41ebe777292c07 Author: Pablo Villacorta Aylagas Date: Wed Sep 4 10:44:08 2024 +0200 Not print uy commit 7ce69f7a51ab1f52bd6447cdf820aef6c2069249 Author: Pablo Villacorta Aylagas Date: Wed Sep 4 10:35:47 2024 +0200 not print time commit 616f7ab94a0a5849e48bf98ff2c2c873fd5cf023 Author: Pablo Villacorta Aylagas Date: Wed Sep 4 10:26:13 2024 +0200 @view 1:3 commit c5e099d0a7350da119107d8d0226c1df7d328e31 Author: Pablo Villacorta Aylagas Date: Wed Sep 4 10:21:22 2024 +0200 More iterations, print bigger @view commit f77429c13f8cb036204c0ced9639cc7392bbcf61 Author: Pablo Villacorta Aylagas Date: Wed Sep 4 10:13:38 2024 +0200 Printing also interpolation result, not sure about what is the problem commit 5a410a148d3f322c1b4918ce263885713fa79d81 Author: Pablo Villacorta Aylagas Date: Wed Sep 4 10:06:02 2024 +0200 println inside `resample` commit 39becd2c96599b6b3f6144955b693286815877b2 Author: Pablo Villacorta Aylagas Date: Wed Sep 4 09:59:19 2024 +0200 Println inside `displacement_y!` commit c40444a315a28128cbdcfb96435f729eb39083c2 Author: Pablo Villacorta Aylagas Date: Wed Sep 4 09:44:06 2024 +0200 println where it was at the beggining commit 8b7797ee0f583b09f1511480c7f7f2942125beb3 Author: Pablo Villacorta Aylagas Date: Wed Sep 4 09:37:41 2024 +0200 println commit 2a76c452f03d8718264d4241d410e3fbb3392945 Author: Pablo Villacorta Aylagas Date: Wed Sep 4 09:31:41 2024 +0200 Try sum before calling `_unit_time` commit 3e5b84545010cb22da496bba70fcd588e4756e06 Author: Pablo Villacorta Aylagas Date: Wed Sep 4 09:24:52 2024 +0200 Try as it is in the main branch, as it is not working now commit 97048f063f3d89ad2969e497511349123199d83f Author: Pablo Villacorta Aylagas Date: Tue Sep 3 16:12:19 2024 +0200 `_ = sum(t)` commit 4120a197156a56ae17aa1e875c3bfadcc2707b69 Author: Pablo Villacorta Aylagas Date: Tue Sep 3 16:02:50 2024 +0200 `_ = copy(t)` commit 92e634f3d120a18b8358b8492d29856daeb48867 Author: Pablo Villacorta Aylagas Date: Tue Sep 3 14:17:05 2024 +0200 Make sure `_ = t` works commit 8655bd1155b6a3c2d7260418b52d249e298c8b1e Author: Pablo Villacorta Aylagas Date: Tue Sep 3 14:08:17 2024 +0200 Try one last time commit ee18d14dc9a047f7a87058bddf37ac8e537e8edb Author: Pablo Villacorta Aylagas Date: Tue Sep 3 14:06:12 2024 +0200 Reorganize Functors.jl commit 7b1e0d7587736ffd5e3f3398d7abba8b7866f596 Author: Pablo Villacorta Aylagas Date: Tue Sep 3 13:53:17 2024 +0200 Solve bug commit bcf0670c1bac3b21352ffe711f8bd80885bbfdf1 Author: Pablo Villacorta Aylagas Date: Tue Sep 3 13:47:33 2024 +0200 Loop and reset ux in every iteration by broadcasting commit 5c4c27dda8dd4263b70864f29ba6bcdc27660eba Author: Pablo Villacorta Aylagas Date: Tue Sep 3 13:37:54 2024 +0200 Pass results to cpu and try to recreate simulator's behaviour commit 33766e23d259ace862f16eeadf3b876d15c53bf0 Author: Pablo Villacorta Aylagas Date: Tue Sep 3 13:32:13 2024 +0200 Delete loop commit e199fb3a4e5b3212511b212b69e1605e34087326 Author: Pablo Villacorta Aylagas Date: Tue Sep 3 13:27:55 2024 +0200 @test true commit 613975427722fe87e70d5e4e45cadb9ec02647ee Author: Pablo Villacorta Aylagas Date: Tue Sep 3 13:23:14 2024 +0200 Print the comprobation instead of using @test commit 087c474fec463d0c29d7306e62fbc79f8f7032ec Author: Pablo Villacorta Aylagas Date: Tue Sep 3 13:16:56 2024 +0200 Try again to isolate the problem commit 67c788f5c195af5f81184ab70f5a2f272966e9ce Author: Pablo Villacorta Aylagas Date: Tue Sep 3 12:36:13 2024 +0200 Now the method is amiguous commit 5d235e224646e38dfd1007c0965b265adfff0189 Author: Pablo Villacorta Aylagas Date: Tue Sep 3 12:31:07 2024 +0200 KomaMRIBase.unit_time commit 61f222ea4c8861fc2a64fd683bddff240f17c3d1 Author: Pablo Villacorta Aylagas Date: Tue Sep 3 12:26:13 2024 +0200 Try to isolate `unit_time` commit 12c6a112617c4d87684e4ebff39727fc621a6ba4 Author: Pablo Villacorta Aylagas Date: Tue Sep 3 12:07:30 2024 +0200 Add synchronize now commit cea2d80039d41643f37d7b93c947f08c2a7df23d Author: Pablo Villacorta Aylagas Date: Tue Sep 3 11:58:57 2024 +0200 Try avoiding the use of `min` or `max` commit db0cc3fc4744c572ee98fe46519887e999998f8f Author: Pablo Villacorta Aylagas Date: Tue Sep 3 11:14:22 2024 +0200 Fix typo commit 590cf682602bfbf99af50b9061a5fde643a3934a Author: Pablo Villacorta Aylagas Date: Tue Sep 3 11:09:36 2024 +0200 Try another function definition using a mask commit 789f9075cf29904a674494d4ccb26db7f90d61d2 Author: Pablo Villacorta Aylagas Date: Tue Sep 3 11:00:00 2024 +0200 2 synchronize commit f6914b5097a6bed391025781436ce28d5b215009 Author: Pablo Villacorta Aylagas Date: Tue Sep 3 10:53:40 2024 +0200 Solve variable name conflict commit 28c3047661e380e5a60dbec610b4e9962f412e48 Author: Pablo Villacorta Aylagas Date: Tue Sep 3 10:48:08 2024 +0200 `synchronize` between `max.` and `min.` commit c71fee88b202b47aa4ab4da39d32c7177919aba0 Author: Pablo Villacorta Aylagas Date: Tue Sep 3 10:41:55 2024 +0200 KA.synchronize commit 62b4f7a8bcdd59c9d31f6ffa2ceb5cfe802f0060 Author: Pablo Villacorta Aylagas Date: Tue Sep 3 10:30:01 2024 +0200 Dummy sum again commit 6c9bfcb53d244e82b540907391816d2cbffc937e Author: Pablo Villacorta Aylagas Date: Tue Sep 3 01:13:21 2024 +0200 Fix typo commit ffe244eab277821d6b4e8bda6bd6d7fc17abbae9 Author: Pablo Villacorta Aylagas Date: Tue Sep 3 01:08:45 2024 +0200 synchronize with no arguments commit fbd3667dd2fdec8a074f401dc140bd66ab8c6935 Author: Pablo Villacorta Aylagas Date: Tue Sep 3 01:04:26 2024 +0200 oneAPI.synchronize commit a5efaf76d6b7244020e0c3539109d1cfa6e7d6ba Author: Pablo Villacorta Aylagas Date: Tue Sep 3 00:57:27 2024 +0200 Fix oneAPIBackend commit d724f21dd995be50f8cae23094808de0f4e861e7 Author: Pablo Villacorta Aylagas Date: Tue Sep 3 00:52:34 2024 +0200 Include previous definition commit 91a906ad78a2b27b48da80aa998f4366de025c86 Author: Pablo Villacorta Aylagas Date: Tue Sep 3 00:46:54 2024 +0200 Go back commit e7104b048ee4a802cb465e855a8ee3f7fe0daca7 Author: Pablo Villacorta Aylagas Date: Tue Sep 3 00:32:59 2024 +0200 Solve bug commit a96eef85bb482fce04cf5f6280f26d8167a845b3 Author: Pablo Villacorta Aylagas Date: Tue Sep 3 00:24:25 2024 +0200 add LinearAlgebra commit 670e6056913812c86450ed6ec71b681810d22666 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 23:58:33 2024 +0200 import LinearAlgebra commit af9332b4825b1e628a456fe24ef51567ac306b12 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 23:54:38 2024 +0200 Extend function to KomaMRICore commit 6d7f292609d0dd27821a4a99850e5e098c37d1e0 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 23:52:35 2024 +0200 Go back to `'`, define alias for oneArray adjoint commit 42225ca2807c94d9313bc2e8bd26e1e45bd5c74b Author: Pablo Villacorta Aylagas Date: Mon Sep 2 23:37:35 2024 +0200 Change `'` to `permutedims` commit 691903b1fe0f1d2fdcae1e6a7e54f843b39c116f Author: Pablo Villacorta Aylagas Date: Mon Sep 2 23:12:28 2024 +0200 Message to check we're inside the new function commit aac0704eaa4b6f07b95f352373d01d28b22c2588 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 23:04:54 2024 +0200 Return in `unit_time` commit 15126ca3a5f51dd8e406d0b40ec7ee257d2b2069 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 23:01:38 2024 +0200 Simplify commit 01fdff1b9b18a5f003a7edb84b14e6487724cec2 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 23:00:48 2024 +0200 Go back to `synchronize`, use `oneArray` commit ccb9066c6d9fcfe0b2004ce1ffa107eb3146cc2e Author: Pablo Villacorta Aylagas Date: Mon Sep 2 22:58:00 2024 +0200 Recover dummy sum, but now only in the `unit_time` extension oneAPI commit 79117cd3179d9b280e692dd4356f49442bce01a2 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 22:47:46 2024 +0200 Temp variable `t` commit 3910d54344e892dcd94ccc39827176a106d4f07e Author: Pablo Villacorta Aylagas Date: Mon Sep 2 22:40:29 2024 +0200 Fix missing import commit 8c15f9d415ed32833f254337586375ada3d39589 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 21:45:04 2024 +0200 Fix typo commit 23581efcd98c8a8ea7c6aca8d2d3a71f6e95083f Author: Pablo Villacorta Aylagas Date: Mon Sep 2 21:37:58 2024 +0200 Solve import problem commit bfe0f9a75f09a1461bd91c8d751d483dc2cc309e Author: Pablo Villacorta Aylagas Date: Mon Sep 2 21:16:17 2024 +0200 Solve bug commit 8a90cd2a3d27f53b5bddb62bacc5e2493d1b26a3 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 21:10:18 2024 +0200 Extend unit_time correctly commit 59417f0252e18e5a88f55ef6c57dd39ce5a3036f Author: Pablo Villacorta Aylagas Date: Mon Sep 2 21:01:25 2024 +0200 Include TimeSpan.jl before using KomaMRIBase commit 1bcff3dba0998a34a33bd384f2c9f3b1d725531a Author: Pablo Villacorta Aylagas Date: Mon Sep 2 20:53:39 2024 +0200 Delete file commit 2628e8a8c78feca75d4173ccd2d4db494e4556fb Merge: 7d337b96 e000c986 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 20:52:22 2024 +0200 Merge branch 'new-motion-debug' of https://github.com/JuliaHealth/KomaMRI.jl into new-motion-debug commit 7d337b96e538b7a17de98896afbfab43e981aea2 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 20:48:24 2024 +0200 Extend `unit_time` (2) commit 8e963e5e8aed0ade4a21ae0357dd2de00f2dba8c Author: Pablo Villacorta Aylagas Date: Mon Sep 2 20:47:31 2024 +0200 Extend `unit_time` commit e000c9866a9167480fecc46b2204fa64da28dccc Author: Pablo Villacorta Aylagas Date: Mon Sep 2 20:35:31 2024 +0200 KA.synchronize inside KomaMRICore commit bd83b01dc4c7dbb86750e9e38ffbe22dcae20b2c Author: Pablo Villacorta Aylagas Date: Mon Sep 2 20:26:40 2024 +0200 Try KA.synchronize commit 5f6abcb0269fc83fa5c998a1b46db3ae7c01d093 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 20:17:59 2024 +0200 Try .= commit 2cc4bfecee1f6fee5cc3cc5b74834f34d6fcb09d Author: Pablo Villacorta Aylagas Date: Mon Sep 2 20:11:20 2024 +0200 Remove dummy sum, only leave the aux `t` variable commit 8835924f01fcc2dcd18a5df48af7e449c2caea7c Author: Pablo Villacorta Aylagas Date: Mon Sep 2 19:59:56 2024 +0200 Try dummy sum inside unit_time instead of resample commit 527c951de5a923b316e215beb5b0197ef49df03f Author: Pablo Villacorta Aylagas Date: Mon Sep 2 19:43:09 2024 +0200 Try dummy sum again, as it worked before commit 7ea46a8dc059d9ca603be782ecab7b40a0c8ae66 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 19:36:26 2024 +0200 Recover ux buffers and try again with @cncastillo suggestion commit 4e82da8d16b033092a97e3356a8e648a4bdb2687 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 19:29:12 2024 +0200 Dummy `sum` commit 5ac223c3fae93dd56f9d1e88f96bdd5ee445574b Author: Pablo Villacorta Aylagas Date: Mon Sep 2 19:23:17 2024 +0200 println again, looks like is the only thing that works commit 61d19f77f10ff4a49933d42cc18c6f43846c15f0 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 19:16:55 2024 +0200 t .= t under copyto! commit f1e8c49f48eb016b34eb1cb73bfb6e0618dfa363 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 19:09:42 2024 +0200 Try t .= t commit d7490081ac076f6f5e6a86e319614242c425a908 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 18:51:34 2024 +0200 Re-assign dummy variable commit 8b4f43dc7070bde3c6b70747b78112f7d0a4caa6 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 18:46:24 2024 +0200 Dummy var commit ce16b57015e8bee4844bb0da4b29ecef4cfd066d Author: Pablo Villacorta Aylagas Date: Mon Sep 2 18:31:20 2024 +0200 `t_aux` inside `resample` commit 74b8b2d13adcaf3d8320eb766dcf246e33317bc7 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 17:52:58 2024 +0200 Print time inside `resample` function commit 8af933ad73f71ad52fc26a8833cb26330f934494 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 17:46:05 2024 +0200 initialize uy commit 446936bb3c544276f19a752ba90981e254c17b7d Author: Pablo Villacorta Aylagas Date: Mon Sep 2 17:39:14 2024 +0200 Aux variable, try again commit 0c1c6edd3a2cb49849b81d0f9e213db668807526 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 17:34:53 2024 +0200 Aux variable commit 900e2e20e0c9390a92c1e74cc7fc42781876441e Author: Pablo Villacorta Aylagas Date: Mon Sep 2 17:27:09 2024 +0200 id_tmp variable commit d9cd20120526c534d4ca8c3f4558366c5c5236a7 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 17:10:43 2024 +0200 Temp variable uy_s commit 000f1cd16f29ea5532240ec8d324363e8caa5596 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 16:50:39 2024 +0200 Display only uy, 8 iterations commit 1262f18c444f0b95e0d984c295a2c76a95191cf3 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 16:46:05 2024 +0200 Display time and uy, 8 iterations commit 6cba05d8331ac060ab69251cc1972f46b446cfb7 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 16:40:56 2024 +0200 Display time and uy, 8 iterations commit f8f0599d084f145e41135e597e878aa67f24564d Author: Pablo Villacorta Aylagas Date: Mon Sep 2 16:33:22 2024 +0200 Try 15 iterations with no display, store itp result in intermediate variable commit 00d7ea8a358e8f18cc0a4a26ec67183aa706bf80 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 14:15:45 2024 +0200 Try changing preallocations in `get_spin_coords` commit f77f59eb7b05ef0933aba245fe616e1f5e313d41 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 13:59:09 2024 +0200 Display time again commit 486d6afbc17a80f75b2769209b74028df74b921b Author: Pablo Villacorta Aylagas Date: Mon Sep 2 13:52:52 2024 +0200 Not display time, 8 iterations commit 54a44c7f37c87a484cb6925dfe92979ee4ac8ec7 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 13:46:19 2024 +0200 20 iterations commit faaa2e61f75ba9ab284ce825eade521c0cf27880 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 13:38:32 2024 +0200 10 iterations commit 75503738972fe0b6057f0b26e6c421c111b27a50 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 13:33:55 2024 +0200 Display time commit a1745da80962476ae42e5aebbdf1b72caa5368a5 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 13:27:56 2024 +0200 Display in a more clear way commit c2c059ba3e5fe091655060444694b14314f3777a Author: Pablo Villacorta Aylagas Date: Mon Sep 2 13:15:39 2024 +0200 Display only first spin commit b2765b5b1c4d43ea3a7c8b7e6151d928c71d8b61 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 12:47:36 2024 +0200 Display @view of first 10 spins commit 9ea456d1924e635eafd4da15e209ac586c1567a4 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 10:36:36 2024 +0200 Check dimensions to see if it's problem of a 1D interpolator commit 887482195cecebab811a67bf2cf2c633dab40683 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 10:05:14 2024 +0200 Fix deleted line, debug with `uy` commit 2e8da4816a28fb97f71388ee09763330f77f5255 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 09:59:36 2024 +0200 t_unit is not the problem, display(uy) commit 9a3c07e334c3db22d1640748a86a9eb5a47b4d1b Author: Pablo Villacorta Aylagas Date: Mon Sep 2 09:26:30 2024 +0200 Remove @suppress commit 66bf3642ed39a81bb9ed5b58069fc4d15c271c99 Author: Pablo Villacorta Aylagas Date: Mon Sep 2 09:21:06 2024 +0200 Debug messages commit 0d4eb5f58b24e917de734956f2b60d4ca2520c87 Author: Pablo Villacorta Aylagas Date: Fri Aug 30 13:52:44 2024 +0200 Run 10 iterations correctly commit e15f5019c649889217f7796070ed0d70a72f9194 Author: Pablo Villacorta Aylagas Date: Fri Aug 30 13:48:31 2024 +0200 10 test iterations to check if any of them fails commit 629750dc9c3930d0fa43021fe53767f02cac858a Author: Pablo Villacorta Aylagas Date: Fri Aug 30 13:40:22 2024 +0200 Tests previous to simulation commit c748249de2a8b84bfce7634daa92b0e8e39fbc9b Author: Pablo Villacorta Aylagas Date: Fri Aug 30 13:35:27 2024 +0200 Original test commit b308511c14609832b652de71d261f896343b4b63 Author: Pablo Villacorta Aylagas Date: Fri Aug 30 13:28:03 2024 +0200 Keep testing commit cfda640d468cac0903fbdb4a00faf2fd266a02e6 Author: Pablo Villacorta Aylagas Date: Fri Aug 30 13:22:04 2024 +0200 commit c1f0f3a95919e3e14f9c00cb8b473e95d5988f8a Author: Pablo Villacorta Aylagas Date: Fri Aug 30 13:15:02 2024 +0200 Display in order to compare results from `get_spin_coords` --- .buildkite/runtests.yml | 2 +- .../src/motion/motionlist/MotionList.jl | 6 +- KomaMRIBase/src/motion/motionlist/TimeSpan.jl | 9 +- .../motionlist/actions/ArbitraryAction.jl | 32 +--- .../actions/arbitraryactions/FlowPath.jl | 6 +- .../actions/simpleactions/HeartBeat.jl | 27 +-- .../actions/simpleactions/Rotate.jl | 65 +++---- .../actions/simpleactions/Translate.jl | 27 +-- KomaMRICore/ext/KomaoneAPIExt.jl | 8 - KomaMRICore/src/simulation/Flow.jl | 23 +-- .../simulation/SimMethods/Bloch/BlochGPU.jl | 3 +- .../SimMethods/BlochDict/BlochDict.jl | 2 +- .../SimMethods/BlochSimple/BlochSimple.jl | 4 +- KomaMRICore/src/simulation/SimulatorCore.jl | 24 +-- KomaMRICore/test/Project.toml | 1 + KomaMRICore/test/runtests.jl | 161 +++++++++--------- 16 files changed, 152 insertions(+), 248 deletions(-) diff --git a/.buildkite/runtests.yml b/.buildkite/runtests.yml index b88fd8163..048b90cfe 100644 --- a/.buildkite/runtests.yml +++ b/.buildkite/runtests.yml @@ -140,7 +140,7 @@ steps: setup: version: - "1.9" - - "1" + # - "1" plugins: - JuliaCI/julia#v1: version: "{{matrix.version}}" diff --git a/KomaMRIBase/src/motion/motionlist/MotionList.jl b/KomaMRIBase/src/motion/motionlist/MotionList.jl index bb1e00062..618e71025 100644 --- a/KomaMRIBase/src/motion/motionlist/MotionList.jl +++ b/KomaMRIBase/src/motion/motionlist/MotionList.jl @@ -101,11 +101,7 @@ For each dimension (x, y, z), the output matrix has ``N_{\t{spins}}`` rows and ` - `x, y, z`: (`::Tuple{AbstractArray, AbstractArray, AbstractArray}`) spin positions over time """ function get_spin_coords( - ml::MotionList{T}, - x::AbstractVector{T}, - y::AbstractVector{T}, - z::AbstractVector{T}, - t::AbstractArray{T} + ml::MotionList{T}, x::AbstractVector{T}, y::AbstractVector{T}, z::AbstractVector{T}, t ) where {T<:Real} # Buffers for positions: xt, yt, zt = x .+ 0*t, y .+ 0*t, z .+ 0*t diff --git a/KomaMRIBase/src/motion/motionlist/TimeSpan.jl b/KomaMRIBase/src/motion/motionlist/TimeSpan.jl index f91621cdd..551e4965a 100644 --- a/KomaMRIBase/src/motion/motionlist/TimeSpan.jl +++ b/KomaMRIBase/src/motion/motionlist/TimeSpan.jl @@ -59,10 +59,7 @@ julia> t_unit = KomaMRIBase.unit_time([0.0, 1.0, 2.0, 3.0, 4.0, 5.0], TimeRange( 1.0 ``` """ -function unit_time(t::AbstractArray{T}, ts::TimeRange{T}) where {T<:Real} - return _unit_time(t, ts) -end -function _unit_time(t::AbstractArray{T}, ts::TimeRange{T}) where {T<:Real} +function unit_time(t, ts::TimeRange{T}) where {T<:Real} if ts.t_start == ts.t_end return (t .>= ts.t_start) .* oneunit(T) else @@ -131,7 +128,7 @@ julia> t_unit = KomaMRIBase.unit_time([0.0, 1.0, 2.0, 3.0, 4.0, 5.0], Periodic(4 0.5 ``` """ -function unit_time(t::AbstractArray{T}, ts::Periodic{T}) where {T<:Real} +function unit_time(t, ts::Periodic{T}) where {T<:Real} t_rise = ts.period * ts.asymmetry t_fall = ts.period * (oneunit(T) - ts.asymmetry) t_relative = mod.(t, ts.period) @@ -143,4 +140,4 @@ function unit_time(t::AbstractArray{T}, ts::Periodic{T}) where {T<:Real} t_unit = ifelse.( t_relative .< t_rise, t_relative ./ t_rise, oneunit(T) .- (t_relative .- t_rise) ./ t_fall) end return t_unit -end +end \ No newline at end of file diff --git a/KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl b/KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl index 5b67070fd..a08a7f71d 100644 --- a/KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl @@ -45,55 +45,35 @@ function interpolate(d::AbstractArray{T}, ITPType, Ns::Val) where {T<:Real} return GriddedInterpolation((id, t), d, ITPType) end -function resample(itp::Interpolator1D{T}, t::AbstractArray{T}) where {T<:Real} +function resample(itp::Interpolator1D{T}, t) where {T<:Real} return itp.(t) end -function resample(itp::Interpolator2D{T}, t::AbstractArray{T}) where {T<:Real} +function resample(itp::Interpolator2D{T}, t) where {T<:Real} Ns = size(itp.coefs, 1) id = similar(itp.coefs, Ns) copyto!(id, collect(range(oneunit(T), T(Ns), Ns))) return itp.(id, t) end -function displacement_x!( - ux::AbstractArray{T}, - action::ArbitraryAction{T}, - x::AbstractArray{T}, - y::AbstractArray{T}, - z::AbstractArray{T}, - t::AbstractArray{T}, -) where {T<:Real} +function displacement_x!(ux, action::ArbitraryAction, x, y, z, t) itp = interpolate(action.dx, Gridded(Linear()), Val(size(action.dx,1))) ux .= resample(itp, t) return nothing end -function displacement_y!( - uy::AbstractArray{T}, - action::ArbitraryAction{T}, - x::AbstractArray{T}, - y::AbstractArray{T}, - z::AbstractArray{T}, - t::AbstractArray{T}, -) where {T<:Real} +function displacement_y!(uy, action::ArbitraryAction, x, y, z, t) itp = interpolate(action.dy, Gridded(Linear()), Val(size(action.dy,1))) uy .= resample(itp, t) return nothing end -function displacement_z!( - uz::AbstractArray{T}, - action::ArbitraryAction{T}, - x::AbstractArray{T}, - y::AbstractArray{T}, - z::AbstractArray{T}, - t::AbstractArray{T}, -) where {T<:Real} +function displacement_z!(uz, action::ArbitraryAction, x, y, z, t) itp = interpolate(action.dz, Gridded(Linear()), Val(size(action.dz,1))) uz .= resample(itp, t) return nothing end + include("arbitraryactions/Path.jl") include("arbitraryactions/FlowPath.jl") \ No newline at end of file diff --git a/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl b/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl index af502626b..bfd192b85 100644 --- a/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl @@ -33,7 +33,5 @@ julia> flowpath = FlowPath( dx::AbstractArray{T} dy::AbstractArray{T} dz::AbstractArray{T} - spin_reset::AbstractArray{Bool} -end - -FlowPath(dx::AbstractArray{T}, dy::AbstractArray{T}, dz::AbstractArray{T}, spin_reset::Array) where T<:Real = FlowPath(dx, dy, dz, Bool.(spin_reset)) \ No newline at end of file + spin_reset::AbstractArray{T} +end \ No newline at end of file diff --git a/KomaMRIBase/src/motion/motionlist/actions/simpleactions/HeartBeat.jl b/KomaMRIBase/src/motion/motionlist/actions/simpleactions/HeartBeat.jl index 45497a91d..92a38b9e4 100644 --- a/KomaMRIBase/src/motion/motionlist/actions/simpleactions/HeartBeat.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/simpleactions/HeartBeat.jl @@ -25,14 +25,7 @@ end is_composable(action::HeartBeat) = true -function displacement_x!( - ux::AbstractArray{T}, - action::HeartBeat{T}, - x::AbstractArray{T}, - y::AbstractArray{T}, - z::AbstractArray{T}, - t::AbstractArray{T}, -) where {T<:Real} +function displacement_x!(ux, action::HeartBeat, x, y, z, t) r = sqrt.(x .^ 2 + y .^ 2) θ = atan.(y, x) Δ_circunferential = action.circumferential_strain * maximum(r) @@ -46,14 +39,7 @@ function displacement_x!( return nothing end -function displacement_y!( - uy::AbstractArray{T}, - action::HeartBeat{T}, - x::AbstractArray{T}, - y::AbstractArray{T}, - z::AbstractArray{T}, - t::AbstractArray{T}, -) where {T<:Real} +function displacement_y!(uy, action::HeartBeat, x, y, z, t) r = sqrt.(x .^ 2 + y .^ 2) θ = atan.(y, x) Δ_circunferential = action.circumferential_strain * maximum(r) @@ -67,14 +53,7 @@ function displacement_y!( return nothing end -function displacement_z!( - uz::AbstractArray{T}, - action::HeartBeat{T}, - x::AbstractArray{T}, - y::AbstractArray{T}, - z::AbstractArray{T}, - t::AbstractArray{T}, -) where {T<:Real} +function displacement_z!(uz, action::HeartBeat, x, y, z, t) uz .= t .* (z .* action.longitudinal_strain) return nothing end \ No newline at end of file diff --git a/KomaMRIBase/src/motion/motionlist/actions/simpleactions/Rotate.jl b/KomaMRIBase/src/motion/motionlist/actions/simpleactions/Rotate.jl index 5302a58a2..c43070923 100644 --- a/KomaMRIBase/src/motion/motionlist/actions/simpleactions/Rotate.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/simpleactions/Rotate.jl @@ -62,53 +62,34 @@ RotateZ(yaw::T) where {T<:Real} = Rotate(zero(T), zero(T), yaw) is_composable(action::Rotate) = true -function displacement_x!( - ux::AbstractArray{T}, - action::Rotate{T}, - x::AbstractArray{T}, - y::AbstractArray{T}, - z::AbstractArray{T}, - t::AbstractArray{T}, -) where {T<:Real} - α = t .* (action.yaw) - β = t .* (action.roll) - γ = t .* (action.pitch) - ux .= cosd.(α) .* cosd.(β) .* x + - (cosd.(α) .* sind.(β) .* sind.(γ) .- sind.(α) .* cosd.(γ)) .* y + - (cosd.(α) .* sind.(β) .* cosd.(γ) .+ sind.(α) .* sind.(γ)) .* z .- x +function displacement_x!(ux, action::Rotate, x, y, z, t) + # Not using sind and cosd functions until bug with oneAPI is solved: + # https://github.com/JuliaGPU/oneAPI.jl/issues/65 + α = t .* (action.yaw*π/180) + β = t .* (action.roll*π/180) + γ = t .* (action.pitch*π/180) + ux .= cos.(α) .* cos.(β) .* x + + (cos.(α) .* sin.(β) .* sin.(γ) .- sin.(α) .* cos.(γ)) .* y + + (cos.(α) .* sin.(β) .* cos.(γ) .+ sin.(α) .* sin.(γ)) .* z .- x return nothing end -function displacement_y!( - uy::AbstractArray{T}, - action::Rotate{T}, - x::AbstractArray{T}, - y::AbstractArray{T}, - z::AbstractArray{T}, - t::AbstractArray{T}, -) where {T<:Real} - α = t .* (action.yaw) - β = t .* (action.roll) - γ = t .* (action.pitch) - uy .= sind.(α) .* cosd.(β) .* x + - (sind.(α) .* sind.(β) .* sind.(γ) .+ cosd.(α) .* cosd.(γ)) .* y + - (sind.(α) .* sind.(β) .* cosd.(γ) .- cosd.(α) .* sind.(γ)) .* z .- y +function displacement_y!(uy, action::Rotate, x, y, z, t) + α = t .* (action.yaw*π/180) + β = t .* (action.roll*π/180) + γ = t .* (action.pitch*π/180) + uy .= sin.(α) .* cos.(β) .* x + + (sin.(α) .* sin.(β) .* sin.(γ) .+ cos.(α) .* cos.(γ)) .* y + + (sin.(α) .* sin.(β) .* cos.(γ) .- cos.(α) .* sin.(γ)) .* z .- y return nothing end -function displacement_z!( - uz::AbstractArray{T}, - action::Rotate{T}, - x::AbstractArray{T}, - y::AbstractArray{T}, - z::AbstractArray{T}, - t::AbstractArray{T}, -) where {T<:Real} - α = t .* (action.yaw) - β = t .* (action.roll) - γ = t .* (action.pitch) - uz .= -sind.(β) .* x + - cosd.(β) .* sind.(γ) .* y + - cosd.(β) .* cosd.(γ) .* z .- z +function displacement_z!(uz, action::Rotate, x, y, z, t) + α = t .* (action.yaw*π/180) + β = t .* (action.roll*π/180) + γ = t .* (action.pitch*π/180) + uz .= -sin.(β) .* x + + cos.(β) .* sin.(γ) .* y + + cos.(β) .* cos.(γ) .* z .- z return nothing end \ No newline at end of file diff --git a/KomaMRIBase/src/motion/motionlist/actions/simpleactions/Translate.jl b/KomaMRIBase/src/motion/motionlist/actions/simpleactions/Translate.jl index 03d4fb709..3508dc975 100644 --- a/KomaMRIBase/src/motion/motionlist/actions/simpleactions/Translate.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/simpleactions/Translate.jl @@ -27,38 +27,17 @@ TranslateX(dx::T) where {T<:Real} = Translate(dx, zero(T), zero(T)) TranslateY(dy::T) where {T<:Real} = Translate(zero(T), dy, zero(T)) TranslateZ(dz::T) where {T<:Real} = Translate(zero(T), zero(T), dz) -function displacement_x!( - ux::AbstractArray{T}, - action::Translate{T}, - x::AbstractVector{T}, - y::AbstractVector{T}, - z::AbstractVector{T}, - t::AbstractArray{T}, -) where {T<:Real} +function displacement_x!(ux, action::Translate, x, y, z, t) ux .= t.* action.dx return nothing end -function displacement_y!( - uy::AbstractArray{T}, - action::Translate{T}, - x::AbstractVector{T}, - y::AbstractVector{T}, - z::AbstractVector{T}, - t::AbstractArray{T}, -) where {T<:Real} +function displacement_y!(uy, action::Translate, x, y, z, t) uy .= t .* action.dy return nothing end -function displacement_z!( - uz::AbstractArray{T}, - action::Translate{T}, - x::AbstractVector{T}, - y::AbstractVector{T}, - z::AbstractVector{T}, - t::AbstractArray{T}, -) where {T<:Real} +function displacement_z!(uz, action::Translate, x, y, z, t) uz .= t .* action.dz return nothing end diff --git a/KomaMRICore/ext/KomaoneAPIExt.jl b/KomaMRICore/ext/KomaoneAPIExt.jl index 8bbb9d6e1..f2f13f4cd 100644 --- a/KomaMRICore/ext/KomaoneAPIExt.jl +++ b/KomaMRICore/ext/KomaoneAPIExt.jl @@ -62,12 +62,4 @@ function __init__() @warn "oneAPI does not support all array operations used by KomaMRI. GPU performance may be slower than expected" end -const AdjointOneArray{T, N, M} = LinearAlgebra.Adjoint{T, oneArray{T, N, M}} where {T<:Real, N, M} -## Extend KomaMRIBase.unit_time (until bug with oneAPI is solved) -function KomaMRIBase.unit_time(t::AdjointOneArray{T, N, M}, ts::KomaMRIBase.TimeRange{T}) where {T<:Real, N, M} - t_unit = KomaMRIBase._unit_time(t, ts) - _ = sum(t_unit) - return t_unit -end - end \ No newline at end of file diff --git a/KomaMRICore/src/simulation/Flow.jl b/KomaMRICore/src/simulation/Flow.jl index a8f025c4d..e724abdff 100644 --- a/KomaMRICore/src/simulation/Flow.jl +++ b/KomaMRICore/src/simulation/Flow.jl @@ -1,28 +1,29 @@ """ reset_magnetization! """ -function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion::NoMotion{T}, t::AbstractArray{T}) where {T<:Real} +function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion::NoMotion{T}, t::AbstractArray{T}, ρ) where {T<:Real} return nothing end -function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion::MotionList{T}, t::AbstractArray{T}) where {T<:Real} +function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion::MotionList{T}, t::AbstractArray{T}, ρ) where {T<:Real} for m in motion.motions + t_unit = KomaMRIBase.unit_time(t, m.time) idx = KomaMRIBase.get_idx(m.spins) - reset_magnetization!(@view(M[idx]), @view(Mxy[idx, :]), m.action, t) + reset_magnetization!(@view(M[idx]), @view(Mxy[idx, :]), m.action, t_unit, @view(ρ[idx])) end return nothing end -function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, action::KomaMRIBase.AbstractActionSpan{T}, t::AbstractArray{T}) where {T<:Real} +function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, action::KomaMRIBase.AbstractActionSpan{T}, t, ρ) where {T<:Real} return nothing end -function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, action::FlowPath{T}, t::AbstractArray{T}) where {T<:Real} - itp = interpolate(action.spin_reset, Gridded(Constant{Previous}), Val(size(action.spin_reset, 1))) - flags = resample(itp, unit_time(t, action.time)) - reset = any(flags; dims=2) - flags = .!(cumsum(flags; dims=2) .>= 1) - Mxy .*= flags - M.z[reset] = p.ρ[reset] +function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, action::FlowPath{T}, t, ρ) where {T<:Real} + itp = KomaMRIBase.interpolate(action.spin_reset, KomaMRIBase.Gridded(KomaMRIBase.Constant{KomaMRIBase.Previous}()), Val(size(action.spin_reset, 1))) + flags = KomaMRIBase.resample(itp, t) + reset = vec(any(flags .> 0; dims=2)) + flags = (cumsum(flags; dims=2) .== 0) + Mxy .*= flags + M.z[reset] = ρ[reset] return nothing end \ No newline at end of file diff --git a/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl b/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl index 14f78765e..1e98dd351 100644 --- a/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl +++ b/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl @@ -134,7 +134,6 @@ function run_spin_precession!( #Simulation #Motion x, y, z = get_spin_coords(p.motion, p.x, p.y, p.z, seq.t') - #Sequence block info seq_block = pre.seq_properties[1] @@ -190,7 +189,7 @@ function run_spin_excitation!( pre.ΔT2 .= exp.(-seq.Δt' ./ p.T2) #Excitation - apply_excitation!(backend, 512)(M.xy, M.z, pre.φ, seq.B1, pre.Bz, pre.B, pre.ΔT1, pre.ΔT2, p.ρ, ndrange=size(M.xy,1)) + apply_excitation!(backend, 256)(M.xy, M.z, pre.φ, seq.B1, pre.Bz, pre.B, pre.ΔT1, pre.ΔT2, p.ρ, ndrange=size(M.xy,1)) KA.synchronize(backend) return nothing diff --git a/KomaMRICore/src/simulation/SimMethods/BlochDict/BlochDict.jl b/KomaMRICore/src/simulation/SimMethods/BlochDict/BlochDict.jl index b0b769c78..7afc01841 100644 --- a/KomaMRICore/src/simulation/SimMethods/BlochDict/BlochDict.jl +++ b/KomaMRICore/src/simulation/SimMethods/BlochDict/BlochDict.jl @@ -53,7 +53,7 @@ function run_spin_precession!( tp = cumsum(seq.Δt) # t' = t - t0 dur = sum(seq.Δt) # Total length, used for signal relaxation Mxy = [M.xy M.xy .* exp.(-tp' ./ p.T2) .* (cos.(ϕ) .+ im .* sin.(ϕ))] #This assumes Δw and T2 are constant in time - reset_magnetization!(M, Mxy, p.motion, seq.t') + reset_magnetization!(M, Mxy, p.motion, seq.t', p.ρ) M.xy .= Mxy[:, end] #Acquired signal sig[:, :, 1] .= transpose(Mxy[:, findall(seq.ADC)]) diff --git a/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl b/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl index 35f8d8568..cd3b6e6a3 100644 --- a/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl +++ b/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl @@ -46,7 +46,7 @@ function run_spin_precession!( dur = sum(seq.Δt) # Total length, used for signal relaxation Mxy = [M.xy M.xy .* exp.(-tp' ./ p.T2) .* (cos.(ϕ) .+ im .* sin.(ϕ))] #This assumes Δw and T2 are constant in time M.z .= M.z .* exp.(-dur ./ p.T1) .+ p.ρ .* (1 .- exp.(-dur ./ p.T1)) - reset_magnetization!(M, Mxy, p.motion, seq.t') + reset_magnetization!(M, Mxy, p.motion, seq.t', p.ρ) M.xy .= Mxy[:, end] #Acquired signal sig .= transpose(sum(Mxy[:, findall(seq.ADC)]; dims=1)) #<--- TODO: add coil sensitivities @@ -93,7 +93,7 @@ function run_spin_excitation!( #Relaxation M.xy .= M.xy .* exp.(-s.Δt ./ p.T2) M.z .= M.z .* exp.(-s.Δt ./ p.T1) .+ p.ρ .* (1 .- exp.(-s.Δt ./ p.T1)) - reset_magnetization!(M, M.xy, p.motion, s.t) + reset_magnetization!(M, M.xy, p.motion, s.t, p.ρ) end #Acquired signal #sig .= -1.4im #<-- This was to test if an ADC point was inside an RF block diff --git a/KomaMRICore/src/simulation/SimulatorCore.jl b/KomaMRICore/src/simulation/SimulatorCore.jl index 6ba42e583..b2b9a3cdf 100644 --- a/KomaMRICore/src/simulation/SimulatorCore.jl +++ b/KomaMRICore/src/simulation/SimulatorCore.jl @@ -181,7 +181,7 @@ function run_sim_time_iter!( # Simulation rfs = 0 samples = 1 - progress_bar = Progress(Nblocks; desc="Running simulation...") + # progress_bar = Progress(Nblocks; desc="Running simulation...") prealloc_result = prealloc(sim_method, backend, obj, Xt, maximum(length.(parts))+1, precalc) for (block, p) in enumerate(parts) @@ -204,13 +204,13 @@ function run_sim_time_iter!( end samples += Nadc #Update progress - next!( - progress_bar; - showvalues=[ - (:simulated_blocks, block), (:rf_blocks, rfs), (:acq_samples, samples - 1) - ], - ) - update_blink_window_progress!(w, block, Nblocks) + # next!( + # progress_bar; + # showvalues=[ + # (:simulated_blocks, block), (:rf_blocks, rfs), (:acq_samples, samples - 1) + # ], + # ) + # update_blink_window_progress!(w, block, Nblocks) end return nothing end @@ -382,10 +382,10 @@ function simulate( end # Simulation - @info "Running simulation in the $(backend isa KA.GPU ? "GPU ($gpu_name)" : "CPU with $(sim_params["Nthreads"]) thread(s)")" koma_version = - pkgversion(@__MODULE__) sim_method = sim_params["sim_method"] spins = length(obj) time_points = length( - seqd.t - ) adc_points = Ndims[1] + # @info "Running simulation in the $(backend isa KA.GPU ? "GPU ($gpu_name)" : "CPU with $(sim_params["Nthreads"]) thread(s)")" koma_version = + # pkgversion(@__MODULE__) sim_method = sim_params["sim_method"] spins = length(obj) time_points = length( + # seqd.t + # ) adc_points = Ndims[1] @time timed_tuple = @timed run_sim_time_iter!( obj, seqd, diff --git a/KomaMRICore/test/Project.toml b/KomaMRICore/test/Project.toml index 50a7da212..0d93ca5de 100644 --- a/KomaMRICore/test/Project.toml +++ b/KomaMRICore/test/Project.toml @@ -3,6 +3,7 @@ HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" KomaMRIBase = "d0bc0b20-b151-4d03-b2a4-6ca51751cb9c" KomaMRICore = "4baa4f4d-2ae9-40db-8331-a7d1080e3f4e" +OrdinaryDiffEqTsit5 = "b1df2697-797e-41e3-8120-5422d3b24e4a" Preferences = "21216c6a-2e73-6563-6e65-726566657250" Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index 62ea3f5b8..bffe795ce 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -421,86 +421,87 @@ end end # --------- Motion-related tests ------------- -@testitem "Bloch SimpleAction" tags=[:core, :motion] begin - using Suppressor - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - sig_jemris = signal_brain_motion_jemris() - seq = seq_epi_100x100_TE100_FOV230() - sys = Scanner() - obj = phantom_brain_simple_motion() - sim_params = Dict{String, Any}( - "gpu"=>USE_GPU, - "sim_method"=>KomaMRICore.Bloch(), - "return_type"=>"mat" - ) - sig = @suppress simulate(obj, seq, sys; sim_params) - sig = sig / prod(size(obj)) - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% -end - -@testitem "BlochSimple SimpleAction" tags=[:core, :motion] begin - using Suppressor - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - sig_jemris = signal_brain_motion_jemris() - seq = seq_epi_100x100_TE100_FOV230() - sys = Scanner() - obj = phantom_brain_simple_motion() - - sim_params = Dict{String, Any}( - "gpu"=>USE_GPU, - "sim_method"=>KomaMRICore.BlochSimple(), - "return_type"=>"mat" - ) - sig = @suppress simulate(obj, seq, sys; sim_params) - sig = sig / prod(size(obj)) - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% -end - -@testitem "Bloch ArbitraryAction" tags=[:core, :motion] begin - using Suppressor +# We compare with the result given by OrdinaryDiffEqTsit5 +@testitem "Motion" tags=[:core, :motion] begin + using Suppressor, OrdinaryDiffEqTsit5 include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - sig_jemris = signal_brain_motion_jemris() - seq = seq_epi_100x100_TE100_FOV230() - sys = Scanner() - obj = phantom_brain_arbitrary_motion() - - sim_params = Dict{String, Any}( - "gpu"=>USE_GPU, - "sim_method"=>KomaMRICore.Bloch(), - "return_type"=>"mat" - ) - sig = @suppress simulate(obj, seq, sys; sim_params) - sig = sig / prod(size(obj)) - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% -end -@testitem "BlochSimple ArbitraryAction" tags=[:core, :motion] begin - using Suppressor - include("initialize_backend.jl") - include(joinpath(@__DIR__, "test_files", "utils.jl")) - - sig_jemris = signal_brain_motion_jemris() - seq = seq_epi_100x100_TE100_FOV230() - sys = Scanner() - obj = phantom_brain_arbitrary_motion() - - sim_params = Dict{String, Any}( - "gpu"=>USE_GPU, - "sim_method"=>KomaMRICore.BlochSimple(), - "return_type"=>"mat" - ) - sig = simulate(obj, seq, sys; sim_params) - sig = sig / prod(size(obj)) - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% + Nadc = 25 + M0 = 1.0 + T1 = 100e-3 + T2 = 10e-3 + B1 = 20e-6 + Trf = 3e-3 + γ = 2π * 42.58e6 + φ = π / 4 + B1e(t) = B1 * (0 <= t <= Trf) + duration = 2*Trf + + Gx = 1e-3 + Gy = 1e-3 + Gz = 0 + + motions = [ + Translate(0.0, 0.1, 0.0, TimeRange(0.0, 1.0)), + Rotate(0.0, 0.0, 45.0, TimeRange(0.0, 1.0)), + HeartBeat(-0.6, 0.0, 0.0, Periodic(1.0)), + Path([0.0 0.0], [0.0 1.0], [0.0 0.0], TimeRange(0.0, 10.0)), + FlowPath([0.0 0.0], [0.0 1.0], [0.0 0.0], [0.0 0.0], TimeRange(0.0, 10.0)) + ] + + x0 = [0.1] + y0 = [0.1] + z0 = [0.0] + + for m in motions + motion = MotionList(m) + + coords(t) = get_spin_coords(motion, x0, y0, z0, t) + x(t) = (coords(t)[1])[1] + y(t) = (coords(t)[2])[1] + z(t) = (coords(t)[3])[1] + + ## Solving using DiffEquations.jl + function bloch!(dm, m, p, t) + mx, my, mz = m + bx, by, bz = [B1e(t) * cos(φ), B1e(t) * sin(φ), (x(t) * Gx + y(t) * Gy + z(t) * Gz)] + dm[1] = -mx / T2 + γ * bz * my - γ * by * mz + dm[2] = -γ * bz * mx - my / T2 + γ * bx * mz + dm[3] = γ * by * mx - γ * bx * my - mz / T1 + M0 / T1 + return nothing + end + m0 = [0.0, 0.0, 1.0] + tspan = (0.0, duration) + prob = ODEProblem(bloch!, m0, tspan) + # Only at ADC points + tadc = range(Trf, duration, Nadc) + sol = @time solve(prob, Tsit5(), saveat = tadc, abstol = 1e-9, reltol = 1e-9) + sol_diffeq = hcat(sol.u...)' + mxy_diffeq = sol_diffeq[:, 1] + im * sol_diffeq[:, 2] + + ## Solving using KomaMRICore.jl + # Creating Sequence + seq = Sequence() + seq += RF(cis(φ) .* B1, Trf) + seq.GR[1,1] = Grad(Gx, duration) + seq.GR[2,1] = Grad(Gy, duration) + seq.GR[3,1] = Grad(Gz, duration) + seq.ADC[1] = ADC(Nadc, duration-Trf, Trf) + # Creating object + obj = Phantom(x = x0, y = y0, z = z0, ρ = [M0], T1 = [T1], T2 = [T2], motion = motion) + # Scanner + sys = Scanner() + # Simulation + for sim_method in [KomaMRICore.Bloch(), KomaMRICore.BlochSimple(), KomaMRICore.BlochDict()] + sim_params = Dict{String, Any}( + "sim_method"=>sim_method, + "return_type"=>"mat" + ) + raw_aux = @suppress simulate(obj, seq, sys; sim_params) + raw = raw_aux[:, 1, 1] + + NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. + @test NMRSE(raw, mxy_diffeq) < 1 + end + end end - From 586adcf599563a47fb75766f7369de59b299e02d Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 12 Sep 2024 13:00:41 +0200 Subject: [PATCH 70/91] Uncomment informative messages --- KomaMRICore/src/simulation/SimulatorCore.jl | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/KomaMRICore/src/simulation/SimulatorCore.jl b/KomaMRICore/src/simulation/SimulatorCore.jl index b2b9a3cdf..6ba42e583 100644 --- a/KomaMRICore/src/simulation/SimulatorCore.jl +++ b/KomaMRICore/src/simulation/SimulatorCore.jl @@ -181,7 +181,7 @@ function run_sim_time_iter!( # Simulation rfs = 0 samples = 1 - # progress_bar = Progress(Nblocks; desc="Running simulation...") + progress_bar = Progress(Nblocks; desc="Running simulation...") prealloc_result = prealloc(sim_method, backend, obj, Xt, maximum(length.(parts))+1, precalc) for (block, p) in enumerate(parts) @@ -204,13 +204,13 @@ function run_sim_time_iter!( end samples += Nadc #Update progress - # next!( - # progress_bar; - # showvalues=[ - # (:simulated_blocks, block), (:rf_blocks, rfs), (:acq_samples, samples - 1) - # ], - # ) - # update_blink_window_progress!(w, block, Nblocks) + next!( + progress_bar; + showvalues=[ + (:simulated_blocks, block), (:rf_blocks, rfs), (:acq_samples, samples - 1) + ], + ) + update_blink_window_progress!(w, block, Nblocks) end return nothing end @@ -382,10 +382,10 @@ function simulate( end # Simulation - # @info "Running simulation in the $(backend isa KA.GPU ? "GPU ($gpu_name)" : "CPU with $(sim_params["Nthreads"]) thread(s)")" koma_version = - # pkgversion(@__MODULE__) sim_method = sim_params["sim_method"] spins = length(obj) time_points = length( - # seqd.t - # ) adc_points = Ndims[1] + @info "Running simulation in the $(backend isa KA.GPU ? "GPU ($gpu_name)" : "CPU with $(sim_params["Nthreads"]) thread(s)")" koma_version = + pkgversion(@__MODULE__) sim_method = sim_params["sim_method"] spins = length(obj) time_points = length( + seqd.t + ) adc_points = Ndims[1] @time timed_tuple = @timed run_sim_time_iter!( obj, seqd, From 2177410cf41451d09e7ccb31e3b7347ecbc54160 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Fri, 13 Sep 2024 18:15:00 +0200 Subject: [PATCH 71/91] Change compat from julia 1.9 to 1.10 --- .buildkite/runtests.yml | 13 +++++++------ .github/workflows/CI.yml | 2 +- KomaMRICore/Project.toml | 2 +- README.md | 21 +++++++++++---------- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/.buildkite/runtests.yml b/.buildkite/runtests.yml index 048b90cfe..c431802a2 100644 --- a/.buildkite/runtests.yml +++ b/.buildkite/runtests.yml @@ -5,7 +5,7 @@ steps: matrix: setup: version: - - "1.9" + - "1.10" - "1" plugins: - JuliaCI/julia#v1: @@ -36,6 +36,7 @@ steps: matrix: setup: version: + - "1.10" - "1" plugins: - JuliaCI/julia#v1: @@ -61,7 +62,7 @@ steps: julia -e 'println("--- :julia: Running tests") using Pkg - Pkg.test("KomaMRICore"; coverage=true, test_args=["AMDGPU", ])' + Pkg.test("KomaMRICore"; coverage=true, test_args=["AMDGPU"])' agents: queue: "juliagpu" rocm: "*" @@ -71,7 +72,7 @@ steps: matrix: setup: version: - - "1.9" + - "1.10" - "1" plugins: - JuliaCI/julia#v1: @@ -107,7 +108,7 @@ steps: matrix: setup: version: - - "1.9" + - "1.10" - "1" plugins: - JuliaCI/julia#v1: @@ -139,8 +140,8 @@ steps: matrix: setup: version: - - "1.9" - # - "1" + - "1.10" + - "1" plugins: - JuliaCI/julia#v1: version: "{{matrix.version}}" diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 6e0d0f6b2..6f1c13932 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -17,7 +17,7 @@ jobs: fail-fast: false matrix: version: - - '1.9' # Replace this with the minimum Julia version that your package supports. E.g. if your package requires Julia 1.5 or higher, change this to '1.5'. + - '1.10' # Replace this with the minimum Julia version that your package supports. E.g. if your package requires Julia 1.5 or higher, change this to '1.5'. - '1' # Leave this line unchanged. '1' will automatically expand to the latest stable 1.x release of Julia. os: [ubuntu-latest, windows-latest, macos-12] # macos-latest] <- M1 Mac was generating problems #386, commented for now arch: [x64] diff --git a/KomaMRICore/Project.toml b/KomaMRICore/Project.toml index c915a6ab1..b3289c3ed 100644 --- a/KomaMRICore/Project.toml +++ b/KomaMRICore/Project.toml @@ -36,7 +36,7 @@ Metal = "1.2" ProgressMeter = "1" Reexport = "1" ThreadsX = "0.1" -julia = "1.9" +julia = "1.10" oneAPI = "1" [workspace] diff --git a/README.md b/README.md index a41fa4e8f..57ef0aad2 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

-![][julia-19] [![][gh-actions-komamri]][gh-actions-url] [![][buildkite-badge]][buildkite-url] [![][codecov-komamri]][codecov-url] [![][license]][license-url] [![][julia-blue]][julia-blue-url] [![][total-downloads-komamri]][downloads-komamri-url] ![][gh-starts-komamri] +![][julia-110] [![][gh-actions-komamri]][gh-actions-url] [![][buildkite-badge]][buildkite-url] [![][codecov-komamri]][codecov-url] [![][license]][license-url] [![][julia-blue]][julia-blue-url] [![][total-downloads-komamri]][downloads-komamri-url] ![][gh-starts-komamri] [![][docr-img]][docr-url] [![][docd-img]][docd-url] [![][paper-img]][paper-url] @@ -155,7 +155,7 @@ All parallel backends are tested on Linux (besides Apple silicon) using the late | KomaMRICore | CPU | GPU (Nvidia) | GPU (AMD) | GPU (Apple) | GPU (Intel) | |:---------------------|:-----------------------------------:|:-----------------------------------:|:--------------------------------:|:----------------------------------:|:----------------------------------:| -| Julia 1.9 | [![][cpu-compat]][buildkite-url] | [![][nvidia-compat]][buildkite-url] | [![][amd-compat]][buildkite-url] | [![][apple-compat]][buildkite-url] | [![][intel-compat]][buildkite-url] | +| Julia 1.10 | [![][cpu-compat]][buildkite-url] | [![][nvidia-compat]][buildkite-url] | [![][amd-compat]][buildkite-url] | [![][apple-compat]][buildkite-url] | [![][intel-compat]][buildkite-url] | | Julia 1 | [![][cpu-stable]][buildkite-url] | [![][nvidia-stable]][buildkite-url] | [![][amd-stable]][buildkite-url] | [![][apple-stable]][buildkite-url] | [![][intel-stable]][buildkite-url] |
@@ -166,9 +166,9 @@ Single-threaded compatibility is tested in all major operating systems (OS). | KomaMRI | CPU (single-threaded) | |:---------------------|:-----------------------------------------:| -| Julia 1.9 (Windows) | [![][gh-actions-komamri]][gh-actions-url] | -| Julia 1.9 (Linux) | [![][gh-actions-komamri]][gh-actions-url] | -| Julia 1.9 (Mac OS) | [![][gh-actions-komamri]][gh-actions-url] | +| Julia 1.10 (Windows) | [![][gh-actions-komamri]][gh-actions-url] | +| Julia 1.10 (Linux) | [![][gh-actions-komamri]][gh-actions-url] | +| Julia 1.10 (Mac OS) | [![][gh-actions-komamri]][gh-actions-url] | | Julia 1 (Windows) | [![][gh-actions-komamri]][gh-actions-url] | | Julia 1 (Linux) | [![][gh-actions-komamri]][gh-actions-url] | | Julia 1 (Mac OS) | [![][gh-actions-komamri]][gh-actions-url] | @@ -180,6 +180,7 @@ If you see any problem with this information, please let us know in a GitHub iss [julia-19]: https://img.shields.io/badge/julia-v1.9-9558B2?logo=julia +[julia-110]: https://img.shields.io/badge/julia-v1.10-9558B2?logo=julia [komamri-version]: https://juliahub.com/docs/General/KomaMRI/stable/version.svg?color=blue [komabase-version]: https://juliahub.com/docs/General/KomaMRIBase/stable/version.svg @@ -204,11 +205,11 @@ If you see any problem with this information, please let us know in a GitHub iss [apple-stable]: https://badge.buildkite.com/f3c2e589ac0c1310cda3c2092814e33ac9db15b4f103eb572b.svg?branch=master&step=Metal%3A%20Run%20tests%20on%20v1 [intel-stable]: https://badge.buildkite.com/f3c2e589ac0c1310cda3c2092814e33ac9db15b4f103eb572b.svg?branch=master&step=oneAPI%3A%20Run%20tests%20on%20v1 -[cpu-compat]: https://badge.buildkite.com/f3c2e589ac0c1310cda3c2092814e33ac9db15b4f103eb572b.svg?branch=master&step=CPU%3A%20Run%20tests%20on%20v1.9 -[nvidia-compat]: https://badge.buildkite.com/f3c2e589ac0c1310cda3c2092814e33ac9db15b4f103eb572b.svg?branch=master&step=CUDA%3A%20Run%20tests%20on%20v1.9 -[amd-compat]: https://badge.buildkite.com/sample.svg?status=failing -[apple-compat]: https://badge.buildkite.com/f3c2e589ac0c1310cda3c2092814e33ac9db15b4f103eb572b.svg?branch=master&step=Metal%3A%20Run%20tests%20on%20v1.9 -[intel-compat]: https://badge.buildkite.com/f3c2e589ac0c1310cda3c2092814e33ac9db15b4f103eb572b.svg?branch=master&step=oneAPI%3A%20Run%20tests%20on%20v1.9 +[cpu-compat]: https://badge.buildkite.com/f3c2e589ac0c1310cda3c2092814e33ac9db15b4f103eb572b.svg?branch=master&step=CPU%3A%20Run%20tests%20on%20v1.10 +[nvidia-compat]: https://badge.buildkite.com/f3c2e589ac0c1310cda3c2092814e33ac9db15b4f103eb572b.svg?branch=master&step=CUDA%3A%20Run%20tests%20on%20v1.10 +[amd-compat]: https://badge.buildkite.com/f3c2e589ac0c1310cda3c2092814e33ac9db15b4f103eb572b.svg?branch=master&step=AMDGPU%3A%20Run%20tests%20on%20v1.10 +[apple-compat]: https://badge.buildkite.com/f3c2e589ac0c1310cda3c2092814e33ac9db15b4f103eb572b.svg?branch=master&step=Metal%3A%20Run%20tests%20on%20v1.10 +[intel-compat]: https://badge.buildkite.com/f3c2e589ac0c1310cda3c2092814e33ac9db15b4f103eb572b.svg?branch=master&step=oneAPI%3A%20Run%20tests%20on%20v1.10 [buildkite-url]: https://buildkite.com/julialang/komamri-dot-jl/builds?branch=master From 32cd42d24e0f8e23b8a296d912347656cdd6aa42 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Fri, 13 Sep 2024 20:31:41 +0200 Subject: [PATCH 72/91] Requested changes --- KomaMRIBase/src/KomaMRIBase.jl | 2 +- KomaMRIBase/src/datatypes/Phantom.jl | 34 +++--- KomaMRIBase/src/motion/MotionSet.jl | 5 +- .../motionlist/{ActionSpan.jl => Action.jl} | 4 +- KomaMRIBase/src/motion/motionlist/Motion.jl | 10 +- .../src/motion/motionlist/MotionList.jl | 17 +-- KomaMRIBase/src/motion/motionlist/SpinSpan.jl | 49 ++++---- .../motionlist/actions/ArbitraryAction.jl | 6 +- .../motion/motionlist/actions/SimpleAction.jl | 6 +- KomaMRIBase/src/motion/nomotion/NoMotion.jl | 26 ++-- KomaMRICore/ext/KomaoneAPIExt.jl | 3 +- KomaMRICore/src/simulation/Flow.jl | 8 +- KomaMRICore/src/simulation/Functors.jl | 4 +- .../simulation/SimMethods/Bloch/BlochGPU.jl | 2 +- .../SimMethods/BlochSimple/BlochSimple.jl | 2 +- .../simulation/SimMethods/Magnetization.jl | 7 +- .../simulation/SimMethods/SimulationMethod.jl | 1 - KomaMRICore/test/runtests.jl | 111 +++++++++--------- KomaMRICore/test/test_files/utils.jl | 2 + KomaMRIFiles/src/Phantom/Phantom.jl | 13 +- KomaMRIPlots/src/ui/DisplayFunctions.jl | 42 +++---- docs/src/reference/2-koma-base.md | 3 +- 22 files changed, 165 insertions(+), 192 deletions(-) rename KomaMRIBase/src/motion/motionlist/{ActionSpan.jl => Action.jl} (55%) diff --git a/KomaMRIBase/src/KomaMRIBase.jl b/KomaMRIBase/src/KomaMRIBase.jl index c1839b3cf..7a4982c6e 100644 --- a/KomaMRIBase/src/KomaMRIBase.jl +++ b/KomaMRIBase/src/KomaMRIBase.jl @@ -54,7 +54,7 @@ export Rotate, RotateX, RotateY, RotateZ export HeartBeat, Path, FlowPath export TimeRange, Periodic export SpinRange, AllSpins -export sort_motions!, get_spin_coords +export get_spin_coords # Secondary export get_kspace, rotx, roty, rotz # Additionals diff --git a/KomaMRIBase/src/datatypes/Phantom.jl b/KomaMRIBase/src/datatypes/Phantom.jl index d6f65aebb..0e580bcf0 100644 --- a/KomaMRIBase/src/datatypes/Phantom.jl +++ b/KomaMRIBase/src/datatypes/Phantom.jl @@ -50,6 +50,9 @@ julia> obj.ρ motion::AbstractMotionSet{T} = NoMotion{eltype(x)}() end +non_string_phantom_fields = Iterators.filter(x -> fieldtype(Phantom, x) != String, fieldnames(Phantom)) +vector_phantom_fields = Iterators.filter(x -> fieldtype(Phantom, x) <: AbstractVector, fieldnames(Phantom)) + """Size and length of a phantom""" size(x::Phantom) = size(x.ρ) Base.length(x::Phantom) = length(x.ρ) @@ -58,37 +61,29 @@ Base.iterate(x::Phantom) = (x[1], 2) Base.iterate(x::Phantom, i::Integer) = (i <= length(x)) ? (x[i], i + 1) : nothing Base.lastindex(x::Phantom) = length(x) Base.getindex(x::Phantom, i::Integer) = x[i:i] -Base.getindex(x::Phantom, c::Colon) = x[1:length(x)] Base.view(x::Phantom, i::Integer) = @view(x[i:i]) -Base.view(x::Phantom, c::Colon) = @view(x[1:length(x)]) """Compare two phantoms""" function Base.:(==)(obj1::Phantom, obj2::Phantom) - return reduce( - &, - [getfield(obj1, field) == getfield(obj2, field) for - field in Iterators.filter(x -> !(x == :name), fieldnames(Phantom))], - ) + if length(obj1) != length(obj2) return false end + return reduce(&, [getfield(obj1, field) == getfield(obj2, field) for field in non_string_phantom_fields]) end function Base.:(≈)(obj1::Phantom, obj2::Phantom) - return reduce( - &, - [getfield(obj1, field) ≈ getfield(obj2, field) for - field in Iterators.filter(x -> !(x == :name), fieldnames(Phantom))], - ) + if length(obj1) != length(obj2) return false end + return reduce(&, [getfield(obj1, field) ≈ getfield(obj2, field) for field in non_string_phantom_fields]) end """Separate object spins in a sub-group""" -Base.getindex(obj::Phantom, p::AbstractVector) = begin +function Base.getindex(obj::Phantom, p) fields = [] - for field in Iterators.filter(x -> x != :name, fieldnames(Phantom)) + for field in non_string_phantom_fields push!(fields, (field, getfield(obj, field)[p])) end return Phantom(; name=obj.name, fields...) end -Base.view(obj::Phantom, p::AbstractVector) = begin +function Base.view(obj::Phantom, p) fields = [] - for field in Iterators.filter(x -> x != :name, fieldnames(Phantom)) + for field in non_string_phantom_fields push!(fields, (field, @view(getfield(obj, field)[p]))) end return Phantom(; name=obj.name, fields...) @@ -96,10 +91,9 @@ end """Addition of phantoms""" +(obj1::Phantom, obj2::Phantom) = begin - Nmaxchars = 50 - name = first(obj1.name * "+" * obj2.name, Nmaxchars) + name = first(obj1.name * "+" * obj2.name, 50) # The name is limited to 50 characters fields = [] - for field in Iterators.filter(x -> !(x in (:name, :motion)), fieldnames(Phantom)) + for field in vector_phantom_fields push!(fields, (field, [getfield(obj1, field); getfield(obj2, field)])) end return Phantom(; @@ -326,7 +320,7 @@ function brain_phantom2D(; axis="axial", ss=4, us=1) end """ - obj = brain_phantom3D(; ss=4, us=1) + obj = brain_phantom3D(; ss=4, us=1, start_end=[160,200]) Creates a three-dimentional brain Phantom struct. Default ss=4 sample spacing is 2 mm. Original file (ss=1) sample spacing is .5 mm. diff --git a/KomaMRIBase/src/motion/MotionSet.jl b/KomaMRIBase/src/motion/MotionSet.jl index aebfafabe..c693eaba8 100644 --- a/KomaMRIBase/src/motion/MotionSet.jl +++ b/KomaMRIBase/src/motion/MotionSet.jl @@ -4,9 +4,8 @@ abstract type AbstractMotionSet{T<:Real} end include("nomotion/NoMotion.jl") # MotionList -include("motionlist/ActionSpan.jl") +include("motionlist/Action.jl") include("motionlist/SpinSpan.jl") include("motionlist/TimeSpan.jl") include("motionlist/Motion.jl") -include("motionlist/MotionList.jl") - +include("motionlist/MotionList.jl") \ No newline at end of file diff --git a/KomaMRIBase/src/motion/motionlist/ActionSpan.jl b/KomaMRIBase/src/motion/motionlist/Action.jl similarity index 55% rename from KomaMRIBase/src/motion/motionlist/ActionSpan.jl rename to KomaMRIBase/src/motion/motionlist/Action.jl index 4cf1a778c..a47143e49 100644 --- a/KomaMRIBase/src/motion/motionlist/ActionSpan.jl +++ b/KomaMRIBase/src/motion/motionlist/Action.jl @@ -1,6 +1,6 @@ -abstract type AbstractActionSpan{T<:Real} end +abstract type AbstractAction{T<:Real} end -is_composable(m::AbstractActionSpan) = false +is_composable(m::AbstractAction) = false # Simple actions include("actions/SimpleAction.jl") diff --git a/KomaMRIBase/src/motion/motionlist/Motion.jl b/KomaMRIBase/src/motion/motionlist/Motion.jl index 4355c88dc..fdc81415f 100644 --- a/KomaMRIBase/src/motion/motionlist/Motion.jl +++ b/KomaMRIBase/src/motion/motionlist/Motion.jl @@ -10,7 +10,7 @@ which the motion takes place, and `spins`, which indicates the spins that are affected by that motion. # Arguments -- `action`: (`::AbstractActionSpan{T<:Real}`) action, such as [`Translate`](@ref) or [`Rotate`](@ref) +- `action`: (`::AbstractAction{T<:Real}`) action, such as [`Translate`](@ref) or [`Rotate`](@ref) - `time`: (`::AbstractTimeSpan{T<:Real}`, `=TimeRange(0.0)`) time information about the motion - `spins`: (`::AbstractSpinSpan`, `=AllSpins()`) spin indexes affected by the motion @@ -27,7 +27,7 @@ julia> motion = Motion( ``` """ @with_kw mutable struct Motion{T<:Real} - action::AbstractActionSpan{T} + action::AbstractAction{T} time ::AbstractTimeSpan{T} = TimeRange(zero(typeof(action).parameters[1])) spins ::AbstractSpinSpan = AllSpins() end @@ -162,16 +162,16 @@ Base.:(==)(m1::Motion, m2::Motion) = (typeof(m1) == typeof(m2)) & reduce(&, [get Base.:(≈)(m1::Motion, m2::Motion) = (typeof(m1) == typeof(m2)) & reduce(&, [getfield(m1, field) ≈ getfield(m2, field) for field in fieldnames(typeof(m1))]) """ Motion sub-group """ -function Base.getindex(m::Motion, p::AbstractVector) +function Base.getindex(m::Motion, p) idx, spin_range = m.spins[p] return Motion(m.action[idx], m.time, spin_range) end -function Base.view(m::Motion, p::AbstractVector) +function Base.view(m::Motion, p) idx, spin_range = @view(m.spins[p]) return Motion(@view(m.action[idx]), m.time, spin_range) end # Auxiliary functions times(m::Motion) = times(m.time) -add_motion!(motion_array, motion) = has_spins(motion.spins) ? push!(motion_array, motion) : nothing +add_motion!(motion_array, motion) = typeof(motion.spins) <: SpinRange ? push!(motion_array, motion) : nothing is_composable(m::Motion) = is_composable(m.action) \ No newline at end of file diff --git a/KomaMRIBase/src/motion/motionlist/MotionList.jl b/KomaMRIBase/src/motion/motionlist/MotionList.jl index 618e71025..ef8c2a3d7 100644 --- a/KomaMRIBase/src/motion/motionlist/MotionList.jl +++ b/KomaMRIBase/src/motion/motionlist/MotionList.jl @@ -35,14 +35,14 @@ end MotionList(motions...) = length([motions]) > 0 ? MotionList([motions...]) : @error "You must provide at least one motion as input argument. If you do not want to define motion, use `NoMotion{T}()`" """ MotionList sub-group """ -function Base.getindex(mv::MotionList{T}, p::AbstractVector) where {T<:Real} +function Base.getindex(mv::MotionList{T}, p) where {T<:Real} motion_array_aux = Motion{T}[] for m in mv.motions add_motion!(motion_array_aux, m[p]) end return length(motion_array_aux) > 0 ? MotionList(motion_array_aux) : NoMotion{T}() end -function Base.view(mv::MotionList{T}, p::AbstractVector) where {T<:Real} +function Base.view(mv::MotionList{T}, p) where {T<:Real} motion_array_aux = Motion{T}[] for m in mv.motions add_motion!(motion_array_aux, @view(m[p])) @@ -95,7 +95,7 @@ For each dimension (x, y, z), the output matrix has ``N_{\t{spins}}`` rows and ` - `x`: (`::AbstractVector{T<:Real}`, `[m]`) spin x-position vector - `y`: (`::AbstractVector{T<:Real}`, `[m]`) spin y-position vector - `z`: (`::AbstractVector{T<:Real}`, `[m]`) spin z-position vector -- `t`: (`::AbstractArray{T<:Real}`) horizontal array of time instants +- `t`: horizontal array of time instants # Returns - `x, y, z`: (`::Tuple{AbstractArray, AbstractArray, AbstractArray}`) spin positions over time @@ -103,6 +103,8 @@ For each dimension (x, y, z), the output matrix has ``N_{\t{spins}}`` rows and ` function get_spin_coords( ml::MotionList{T}, x::AbstractVector{T}, y::AbstractVector{T}, z::AbstractVector{T}, t ) where {T<:Real} + # Sort motions + sort_motions!(ml) # Buffers for positions: xt, yt, zt = x .+ 0*t, y .+ 0*t, z .+ 0*t # Buffers for displacements: @@ -134,12 +136,13 @@ end times = times(motion) """ function times(ml::MotionList{T}) where {T<:Real} - nodes = reduce(vcat, [times(m) for m in ml.motions]; init=[zero(T)]) + nodes = reduce(vcat, [times(m) for m in ml.motions]) return unique(sort(nodes)) end """ sort_motions!(motionset) + Sorts motions in a list according to their starting time. It modifies the original list. If `motionset::NoMotion`, this function does nothing. If `motionset::MotionList`, this function sorts its motions. @@ -150,9 +153,7 @@ If `motionset::MotionList`, this function sorts its motions. # Returns - `nothing` """ -function sort_motions!(mv::MotionList{T}) where {T<:Real} - sort!(mv.motions; by=m -> times(m)[1]) +function sort_motions!(m::MotionList) + sort!(m.motions; by=m -> times(m)[1]) return nothing end - - diff --git a/KomaMRIBase/src/motion/motionlist/SpinSpan.jl b/KomaMRIBase/src/motion/motionlist/SpinSpan.jl index c4ee26d15..e1566621a 100644 --- a/KomaMRIBase/src/motion/motionlist/SpinSpan.jl +++ b/KomaMRIBase/src/motion/motionlist/SpinSpan.jl @@ -16,12 +16,11 @@ julia> allspins = AllSpins() """ struct AllSpins <: AbstractSpinSpan end -Base.getindex(spins::AllSpins, p::AbstractVector) = p, spins -Base.view(spins::AllSpins, p::AbstractVector) = p, spins - +# Functions +Base.getindex(spins::AllSpins, p) = p, spins +Base.view(spins::AllSpins, p) = p, spins get_idx(spins::AllSpins) = Colon() -has_spins(spins::AllSpins) = true - +expand(sr::AllSpins, Ns::Int) = SpinRange(1:Ns) """ spinrange = SpinRange(range) @@ -43,36 +42,30 @@ julia> spinrange = SpinRange(obj.x .> 0) ``` """ @with_kw struct SpinRange <: AbstractSpinSpan - range::AbstractVector + range::AbstractRange end +# Constructors SpinRange(c::Colon) = AllSpins() -SpinRange(range::BitVector) = SpinRange(findall(x->x==true, range)) +SpinRange(b::BitVector) = SpinRange(findall(x->x==true, b)) +SpinRange(v::Vector) = begin + step = v[2] - v[1] + r = step == 1 ? (v[1]:v[end]) : (v[1]:step:v[end]) + @assert r == v "Cannot create a SpinRange with indices that are not evenly spaced (e.g., [1, 3, 4])." + return SpinRange(r) +end -function Base.getindex(spins::SpinRange, p::AbstractVector) - idx = get_idx(spins.range, p) - return get_idx(p, spins.range), SpinRange(idx) +# Functions +function Base.getindex(spins::SpinRange, p) + idx = intersect_idx(spins.range, p) + return intersect_idx(p, spins.range), SpinRange(idx) end -function Base.view(spins::SpinRange, p::AbstractVector) - idx = get_idx(spins.range, p) - return get_idx(p, spins.range), SpinRange(idx) +function Base.view(spins::SpinRange, p) + idx = intersect_idx(spins.range, p) + return intersect_idx(p, spins.range), SpinRange(idx) end - -Base.getindex(spins::SpinRange, b::BitVector) = spins[findall(x->x==true, b)] -Base.view(spins::SpinRange, b::BitVector) = @view(spins[findall(x->x==true, b)]) - Base.:(==)(sr1::SpinRange, sr2::SpinRange) = sr1.range == sr2.range - Base.length(sr::SpinRange) = length(sr.range) - get_idx(spins::SpinRange) = spins.range -has_spins(spins::SpinRange) = length(spins.range) > 0 - -# Auxiliary functions -function get_idx(spin_range::AbstractVector, p::AbstractVector) - idx = findall(x -> x in spin_range, p) - return (length(idx) > 0 && idx == collect(first(idx):last(idx))) ? (first(idx):last(idx)) : idx -end - expand(sr::SpinRange, Ns::Int) = sr -expand(sr::AllSpins, Ns::Int) = SpinRange(1:Ns) \ No newline at end of file +intersect_idx(a, b) = findall(x -> x in a, b) diff --git a/KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl b/KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl index a08a7f71d..df864f341 100644 --- a/KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl @@ -16,12 +16,12 @@ const Interpolator2D = Interpolations.GriddedInterpolation{ K<:Tuple{AbstractVector{T}, AbstractVector{T}}, } -abstract type ArbitraryAction{T<:Real} <: AbstractActionSpan{T} end +abstract type ArbitraryAction{T<:Real} <: AbstractAction{T} end -function Base.getindex(action::ArbitraryAction, p::Union{AbstractVector, Colon}) +function Base.getindex(action::ArbitraryAction, p) return typeof(action)([getfield(action, d)[p,:] for d in fieldnames(typeof(action))]...) end -function Base.view(action::ArbitraryAction, p::Union{AbstractVector, Colon}) +function Base.view(action::ArbitraryAction, p) return typeof(action)([@view(getfield(action, d)[p,:]) for d in fieldnames(typeof(action))]...) end diff --git a/KomaMRIBase/src/motion/motionlist/actions/SimpleAction.jl b/KomaMRIBase/src/motion/motionlist/actions/SimpleAction.jl index d2db8bfd8..031614f7f 100644 --- a/KomaMRIBase/src/motion/motionlist/actions/SimpleAction.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/SimpleAction.jl @@ -1,7 +1,7 @@ -abstract type SimpleAction{T<:Real} <: AbstractActionSpan{T} end +abstract type SimpleAction{T<:Real} <: AbstractAction{T} end -Base.getindex(action::SimpleAction, p::Union{AbstractVector, Colon}) = action -Base.view(action::SimpleAction, p::Union{AbstractVector, Colon}) = action +Base.getindex(action::SimpleAction, p) = action +Base.view(action::SimpleAction, p) = action include("simpleactions/Translate.jl") include("simpleactions/Rotate.jl") diff --git a/KomaMRIBase/src/motion/nomotion/NoMotion.jl b/KomaMRIBase/src/motion/nomotion/NoMotion.jl index a909ed204..d139dc039 100644 --- a/KomaMRIBase/src/motion/nomotion/NoMotion.jl +++ b/KomaMRIBase/src/motion/nomotion/NoMotion.jl @@ -13,12 +13,13 @@ julia> nomotion = NoMotion{Float64}() """ struct NoMotion{T<:Real} <: AbstractMotionSet{T} end -Base.getindex(mv::NoMotion, p::AbstractVector) = mv -Base.view(mv::NoMotion, p::AbstractVector) = mv +Base.getindex(mv::NoMotion, p) = mv +Base.view(mv::NoMotion, p) = mv """ Addition of NoMotions """ -Base.vcat(m1::NoMotion{T}, m2::NoMotion{T}, Ns1::Int, Ns2::Int) where {T<:Real} = NoMotion{T}() -function Base.vcat(m1::NoMotion{T}, m2::AbstractMotionSet{T}, Ns1::Int, Ns2::Int) where {T<:Real} +Base.vcat(m1::NoMotion, m2::NoMotion, Ns1, Ns2) = m1 +Base.vcat(m1, m2::NoMotion, Ns1, Ns2) = vcat(m2, m1, 0, Ns1) +function Base.vcat(m1::NoMotion{T}, m2, Ns1, Ns2) where {T} mv_aux = Motion{T}[] for m in m2.motions m_aux = copy(m) @@ -28,16 +29,8 @@ function Base.vcat(m1::NoMotion{T}, m2::AbstractMotionSet{T}, Ns1::Int, Ns2::Int end return MotionList(mv_aux) end -function Base.vcat(m1::AbstractMotionSet{T}, m2::NoMotion{T}, Ns1::Int, Ns2::Int) where {T<:Real} - mv_aux = Motion{T}[] - for m in m1.motions - m_aux = copy(m) - m_aux.spins = expand(m_aux.spins, Ns1) - push!(mv_aux, m_aux) - end - return MotionList(mv_aux) -end +""" Compare two NoMotions """ Base.:(==)(m1::NoMotion{T}, m2::NoMotion{T}) where {T<:Real} = true Base.:(≈)(m1::NoMotion{T}, m2::NoMotion{T}) where {T<:Real} = true @@ -56,4 +49,9 @@ end """ times(mv::NoMotion{T}) where {T<:Real} = [zero(T)] -sort_motions!(mv::NoMotion) = nothing \ No newline at end of file +""" + sort_motions!(motionset) +""" +function sort_motions!(m::NoMotion) + return nothing +end diff --git a/KomaMRICore/ext/KomaoneAPIExt.jl b/KomaMRICore/ext/KomaoneAPIExt.jl index f2f13f4cd..c58469421 100644 --- a/KomaMRICore/ext/KomaoneAPIExt.jl +++ b/KomaMRICore/ext/KomaoneAPIExt.jl @@ -1,9 +1,8 @@ module KomaoneAPIExt using oneAPI -import KomaMRICore, KomaMRIBase +import KomaMRICore import Adapt -import LinearAlgebra KomaMRICore.name(::oneAPIBackend) = "oneAPI" KomaMRICore.isfunctional(::oneAPIBackend) = oneAPI.functional() diff --git a/KomaMRICore/src/simulation/Flow.jl b/KomaMRICore/src/simulation/Flow.jl index e724abdff..114592d20 100644 --- a/KomaMRICore/src/simulation/Flow.jl +++ b/KomaMRICore/src/simulation/Flow.jl @@ -1,11 +1,11 @@ """ reset_magnetization! """ -function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion::NoMotion{T}, t::AbstractArray{T}, ρ) where {T<:Real} +function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion, t, ρ) where {T<:Real} return nothing end -function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion::MotionList{T}, t::AbstractArray{T}, ρ) where {T<:Real} +function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion::MotionList{T}, t, ρ) where {T<:Real} for m in motion.motions t_unit = KomaMRIBase.unit_time(t, m.time) idx = KomaMRIBase.get_idx(m.spins) @@ -14,10 +14,6 @@ function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion: return nothing end -function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, action::KomaMRIBase.AbstractActionSpan{T}, t, ρ) where {T<:Real} - return nothing -end - function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, action::FlowPath{T}, t, ρ) where {T<:Real} itp = KomaMRIBase.interpolate(action.spin_reset, KomaMRIBase.Gridded(KomaMRIBase.Constant{KomaMRIBase.Previous}()), Val(size(action.spin_reset, 1))) flags = KomaMRIBase.resample(itp, t) diff --git a/KomaMRICore/src/simulation/Functors.jl b/KomaMRICore/src/simulation/Functors.jl index 753b0d3af..d785384bd 100644 --- a/KomaMRICore/src/simulation/Functors.jl +++ b/KomaMRICore/src/simulation/Functors.jl @@ -98,8 +98,8 @@ See also [`f32`](@ref). f64(m) = paramtype(Float64, m) # Koma motion-related adapts -adapt_storage(backend::KA.GPU, xs::MotionList) = MotionList(gpu.(xs.motions, Ref(backend))) -adapt_storage(backend::KA.GPU, xs::Motion) = Motion(gpu(xs.action, backend), gpu(xs.time, backend), xs.spins) +adapt_storage(backend::KA.GPU, xs::MotionList) = MotionList(adapt.(Ref(backend), xs.motions)) +adapt_storage(backend::KA.GPU, xs::Motion) = Motion(adapt(backend, xs.action), adapt(backend, xs.time), xs.spins) adapt_storage(T::Type{<:Real}, xs::NoMotion) = NoMotion{T}() adapt_storage(T::Type{<:Real}, xs::MotionList) = MotionList(paramtype.(T, xs.motions)) adapt_storage(T::Type{<:Real}, xs::Motion) = Motion(paramtype(T, xs.action), paramtype(T, xs.time), xs.spins) diff --git a/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl b/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl index 1e98dd351..dd523791e 100644 --- a/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl +++ b/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl @@ -157,7 +157,7 @@ function run_spin_precession!( sig .= transpose(sum(pre.Mxy; dims=1)) end - + #Mxy precession and relaxation, and Mz relaxation M.z .= M.z .* exp.(-seq_block.dur ./ p.T1) .+ p.ρ .* (T(1) .- exp.(-seq_block.dur ./ p.T1)) M.xy .= M.xy .* exp.(-seq_block.dur ./ p.T2) .* _cis.(pre.ϕ[:,end]) diff --git a/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl b/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl index cd3b6e6a3..5370a2e78 100644 --- a/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl +++ b/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl @@ -44,7 +44,7 @@ function run_spin_precession!( #Mxy precession and relaxation, and Mz relaxation tp = cumsum(seq.Δt) # t' = t - t0 dur = sum(seq.Δt) # Total length, used for signal relaxation - Mxy = [M.xy M.xy .* exp.(-tp' ./ p.T2) .* (cos.(ϕ) .+ im .* sin.(ϕ))] #This assumes Δw and T2 are constant in time + Mxy = [M.xy M.xy .* exp.(-tp' ./ p.T2) .* cis.(ϕ)] #This assumes Δw and T2 are constant in time M.z .= M.z .* exp.(-dur ./ p.T1) .+ p.ρ .* (1 .- exp.(-dur ./ p.T1)) reset_magnetization!(M, Mxy, p.motion, seq.t', p.ρ) M.xy .= Mxy[:, end] diff --git a/KomaMRICore/src/simulation/SimMethods/Magnetization.jl b/KomaMRICore/src/simulation/SimMethods/Magnetization.jl index d718155fb..2bf353a38 100644 --- a/KomaMRICore/src/simulation/SimMethods/Magnetization.jl +++ b/KomaMRICore/src/simulation/SimMethods/Magnetization.jl @@ -19,11 +19,8 @@ end # M[i] Base.getindex(M::Mag, i::Integer) = Mag(M.xy[i,:], M.z[i,:]) # M[a:b] -Base.getindex(M::Mag, i::UnitRange) = Mag(M.xy[i], M.z[i]) -Base.view(M::Mag, i::UnitRange) = @views Mag(M.xy[i], M.z[i]) -# M[:] -Base.getindex(M::Mag, i::Colon) = M[1:length(M.z)] -Base.view(M::Mag, i::Colon) = @view(M[1:length(M.z)]) +Base.getindex(M::Mag, i) = Mag(M.xy[i], M.z[i]) +Base.view(M::Mag, i) = @views Mag(M.xy[i], M.z[i]) # Definition of rotation Spinor×SpinStateRepresentation @doc raw""" diff --git a/KomaMRICore/src/simulation/SimMethods/SimulationMethod.jl b/KomaMRICore/src/simulation/SimMethods/SimulationMethod.jl index 2f0735231..8326d0a85 100644 --- a/KomaMRICore/src/simulation/SimMethods/SimulationMethod.jl +++ b/KomaMRICore/src/simulation/SimMethods/SimulationMethod.jl @@ -21,7 +21,6 @@ function initialize_spins_state( Mxy = zeros(T, Nspins) Mz = obj.ρ Xt = Mag{T}(Mxy, Mz) - sort_motions!(obj.motion) return Xt, obj end diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index bffe795ce..9ab368fb9 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -238,9 +238,7 @@ end sig = @suppress simulate(obj, seq, sys; sim_params) sig = sig / prod(size(obj)) - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - - @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% + @test NRMSE(sig, sig_jemris) < 1 #NRMSE < 1% end @testitem "Bloch_RF_accuracy" tags=[:important, :core, :nomotion] begin @@ -287,7 +285,7 @@ end error1 = norm2(raw.profiles[2].data .- res1) ./ norm2(res1) * 100 error2 = norm2(raw.profiles[3].data .- res2) ./ norm2(res2) * 100 - @test error0 + error1 + error2 < 0.1 #NMRSE < 0.1% + @test error0 + error1 + error2 < 0.1 #NRMSE < 0.1% end @testitem "Bloch_phase_compensation" tags=[:important, :core, :nomotion] begin @@ -363,9 +361,7 @@ end sig = @suppress simulate(obj, seq, sys; sim_params) sig = sig / prod(size(obj)) - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - - @test NMRSE(sig, sig_jemris) < 1 #NMRSE < 1% + @test NRMSE(sig, sig_jemris) < 1 #NRMSE < 1% end @testitem "simulate_slice_profile" tags=[:core, :nomotion] begin @@ -422,7 +418,7 @@ end # --------- Motion-related tests ------------- # We compare with the result given by OrdinaryDiffEqTsit5 -@testitem "Motion" tags=[:core, :motion] begin +@testitem "Motion" tags=[:core, :motion] begin using Suppressor, OrdinaryDiffEqTsit5 include("initialize_backend.jl") @@ -453,55 +449,56 @@ end y0 = [0.1] z0 = [0.0] - for m in motions - motion = MotionList(m) - - coords(t) = get_spin_coords(motion, x0, y0, z0, t) - x(t) = (coords(t)[1])[1] - y(t) = (coords(t)[2])[1] - z(t) = (coords(t)[3])[1] - - ## Solving using DiffEquations.jl - function bloch!(dm, m, p, t) - mx, my, mz = m - bx, by, bz = [B1e(t) * cos(φ), B1e(t) * sin(φ), (x(t) * Gx + y(t) * Gy + z(t) * Gz)] - dm[1] = -mx / T2 + γ * bz * my - γ * by * mz - dm[2] = -γ * bz * mx - my / T2 + γ * bx * mz - dm[3] = γ * by * mx - γ * bx * my - mz / T1 + M0 / T1 - return nothing - end - m0 = [0.0, 0.0, 1.0] - tspan = (0.0, duration) - prob = ODEProblem(bloch!, m0, tspan) - # Only at ADC points - tadc = range(Trf, duration, Nadc) - sol = @time solve(prob, Tsit5(), saveat = tadc, abstol = 1e-9, reltol = 1e-9) - sol_diffeq = hcat(sol.u...)' - mxy_diffeq = sol_diffeq[:, 1] + im * sol_diffeq[:, 2] - - ## Solving using KomaMRICore.jl - # Creating Sequence - seq = Sequence() - seq += RF(cis(φ) .* B1, Trf) - seq.GR[1,1] = Grad(Gx, duration) - seq.GR[2,1] = Grad(Gy, duration) - seq.GR[3,1] = Grad(Gz, duration) - seq.ADC[1] = ADC(Nadc, duration-Trf, Trf) - # Creating object - obj = Phantom(x = x0, y = y0, z = z0, ρ = [M0], T1 = [T1], T2 = [T2], motion = motion) - # Scanner - sys = Scanner() - # Simulation - for sim_method in [KomaMRICore.Bloch(), KomaMRICore.BlochSimple(), KomaMRICore.BlochDict()] - sim_params = Dict{String, Any}( - "sim_method"=>sim_method, - "return_type"=>"mat" - ) - raw_aux = @suppress simulate(obj, seq, sys; sim_params) - raw = raw_aux[:, 1, 1] - - NMRSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. - @test NMRSE(raw, mxy_diffeq) < 1 + for sim_method in [KomaMRICore.Bloch(), KomaMRICore.BlochSimple(), KomaMRICore.BlochDict()] + @testset "$(typeof(sim_method))" begin + for m in motions + motion = MotionList(m) + + coords(t) = get_spin_coords(motion, x0, y0, z0, t) + x(t) = (coords(t)[1])[1] + y(t) = (coords(t)[2])[1] + z(t) = (coords(t)[3])[1] + + ## Solving using DiffEquations.jl + function bloch!(dm, m, p, t) + mx, my, mz = m + bx, by, bz = [B1e(t) * cos(φ), B1e(t) * sin(φ), (x(t) * Gx + y(t) * Gy + z(t) * Gz)] + dm[1] = -mx / T2 + γ * bz * my - γ * by * mz + dm[2] = -γ * bz * mx - my / T2 + γ * bx * mz + dm[3] = γ * by * mx - γ * bx * my - mz / T1 + M0 / T1 + return nothing + end + m0 = [0.0, 0.0, 1.0] + tspan = (0.0, duration) + prob = ODEProblem(bloch!, m0, tspan) + # Only at ADC points + tadc = range(Trf, duration, Nadc) + sol = @time solve(prob, Tsit5(), saveat = tadc, abstol = 1e-9, reltol = 1e-9) + sol_diffeq = hcat(sol.u...)' + mxy_diffeq = sol_diffeq[:, 1] + im * sol_diffeq[:, 2] + + ## Solving using KomaMRICore.jl + # Creating Sequence + seq = Sequence() + seq += RF(cis(φ) .* B1, Trf) + seq.GR[1,1] = Grad(Gx, duration) + seq.GR[2,1] = Grad(Gy, duration) + seq.GR[3,1] = Grad(Gz, duration) + seq.ADC[1] = ADC(Nadc, duration-Trf, Trf) + # Creating object + obj = Phantom(x = x0, y = y0, z = z0, ρ = [M0], T1 = [T1], T2 = [T2], motion = motion) + # Scanner + sys = Scanner() + # Simulation + sim_params = Dict{String, Any}( + "sim_method"=>sim_method, + "return_type"=>"mat", + "gpu" => USE_GPU + ) + raw_aux = @suppress simulate(obj, seq, sys; sim_params) + raw = raw_aux[:, 1, 1] + @test NRMSE(raw, mxy_diffeq) < 1 + end end end end diff --git a/KomaMRICore/test/test_files/utils.jl b/KomaMRICore/test/test_files/utils.jl index 012bc3a0c..769eddc34 100644 --- a/KomaMRICore/test/test_files/utils.jl +++ b/KomaMRICore/test/test_files/utils.jl @@ -157,3 +157,5 @@ function seq_epi_100x100_TE100_FOV230() seq = ex + dephaser + delayTE + epi return seq end + +NRMSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. \ No newline at end of file diff --git a/KomaMRIFiles/src/Phantom/Phantom.jl b/KomaMRIFiles/src/Phantom/Phantom.jl index d3a848061..6aac7400b 100644 --- a/KomaMRIFiles/src/Phantom/Phantom.jl +++ b/KomaMRIFiles/src/Phantom/Phantom.jl @@ -10,11 +10,9 @@ function read_phantom(filename::String) # Version file_version = VersionNumber(read_attribute(fid, "Version")) program_version = pkgversion(KomaMRIFiles) - if file_version.major != program_version.major | file_version.minor != program_version.minor - @warn "Version mismatch detected: - File version: $file_version - KomaMRIFiles version: $program_version - This may lead to compatibility issues. Please update the file or the program to the matching version." + if file_version.major != program_version.major + @warn "Version mismatch detected: $file_version (file) vs $program_version (Koma) + This may lead to compatibility issues. Please update the file or the program." end phantom_fields = [] # Name @@ -107,7 +105,8 @@ function write_phantom( obj::Phantom, filename::String; store_coords=[:x, :y, :z], - store_contrasts=[:ρ, :T1, :T2, :T2s, :Δw] + store_contrasts=[:ρ, :T1, :T2, :T2s, :Δw], + store_motion=true ) # Create HDF5 phantom file fid = h5open(filename, "w") @@ -128,7 +127,7 @@ function write_phantom( contrast[String(x)] = getfield(obj, x) end # Motion - if typeof(obj.motion) <: MotionList + if (typeof(obj.motion) <: MotionList) & store_motion motion_group = create_group(fid, "motion") export_motion!(motion_group, obj.motion) end diff --git a/KomaMRIPlots/src/ui/DisplayFunctions.jl b/KomaMRIPlots/src/ui/DisplayFunctions.jl index f361d2f7a..cdfbeb884 100644 --- a/KomaMRIPlots/src/ui/DisplayFunctions.jl +++ b/KomaMRIPlots/src/ui/DisplayFunctions.jl @@ -1019,12 +1019,12 @@ julia> plot_phantom_map(obj3D, :ρ) ``` """ function plot_phantom_map( - ph::Phantom, + obj::Phantom, key::Symbol; height=700, width=nothing, darkmode=false, - view_2d=sum(KomaMRIBase.get_dims(ph)) < 3, + view_2d=sum(KomaMRIBase.get_dims(obj)) < 3, colorbar=true, intermediate_time_samples=0, max_time_samples=100, @@ -1033,7 +1033,7 @@ function plot_phantom_map( kwargs..., ) - function interpolate_times(motion::KomaMRIBase.AbstractMotionSet{T}) where {T<:Real} + function interpolate_times(motion) t = times(motion) if length(t)>1 # Interpolate time points (as many as indicated by intermediate_time_samples) @@ -1043,35 +1043,35 @@ function plot_phantom_map( return t end - function process_times(motion::KomaMRIBase.AbstractMotionSet{T}) where {T<:Real} - sort_motions!(motion) + function process_times(motion) + KomaMRIBase.sort_motions!(motion) t = interpolate_times(motion) # Decimate time points so their number is smaller than max_time_samples ss = length(t) > max_time_samples ? length(t) ÷ max_time_samples : 1 return t[1:ss:end] end - function decimate_uniform_phantom(ph::Phantom, num_points::Int) - dimx, dimy, dimz = KomaMRIBase.get_dims(ph) - ss = Int(ceil((length(ph) / num_points)^(1 / sum(KomaMRIBase.get_dims(ph))))) + function decimate_uniform_phantom(obj, num_points::Int) + dimx, dimy, dimz = KomaMRIBase.get_dims(obj) + ss = Int(ceil((length(obj) / num_points)^(1 / sum(KomaMRIBase.get_dims(obj))))) ssx = dimx ? ss : 1 ssy = dimy ? ss : 1 ssz = dimz ? ss : 1 - ix = sortperm(ph.x)[1:ssx:end] - iy = sortperm(ph.y)[1:ssy:end] - iz = sortperm(ph.z)[1:ssz:end] + ix = sortperm(obj.x)[1:ssx:end] + iy = sortperm(obj.y)[1:ssy:end] + iz = sortperm(obj.z)[1:ssz:end] idx = intersect(ix, iy, iz) - return ph[idx] + return obj[idx] end - if length(ph) > max_spins - ph = decimate_uniform_phantom(ph, max_spins) + if length(obj) > max_spins + obj = decimate_uniform_phantom(obj, max_spins) @warn "For performance reasons, the number of displayed spins was capped to `max_spins`=$(max_spins)." end path = @__DIR__ - cmin_key = minimum(getproperty(ph, key)) - cmax_key = maximum(getproperty(ph, key)) + cmin_key = minimum(getproperty(obj, key)) + cmax_key = maximum(getproperty(obj, key)) if key == :T1 || key == :T2 || key == :T2s cmin_key = 0 factor = 1e3 @@ -1118,8 +1118,8 @@ function plot_phantom_map( cmin_key = get(kwargs, :cmin, factor * cmin_key) cmax_key = get(kwargs, :cmax, factor * cmax_key) - t = process_times(ph.motion) - x, y, z = get_spin_coords(ph.motion, ph.x, ph.y, ph.z, t') + t = process_times(obj.motion) + x, y, z = get_spin_coords(obj.motion, obj.x, obj.y, obj.z, t') x0 = -maximum(abs.([x y z])) * 1e2 xf = maximum(abs.([x y z])) * 1e2 @@ -1131,7 +1131,7 @@ function plot_phantom_map( y=(y[:, 1]) * 1e2, mode="markers", marker=attr(; - color=getproperty(ph, key) * factor, + color=getproperty(obj, key) * factor, showscale=colorbar, colorscale=colormap, colorbar=attr(; ticksuffix=unit, title=string(key)), @@ -1176,7 +1176,7 @@ function plot_phantom_map( size=2, ), showlegend=false, - text=round.(getproperty(ph, key) * factor, digits=4), + text=round.(getproperty(obj, key) * factor, digits=4), hovertemplate="x: %{x:.1f} cm
y: %{y:.1f} cm
z: %{z:.1f} cm
$(string(key)): %{text}$unit", ), ] @@ -1260,7 +1260,7 @@ function plot_phantom_map( #Layout bgcolor, text_color, plot_bgcolor, grid_color, sep_color = theme_chooser(darkmode) l = Layout(; - title=ph.name * ": " * string(key), + title=obj.name * ": " * string(key), xaxis_title="x", yaxis_title="y", xaxis_range=[x0, xf], diff --git a/docs/src/reference/2-koma-base.md b/docs/src/reference/2-koma-base.md index 07bb22ae7..d410c0836 100644 --- a/docs/src/reference/2-koma-base.md +++ b/docs/src/reference/2-koma-base.md @@ -26,7 +26,6 @@ heart_phantom ```@docs NoMotion MotionList -sort_motions! get_spin_coords ``` @@ -36,7 +35,7 @@ get_spin_coords Motion ``` -### `AbstractActionSpan` types +### `AbstractAction` types ```@docs Translate From e5c9dd87846fd67fab7dd908f339bb0038a941ae Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Date: Sun, 15 Sep 2024 02:12:06 +0200 Subject: [PATCH 73/91] consts in upper case --- KomaMRIBase/src/datatypes/Phantom.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/KomaMRIBase/src/datatypes/Phantom.jl b/KomaMRIBase/src/datatypes/Phantom.jl index 0e580bcf0..68aa420fc 100644 --- a/KomaMRIBase/src/datatypes/Phantom.jl +++ b/KomaMRIBase/src/datatypes/Phantom.jl @@ -50,8 +50,8 @@ julia> obj.ρ motion::AbstractMotionSet{T} = NoMotion{eltype(x)}() end -non_string_phantom_fields = Iterators.filter(x -> fieldtype(Phantom, x) != String, fieldnames(Phantom)) -vector_phantom_fields = Iterators.filter(x -> fieldtype(Phantom, x) <: AbstractVector, fieldnames(Phantom)) +const NON_STRING_PHANTOM_FIELDS = Iterators.filter(x -> fieldtype(Phantom, x) != String, fieldnames(Phantom)) +const VECTOR_PHANTOM_FIELDS = Iterators.filter(x -> fieldtype(Phantom, x) <: AbstractVector, fieldnames(Phantom)) """Size and length of a phantom""" size(x::Phantom) = size(x.ρ) @@ -66,24 +66,24 @@ Base.view(x::Phantom, i::Integer) = @view(x[i:i]) """Compare two phantoms""" function Base.:(==)(obj1::Phantom, obj2::Phantom) if length(obj1) != length(obj2) return false end - return reduce(&, [getfield(obj1, field) == getfield(obj2, field) for field in non_string_phantom_fields]) + return reduce(&, [getfield(obj1, field) == getfield(obj2, field) for field in NON_STRING_PHANTOM_FIELDS]) end function Base.:(≈)(obj1::Phantom, obj2::Phantom) if length(obj1) != length(obj2) return false end - return reduce(&, [getfield(obj1, field) ≈ getfield(obj2, field) for field in non_string_phantom_fields]) + return reduce(&, [getfield(obj1, field) ≈ getfield(obj2, field) for field in NON_STRING_PHANTOM_FIELDS]) end """Separate object spins in a sub-group""" function Base.getindex(obj::Phantom, p) fields = [] - for field in non_string_phantom_fields + for field in NON_STRING_PHANTOM_FIELDS push!(fields, (field, getfield(obj, field)[p])) end return Phantom(; name=obj.name, fields...) end function Base.view(obj::Phantom, p) fields = [] - for field in non_string_phantom_fields + for field in NON_STRING_PHANTOM_FIELDS push!(fields, (field, @view(getfield(obj, field)[p]))) end return Phantom(; name=obj.name, fields...) @@ -93,7 +93,7 @@ end +(obj1::Phantom, obj2::Phantom) = begin name = first(obj1.name * "+" * obj2.name, 50) # The name is limited to 50 characters fields = [] - for field in vector_phantom_fields + for field in VECTOR_PHANTOM_FIELDS push!(fields, (field, [getfield(obj1, field); getfield(obj2, field)])) end return Phantom(; From 0b1f31acfd821fa464707a596e918e2664d732ff Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Date: Sun, 15 Sep 2024 02:13:00 +0200 Subject: [PATCH 74/91] Fix and rename aux motion functions --- KomaMRIBase/src/motion/motionlist/Motion.jl | 1 - KomaMRIBase/src/motion/motionlist/MotionList.jl | 8 ++++---- KomaMRIBase/src/motion/motionlist/SpinSpan.jl | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/KomaMRIBase/src/motion/motionlist/Motion.jl b/KomaMRIBase/src/motion/motionlist/Motion.jl index fdc81415f..7fbd2ec74 100644 --- a/KomaMRIBase/src/motion/motionlist/Motion.jl +++ b/KomaMRIBase/src/motion/motionlist/Motion.jl @@ -173,5 +173,4 @@ end # Auxiliary functions times(m::Motion) = times(m.time) -add_motion!(motion_array, motion) = typeof(motion.spins) <: SpinRange ? push!(motion_array, motion) : nothing is_composable(m::Motion) = is_composable(m.action) \ No newline at end of file diff --git a/KomaMRIBase/src/motion/motionlist/MotionList.jl b/KomaMRIBase/src/motion/motionlist/MotionList.jl index ef8c2a3d7..c0b5a1094 100644 --- a/KomaMRIBase/src/motion/motionlist/MotionList.jl +++ b/KomaMRIBase/src/motion/motionlist/MotionList.jl @@ -38,14 +38,14 @@ MotionList(motions...) = length([motions]) > 0 ? MotionList([motions...]) : @err function Base.getindex(mv::MotionList{T}, p) where {T<:Real} motion_array_aux = Motion{T}[] for m in mv.motions - add_motion!(motion_array_aux, m[p]) + push!(motion_array_aux, m[p]) end return length(motion_array_aux) > 0 ? MotionList(motion_array_aux) : NoMotion{T}() end function Base.view(mv::MotionList{T}, p) where {T<:Real} motion_array_aux = Motion{T}[] for m in mv.motions - add_motion!(motion_array_aux, @view(m[p])) + push!(motion_array_aux, @view(m[p])) end return length(motion_array_aux) > 0 ? MotionList(motion_array_aux) : NoMotion{T}() end @@ -112,7 +112,7 @@ function get_spin_coords( # Composable motions: they need to be run sequentially. Note that they depend on xt, yt, and zt for m in Iterators.filter(is_composable, ml.motions) t_unit = unit_time(t, m.time) - idx = get_idx(m.spins) + idx = get_indexing_range(m.spins) displacement_x!(@view(ux[idx, :]), m.action, @view(xt[idx, :]), @view(yt[idx, :]), @view(zt[idx, :]), t_unit) displacement_y!(@view(uy[idx, :]), m.action, @view(xt[idx, :]), @view(yt[idx, :]), @view(zt[idx, :]), t_unit) displacement_z!(@view(uz[idx, :]), m.action, @view(xt[idx, :]), @view(yt[idx, :]), @view(zt[idx, :]), t_unit) @@ -122,7 +122,7 @@ function get_spin_coords( # Additive motions: these motions can be run in parallel for m in Iterators.filter(!is_composable, ml.motions) t_unit = unit_time(t, m.time) - idx = get_idx(m.spins) + idx = get_indexing_range(m.spins) displacement_x!(@view(ux[idx, :]), m.action, @view(x[idx]), @view(y[idx]), @view(z[idx]), t_unit) displacement_y!(@view(uy[idx, :]), m.action, @view(x[idx]), @view(y[idx]), @view(z[idx]), t_unit) displacement_z!(@view(uz[idx, :]), m.action, @view(x[idx]), @view(y[idx]), @view(z[idx]), t_unit) diff --git a/KomaMRIBase/src/motion/motionlist/SpinSpan.jl b/KomaMRIBase/src/motion/motionlist/SpinSpan.jl index e1566621a..0ed4e7a08 100644 --- a/KomaMRIBase/src/motion/motionlist/SpinSpan.jl +++ b/KomaMRIBase/src/motion/motionlist/SpinSpan.jl @@ -19,7 +19,7 @@ struct AllSpins <: AbstractSpinSpan end # Functions Base.getindex(spins::AllSpins, p) = p, spins Base.view(spins::AllSpins, p) = p, spins -get_idx(spins::AllSpins) = Colon() +get_indexing_range(spins::AllSpins) = Colon() expand(sr::AllSpins, Ns::Int) = SpinRange(1:Ns) """ @@ -66,6 +66,6 @@ function Base.view(spins::SpinRange, p) end Base.:(==)(sr1::SpinRange, sr2::SpinRange) = sr1.range == sr2.range Base.length(sr::SpinRange) = length(sr.range) -get_idx(spins::SpinRange) = spins.range +get_indexing_range(spins::SpinRange) = spins.range expand(sr::SpinRange, Ns::Int) = sr intersect_idx(a, b) = findall(x -> x in a, b) From 717ee28ba1183d30b2b02357a6fc512e7e609f6a Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Date: Sun, 15 Sep 2024 02:14:02 +0200 Subject: [PATCH 75/91] Add functor to Motion. MotionList cannot have a functor --- KomaMRICore/src/simulation/Functors.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/KomaMRICore/src/simulation/Functors.jl b/KomaMRICore/src/simulation/Functors.jl index d785384bd..42b3a89ac 100644 --- a/KomaMRICore/src/simulation/Functors.jl +++ b/KomaMRICore/src/simulation/Functors.jl @@ -52,7 +52,7 @@ x = gpu(x, CUDABackend()) ``` """ function gpu(x, backend::KA.GPU) - fmap(x -> adapt(backend, x), x; exclude=_isleaf) + return fmap(x -> adapt(backend, x), x; exclude=_isleaf) end # To CPU @@ -98,8 +98,7 @@ See also [`f32`](@ref). f64(m) = paramtype(Float64, m) # Koma motion-related adapts -adapt_storage(backend::KA.GPU, xs::MotionList) = MotionList(adapt.(Ref(backend), xs.motions)) -adapt_storage(backend::KA.GPU, xs::Motion) = Motion(adapt(backend, xs.action), adapt(backend, xs.time), xs.spins) +adapt_storage(backend::KA.GPU, xs::MotionList) = MotionList(gpu.(xs.motions, Ref(backend))) adapt_storage(T::Type{<:Real}, xs::NoMotion) = NoMotion{T}() adapt_storage(T::Type{<:Real}, xs::MotionList) = MotionList(paramtype.(T, xs.motions)) adapt_storage(T::Type{<:Real}, xs::Motion) = Motion(paramtype(T, xs.action), paramtype(T, xs.time), xs.spins) @@ -107,6 +106,7 @@ adapt_storage(T::Type{<:Real}, xs::Motion) = Motion(paramtype(T, xs.action), par #The functor macro makes it easier to call a function in all the parameters # Phantom @functor Phantom +@functor Motion @functor Translate @functor Rotate @functor HeartBeat From d7666f69d7989d4d8fb45381b41505a466998890 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Date: Sun, 15 Sep 2024 02:14:21 +0200 Subject: [PATCH 76/91] Fix tests --- KomaMRICore/test/runtests.jl | 3 ++- KomaMRICore/test/test_files/utils.jl | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index 9ab368fb9..739fe71c5 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -421,6 +421,7 @@ end @testitem "Motion" tags=[:core, :motion] begin using Suppressor, OrdinaryDiffEqTsit5 include("initialize_backend.jl") + include(joinpath(@__DIR__, "test_files", "utils.jl")) Nadc = 25 M0 = 1.0 @@ -438,7 +439,7 @@ end Gz = 0 motions = [ - Translate(0.0, 0.1, 0.0, TimeRange(0.0, 1.0)), + Translate(0.1, 0.1, 0.0, TimeRange(0.0, 1.0)), Rotate(0.0, 0.0, 45.0, TimeRange(0.0, 1.0)), HeartBeat(-0.6, 0.0, 0.0, Periodic(1.0)), Path([0.0 0.0], [0.0 1.0], [0.0 0.0], TimeRange(0.0, 10.0)), diff --git a/KomaMRICore/test/test_files/utils.jl b/KomaMRICore/test/test_files/utils.jl index 769eddc34..769732348 100644 --- a/KomaMRICore/test/test_files/utils.jl +++ b/KomaMRICore/test/test_files/utils.jl @@ -158,4 +158,6 @@ function seq_epi_100x100_TE100_FOV230() return seq end -NRMSE(x, x_true) = sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. \ No newline at end of file +function NRMSE(x, x_true) + return sqrt.( sum(abs.(x .- x_true).^2) ./ sum(abs.(x_true).^2) ) * 100. +end \ No newline at end of file From ad102653816488335e05cf98af06cc2ec57490e3 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Date: Sun, 15 Sep 2024 02:14:52 +0200 Subject: [PATCH 77/91] `cis` -> `_cis` --- .../src/simulation/SimMethods/BlochSimple/BlochSimple.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl b/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl index 5370a2e78..2695b0129 100644 --- a/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl +++ b/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl @@ -44,7 +44,7 @@ function run_spin_precession!( #Mxy precession and relaxation, and Mz relaxation tp = cumsum(seq.Δt) # t' = t - t0 dur = sum(seq.Δt) # Total length, used for signal relaxation - Mxy = [M.xy M.xy .* exp.(-tp' ./ p.T2) .* cis.(ϕ)] #This assumes Δw and T2 are constant in time + Mxy = [M.xy M.xy .* exp.(-tp' ./ p.T2) .* _cis.(ϕ)] #This assumes Δw and T2 are constant in time M.z .= M.z .* exp.(-dur ./ p.T1) .+ p.ρ .* (1 .- exp.(-dur ./ p.T1)) reset_magnetization!(M, Mxy, p.motion, seq.t', p.ρ) M.xy .= Mxy[:, end] From 27ecbe502a9a7b197d11e94423c4b814271c0cdc Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Date: Sun, 15 Sep 2024 02:15:43 +0200 Subject: [PATCH 78/91] Formatting and renaming --- KomaMRIBase/src/motion/nomotion/NoMotion.jl | 6 +----- KomaMRICore/Project.toml | 1 - KomaMRICore/src/simulation/Flow.jl | 2 +- KomaMRIFiles/src/Phantom/Phantom.jl | 4 ++-- KomaMRIPlots/src/ui/DisplayFunctions.jl | 4 ++-- 5 files changed, 6 insertions(+), 11 deletions(-) diff --git a/KomaMRIBase/src/motion/nomotion/NoMotion.jl b/KomaMRIBase/src/motion/nomotion/NoMotion.jl index d139dc039..c9fcb209e 100644 --- a/KomaMRIBase/src/motion/nomotion/NoMotion.jl +++ b/KomaMRIBase/src/motion/nomotion/NoMotion.jl @@ -35,11 +35,7 @@ Base.:(==)(m1::NoMotion{T}, m2::NoMotion{T}) where {T<:Real} = true Base.:(≈)(m1::NoMotion{T}, m2::NoMotion{T}) where {T<:Real} = true function get_spin_coords( - mv::NoMotion{T}, - x::AbstractVector{T}, - y::AbstractVector{T}, - z::AbstractVector{T}, - t::AbstractArray{T} + mv::NoMotion{T}, x::AbstractVector{T}, y::AbstractVector{T}, z::AbstractVector{T}, t ) where {T<:Real} return x, y, z end diff --git a/KomaMRICore/Project.toml b/KomaMRICore/Project.toml index b3289c3ed..d5c7496fb 100644 --- a/KomaMRICore/Project.toml +++ b/KomaMRICore/Project.toml @@ -8,7 +8,6 @@ Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" Functors = "d9f16b24-f501-4c13-a1f2-28368ffc5196" KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c" KomaMRIBase = "d0bc0b20-b151-4d03-b2a4-6ca51751cb9c" -LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" ThreadsX = "ac1d9e8a-700a-412c-b207-f0111f4b6c0d" diff --git a/KomaMRICore/src/simulation/Flow.jl b/KomaMRICore/src/simulation/Flow.jl index 114592d20..b100a160d 100644 --- a/KomaMRICore/src/simulation/Flow.jl +++ b/KomaMRICore/src/simulation/Flow.jl @@ -8,7 +8,7 @@ end function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion::MotionList{T}, t, ρ) where {T<:Real} for m in motion.motions t_unit = KomaMRIBase.unit_time(t, m.time) - idx = KomaMRIBase.get_idx(m.spins) + idx = KomaMRIBase.get_indexing_range(m.spins) reset_magnetization!(@view(M[idx]), @view(Mxy[idx, :]), m.action, t_unit, @view(ρ[idx])) end return nothing diff --git a/KomaMRIFiles/src/Phantom/Phantom.jl b/KomaMRIFiles/src/Phantom/Phantom.jl index 6aac7400b..cdf93c712 100644 --- a/KomaMRIFiles/src/Phantom/Phantom.jl +++ b/KomaMRIFiles/src/Phantom/Phantom.jl @@ -11,8 +11,8 @@ function read_phantom(filename::String) file_version = VersionNumber(read_attribute(fid, "Version")) program_version = pkgversion(KomaMRIFiles) if file_version.major != program_version.major - @warn "Version mismatch detected: $file_version (file) vs $program_version (Koma) - This may lead to compatibility issues. Please update the file or the program." + @warn "KomaMRIFiles: Version mismatch detected: $file_version (used to write .phantom) vs $program_version (installed) + This may lead to compatibility issues. " end phantom_fields = [] # Name diff --git a/KomaMRIPlots/src/ui/DisplayFunctions.jl b/KomaMRIPlots/src/ui/DisplayFunctions.jl index cdfbeb884..df886588b 100644 --- a/KomaMRIPlots/src/ui/DisplayFunctions.jl +++ b/KomaMRIPlots/src/ui/DisplayFunctions.jl @@ -1140,7 +1140,7 @@ function plot_phantom_map( size=4, ), showlegend=false, - text=round.(getproperty(ph, key) * factor, digits=4), + text=round.(getproperty(obj, key) * factor, digits=4), hovertemplate="x: %{x:.1f} cm
y: %{y:.1f} cm
$(string(key)): %{text}$unit", ), ] @@ -1167,7 +1167,7 @@ function plot_phantom_map( z=(z[:, 1]) * 1e2, mode="markers", marker=attr(; - color=getproperty(ph, key) * factor, + color=getproperty(obj, key) * factor, showscale=colorbar, colorscale=colormap, colorbar=attr(; ticksuffix=unit, title=string(key)), From 5e71f163436712a4bf2cf1de4dc6d82e285dc2eb Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Date: Sun, 15 Sep 2024 02:23:27 +0200 Subject: [PATCH 79/91] =?UTF-8?q?Back=20to=20`(cos.(=CF=95)=20.+=20im=20.*?= =?UTF-8?q?=20sin.(=CF=95))`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/simulation/SimMethods/BlochSimple/BlochSimple.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl b/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl index 2695b0129..cd3b6e6a3 100644 --- a/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl +++ b/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl @@ -44,7 +44,7 @@ function run_spin_precession!( #Mxy precession and relaxation, and Mz relaxation tp = cumsum(seq.Δt) # t' = t - t0 dur = sum(seq.Δt) # Total length, used for signal relaxation - Mxy = [M.xy M.xy .* exp.(-tp' ./ p.T2) .* _cis.(ϕ)] #This assumes Δw and T2 are constant in time + Mxy = [M.xy M.xy .* exp.(-tp' ./ p.T2) .* (cos.(ϕ) .+ im .* sin.(ϕ))] #This assumes Δw and T2 are constant in time M.z .= M.z .* exp.(-dur ./ p.T1) .+ p.ρ .* (1 .- exp.(-dur ./ p.T1)) reset_magnetization!(M, Mxy, p.motion, seq.t', p.ρ) M.xy .= Mxy[:, end] From b79a28bb6c0733255f359ed1abcb12239ee97af7 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Date: Sun, 15 Sep 2024 10:40:50 +0200 Subject: [PATCH 80/91] Add `KomaMRIBase` into `sort_motions!` call --- KomaMRIFiles/src/Phantom/Phantom.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KomaMRIFiles/src/Phantom/Phantom.jl b/KomaMRIFiles/src/Phantom/Phantom.jl index cdf93c712..601e46e5c 100644 --- a/KomaMRIFiles/src/Phantom/Phantom.jl +++ b/KomaMRIFiles/src/Phantom/Phantom.jl @@ -135,7 +135,7 @@ function write_phantom( end function export_motion!(motion_group::HDF5.Group, motion_list::MotionList) - sort_motions!(motion_list) + KomaMRIBase.sort_motions!(motion_list) for (counter, m) in enumerate(motion_list.motions) motion = create_group(motion_group, "motion_$(counter)") for key in fieldnames(Motion) # action, time, spins From b4a878ba151a199ddd53d45c53688476fcbbaf16 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Mon, 16 Sep 2024 13:46:25 +0200 Subject: [PATCH 81/91] Add issue to Interpolations.jl --- .../src/motion/motionlist/actions/ArbitraryAction.jl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl b/KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl index df864f341..c17aa5bee 100644 --- a/KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl @@ -1,3 +1,15 @@ +# We defined two types of Interpolation objects: Interpolator1D and Interpolator2D +# 1D is for interpolating for 1 spin +# 2D is for interpolating for 2 or more spins +# This dispatch based on the number of spins wouldn't be necessary if it weren't for this: +# https://github.com/JuliaMath/Interpolations.jl/issues/603 +# +# Once this issue is solved, this file should be simpler. +# We should then be able to define a single method for functions: +# - interpolate +# - resample +# and delete the Interpolator1D and Interpolator2D definitions + const Interpolator1D = Interpolations.GriddedInterpolation{ T,1,V,Itp,K } where { From 30cc9cc278deebec0da866d3cc27537a1da39123 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Tue, 17 Sep 2024 03:07:02 +0200 Subject: [PATCH 82/91] `outflow_spin_reset!` inside all methods --- KomaMRICore/src/simulation/Flow.jl | 51 +++++++++++++------ .../simulation/SimMethods/Bloch/BlochCPU.jl | 12 +++++ .../simulation/SimMethods/Bloch/BlochGPU.jl | 12 +++++ .../SimMethods/BlochDict/BlochDict.jl | 7 ++- .../SimMethods/BlochSimple/BlochSimple.jl | 9 ++-- 5 files changed, 71 insertions(+), 20 deletions(-) diff --git a/KomaMRICore/src/simulation/Flow.jl b/KomaMRICore/src/simulation/Flow.jl index b100a160d..86f1a687b 100644 --- a/KomaMRICore/src/simulation/Flow.jl +++ b/KomaMRICore/src/simulation/Flow.jl @@ -1,25 +1,44 @@ -""" - reset_magnetization! -""" -function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion, t, ρ) where {T<:Real} +function outflow_spin_reset!(args...; kwargs...) return nothing end -function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, motion::MotionList{T}, t, ρ) where {T<:Real} - for m in motion.motions - t_unit = KomaMRIBase.unit_time(t, m.time) - idx = KomaMRIBase.get_indexing_range(m.spins) - reset_magnetization!(@view(M[idx]), @view(Mxy[idx, :]), m.action, t_unit, @view(ρ[idx])) +function outflow_spin_reset!(M, t, motion::MotionList; M0=0, seq_t=0, add_t0=false) + for m in motion.motions + outflow_spin_reset!(M, t, m.action, m.time, m.spins; M0=M0, seq_t=seq_t, add_t0=add_t0) end return nothing end -function reset_magnetization!(M::Mag{T}, Mxy::AbstractArray{Complex{T}}, action::FlowPath{T}, t, ρ) where {T<:Real} +function outflow_spin_reset!(M, t, action::FlowPath, time, spins; M0=0, seq_t=0, add_t0=false) + t_unit = KomaMRIBase.unit_time(t, time) + idx = KomaMRIBase.get_indexing_range(spins) + M = @view(M[idx, :]) + M0 = init_magnetization(M, M0) + t = init_time(t_unit, seq_t, add_t0) itp = KomaMRIBase.interpolate(action.spin_reset, KomaMRIBase.Gridded(KomaMRIBase.Constant{KomaMRIBase.Previous}()), Val(size(action.spin_reset, 1))) - flags = KomaMRIBase.resample(itp, t) - reset = vec(any(flags .> 0; dims=2)) - flags = (cumsum(flags; dims=2) .== 0) - Mxy .*= flags - M.z[reset] = ρ[reset] + mask = KomaMRIBase.resample(itp, t) + mask .= (cumsum(mask; dims=2) .== 0) + mask_end = 1 .- vec(any(mask .== 0; dims=2)) + if size(M, 2) > 1 + M .*= mask + M .+= M0 .* (1 .- mask) + else + M .*= mask_end + M .+= M0 .* (1 .- mask_end) + end return nothing -end \ No newline at end of file +end + +init_time(t, seq_t, add_t0) = t +init_time(t, seq_t::AbstractArray, add_t0) = begin + t1 = @view(seq_t[1]) + return add_t0 ? [t1 (t1 .+ t)] : t1 .+ t +end + +init_magnetization(M, M0) = M0 +init_magnetization(M, M0::Real) = begin + x = similar(M, size(M,1)) + x .*= M0 + return x +end + diff --git a/KomaMRICore/src/simulation/SimMethods/Bloch/BlochCPU.jl b/KomaMRICore/src/simulation/SimMethods/Bloch/BlochCPU.jl index 49a6312a8..e2a7ec17b 100644 --- a/KomaMRICore/src/simulation/SimMethods/Bloch/BlochCPU.jl +++ b/KomaMRICore/src/simulation/SimMethods/Bloch/BlochCPU.jl @@ -88,6 +88,10 @@ function run_spin_precession!( #Acquired Signal if seq_idx <= length(seq.ADC) && seq.ADC[seq_idx] @. Mxy = exp(-t_seq / p.T2) * M.xy * cis(ϕ) + + #Reset Spin-State (Magnetization). Only for FlowPath + outflow_spin_reset!(Mxy, seq.t[seq_idx,:]', p.motion) + sig[ADC_idx] = sum(Mxy) ADC_idx += 1 end @@ -98,6 +102,10 @@ function run_spin_precession!( #Final Spin-State @. M.xy = M.xy * exp(-t_seq / p.T2) * cis(ϕ) @. M.z = M.z * exp(-t_seq / p.T1) + p.ρ * (T(1) - exp(-t_seq / p.T1)) + + #Reset Spin-State (Magnetization). Only for FlowPath + outflow_spin_reset!(M.xy, seq.t', p.motion) + outflow_spin_reset!(M.z, seq.t', p.motion; M0=p.ρ) return nothing end @@ -142,6 +150,10 @@ function run_spin_excitation!( #Relaxation @. M.xy = M.xy * exp(-s.Δt / p.T2) @. M.z = M.z * exp(-s.Δt / p.T1) + p.ρ * (T(1) - exp(-s.Δt / p.T1)) + + #Reset Spin-State (Magnetization). Only for FlowPath + outflow_spin_reset!(M.xy, s.t, p.motion) + outflow_spin_reset!(M.z, s.t, p.motion; M0=p.ρ) end #Acquired signal #sig .= -1.4im #<-- This was to test if an ADC point was inside an RF block diff --git a/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl b/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl index dd523791e..22d2d4d21 100644 --- a/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl +++ b/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl @@ -151,8 +151,12 @@ function run_spin_precession!( if seq_block.first_ADC pre.Mxy[:,1] .= M.xy pre.Mxy[:,2:end] .= M.xy .* exp.(-seq_block.tp_ADC' ./ p.T2) .* _cis.(ϕ_ADC) + #Reset Spin-State (Magnetization). Only for FlowPath + outflow_spin_reset!(pre.Mxy, seq_block.tp_ADC', p.motion; seq_t=seq.t', add_t0=true) else pre.Mxy .= M.xy .* exp.(-seq_block.tp_ADC' ./ p.T2) .* _cis.(ϕ_ADC) + #Reset Spin-State (Magnetization). Only for FlowPath + outflow_spin_reset!(pre.Mxy, seq_block.tp_ADC', p.motion; seq_t=seq.t') end sig .= transpose(sum(pre.Mxy; dims=1)) @@ -162,6 +166,10 @@ function run_spin_precession!( M.z .= M.z .* exp.(-seq_block.dur ./ p.T1) .+ p.ρ .* (T(1) .- exp.(-seq_block.dur ./ p.T1)) M.xy .= M.xy .* exp.(-seq_block.dur ./ p.T2) .* _cis.(pre.ϕ[:,end]) + #Reset Spin-State (Magnetization). Only for FlowPath + outflow_spin_reset!(M.xy, seq.t', p.motion) + outflow_spin_reset!(M.z, seq.t', p.motion; M0=p.ρ) + return nothing end @@ -192,5 +200,9 @@ function run_spin_excitation!( apply_excitation!(backend, 256)(M.xy, M.z, pre.φ, seq.B1, pre.Bz, pre.B, pre.ΔT1, pre.ΔT2, p.ρ, ndrange=size(M.xy,1)) KA.synchronize(backend) + #Reset Spin-State (Magnetization). Only for FlowPath + outflow_spin_reset!(M.xy, seq.t', p.motion) + outflow_spin_reset!(M.z, seq.t', p.motion; M0=p.ρ) # TODO: reset state inside kernel + return nothing end \ No newline at end of file diff --git a/KomaMRICore/src/simulation/SimMethods/BlochDict/BlochDict.jl b/KomaMRICore/src/simulation/SimMethods/BlochDict/BlochDict.jl index 7afc01841..341beee45 100644 --- a/KomaMRICore/src/simulation/SimMethods/BlochDict/BlochDict.jl +++ b/KomaMRICore/src/simulation/SimMethods/BlochDict/BlochDict.jl @@ -53,17 +53,22 @@ function run_spin_precession!( tp = cumsum(seq.Δt) # t' = t - t0 dur = sum(seq.Δt) # Total length, used for signal relaxation Mxy = [M.xy M.xy .* exp.(-tp' ./ p.T2) .* (cos.(ϕ) .+ im .* sin.(ϕ))] #This assumes Δw and T2 are constant in time - reset_magnetization!(M, Mxy, p.motion, seq.t', p.ρ) + #Reset Spin-State (Magnetization). Only for FlowPath + outflow_spin_reset!(Mxy, seq.t', p.motion) M.xy .= Mxy[:, end] #Acquired signal sig[:, :, 1] .= transpose(Mxy[:, findall(seq.ADC)]) if sim_method.save_Mz Mz = [M.z M.z .* exp.(-tp' ./ p.T1) .+ p.ρ .* (1 .- exp.(-tp' ./ p.T1))] #Calculate intermediate points + #Reset Spin-State (Magnetization). Only for FlowPath + outflow_spin_reset!(Mz, seq.t', p.motion; M0=p.ρ) sig[:, :, 2] .= transpose(Mz[:, findall(seq.ADC)]) #Save state to signal M.z .= Mz[:, end] else M.z .= M.z .* exp.(-dur ./ p.T1) .+ p.ρ .* (1 .- exp.(-dur ./ p.T1)) #Jump to the last point + #Reset Spin-State (Magnetization). Only for FlowPath + outflow_spin_reset!(M.z, seq.t', p.motion; M0=p.ρ) end return nothing end \ No newline at end of file diff --git a/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl b/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl index cd3b6e6a3..1e3dc0e17 100644 --- a/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl +++ b/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl @@ -46,11 +46,12 @@ function run_spin_precession!( dur = sum(seq.Δt) # Total length, used for signal relaxation Mxy = [M.xy M.xy .* exp.(-tp' ./ p.T2) .* (cos.(ϕ) .+ im .* sin.(ϕ))] #This assumes Δw and T2 are constant in time M.z .= M.z .* exp.(-dur ./ p.T1) .+ p.ρ .* (1 .- exp.(-dur ./ p.T1)) - reset_magnetization!(M, Mxy, p.motion, seq.t', p.ρ) + #Reset Spin-State (Magnetization). Only for FlowPath + outflow_spin_reset!(Mxy, seq.t', p.motion) + outflow_spin_reset!(M.z, seq.t', p.motion; M0=p.ρ) M.xy .= Mxy[:, end] #Acquired signal sig .= transpose(sum(Mxy[:, findall(seq.ADC)]; dims=1)) #<--- TODO: add coil sensitivities - return nothing end @@ -93,7 +94,9 @@ function run_spin_excitation!( #Relaxation M.xy .= M.xy .* exp.(-s.Δt ./ p.T2) M.z .= M.z .* exp.(-s.Δt ./ p.T1) .+ p.ρ .* (1 .- exp.(-s.Δt ./ p.T1)) - reset_magnetization!(M, M.xy, p.motion, s.t, p.ρ) + #Reset Spin-State (Magnetization). Only for FlowPath + outflow_spin_reset!(M.xy, s.t, p.motion) + outflow_spin_reset!(M.z, s.t, p.motion; M0=p.ρ) end #Acquired signal #sig .= -1.4im #<-- This was to test if an ADC point was inside an RF block From 12f6fed01a2d0390401e68f0a1b61427e647616e Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Tue, 17 Sep 2024 15:56:41 +0200 Subject: [PATCH 83/91] Workgroup size back to 512 --- KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl b/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl index 22d2d4d21..65b363385 100644 --- a/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl +++ b/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl @@ -197,7 +197,7 @@ function run_spin_excitation!( pre.ΔT2 .= exp.(-seq.Δt' ./ p.T2) #Excitation - apply_excitation!(backend, 256)(M.xy, M.z, pre.φ, seq.B1, pre.Bz, pre.B, pre.ΔT1, pre.ΔT2, p.ρ, ndrange=size(M.xy,1)) + apply_excitation!(backend, 512)(M.xy, M.z, pre.φ, seq.B1, pre.Bz, pre.B, pre.ΔT1, pre.ΔT2, p.ρ, ndrange=size(M.xy,1)) KA.synchronize(backend) #Reset Spin-State (Magnetization). Only for FlowPath From 108886d257a659ede2a8e111a8cd370e9fc52c17 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Tue, 17 Sep 2024 16:18:25 +0200 Subject: [PATCH 84/91] Remove `*`, since it produces errors in CPU --- KomaMRICore/src/simulation/Flow.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KomaMRICore/src/simulation/Flow.jl b/KomaMRICore/src/simulation/Flow.jl index 86f1a687b..2b18223bc 100644 --- a/KomaMRICore/src/simulation/Flow.jl +++ b/KomaMRICore/src/simulation/Flow.jl @@ -38,7 +38,7 @@ end init_magnetization(M, M0) = M0 init_magnetization(M, M0::Real) = begin x = similar(M, size(M,1)) - x .*= M0 + x .= M0 return x end From 0430560f20736cfbdc30b47c0e81ec7685eaf7dc Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Wed, 18 Sep 2024 13:57:44 +0200 Subject: [PATCH 85/91] Suggested changes for flow functions --- .../actions/arbitraryactions/FlowPath.jl | 2 +- KomaMRICore/src/simulation/Flow.jl | 83 +++++++++++++------ .../simulation/SimMethods/Bloch/BlochCPU.jl | 6 +- .../simulation/SimMethods/Bloch/BlochGPU.jl | 10 +-- .../SimMethods/BlochDict/BlochDict.jl | 8 +- .../SimMethods/BlochSimple/BlochSimple.jl | 9 +- 6 files changed, 72 insertions(+), 46 deletions(-) diff --git a/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl b/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl index bfd192b85..6c2051857 100644 --- a/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl @@ -14,7 +14,7 @@ has a size of (``N_{spins} \times \; N_{discrete\,times}``). - `dx`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in x - `dy`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in y - `dz`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in z -- `spin_reset`: (`::AbstractArray{Bool}`) reset spin state flags +- `spin_reset`: (`::AbstractArray{T<:Real}`) reset spin state flags # Returns - `flowpath`: (`::FlowPath`) FlowPath struct diff --git a/KomaMRICore/src/simulation/Flow.jl b/KomaMRICore/src/simulation/Flow.jl index 2b18223bc..e9037d733 100644 --- a/KomaMRICore/src/simulation/Flow.jl +++ b/KomaMRICore/src/simulation/Flow.jl @@ -2,30 +2,69 @@ function outflow_spin_reset!(args...; kwargs...) return nothing end -function outflow_spin_reset!(M, t, motion::MotionList; M0=0, seq_t=0, add_t0=false) +function outflow_spin_reset!( + spin_state_matrix, t, motion::MotionList; + replace_by=0, seq_t=0, add_t0=false +) for m in motion.motions - outflow_spin_reset!(M, t, m.action, m.time, m.spins; M0=M0, seq_t=seq_t, add_t0=add_t0) + outflow_spin_reset!( + spin_state_matrix, t, m.action, m.time, m.spins; + replace_by=replace_by, seq_t=seq_t, add_t0=add_t0 + ) end return nothing end -function outflow_spin_reset!(M, t, action::FlowPath, time, spins; M0=0, seq_t=0, add_t0=false) - t_unit = KomaMRIBase.unit_time(t, time) - idx = KomaMRIBase.get_indexing_range(spins) - M = @view(M[idx, :]) - M0 = init_magnetization(M, M0) - t = init_time(t_unit, seq_t, add_t0) - itp = KomaMRIBase.interpolate(action.spin_reset, KomaMRIBase.Gridded(KomaMRIBase.Constant{KomaMRIBase.Previous}()), Val(size(action.spin_reset, 1))) - mask = KomaMRIBase.resample(itp, t) +function outflow_spin_reset!( + spin_state_matrix::AbstractArray, + t, + action::FlowPath, + time_span, + spin_span; + replace_by=0, + seq_t=0, + add_t0=false, +) where T + # Initialize time: add t0 and normalize + ts = KomaMRIBase.unit_time(init_time(t, seq_t, add_t0), time_span) + # Get spin state range affected by the spin span + idx = KomaMRIBase.get_indexing_range(spin_span) + spin_state_matrix = @view(spin_state_matrix[idx, :]) + # Obtain mask + itp = KomaMRIBase.interpolate(action.spin_reset, KomaMRIBase.Gridded(KomaMRIBase.Constant{KomaMRIBase.Previous}()), Val(size(action.spin_reset, 1))) + mask = KomaMRIBase.resample(itp, ts) mask .= (cumsum(mask; dims=2) .== 0) - mask_end = 1 .- vec(any(mask .== 0; dims=2)) - if size(M, 2) > 1 - M .*= mask - M .+= M0 .* (1 .- mask) - else - M .*= mask_end - M .+= M0 .* (1 .- mask_end) - end + # Modify spin state: reset and replace by initial value + spin_state_matrix .*= mask + spin_state_matrix .+= replace_by .* (1 .- mask) + return nothing +end + +function outflow_spin_reset!( + M::Mag, + t, + action::FlowPath, + time_span, + spin_span; + replace_by=0, + seq_t=0, + add_t0=false, +) + # Initialize time: add t0 and normalize + ts = KomaMRIBase.unit_time(init_time(t, seq_t, add_t0), time_span) + # Get spin state range affected by the spin span + idx = KomaMRIBase.get_indexing_range(spin_span) + M = @view(M[idx]) + # Obtain mask + itp = KomaMRIBase.interpolate(action.spin_reset, KomaMRIBase.Gridded(KomaMRIBase.Constant{KomaMRIBase.Previous}()), Val(size(action.spin_reset, 1))) + mask = KomaMRIBase.resample(itp, ts) + mask .= (cumsum(mask; dims=2) .== 0) + mask = @view(mask[:, end]) + # Modify spin state: reset and replace by initial value + M.xy .*= mask + M.z .*= mask + M.xy .+= 0 .* (1 .- mask) + M.z .+= replace_by .* (1 .- mask) return nothing end @@ -34,11 +73,3 @@ init_time(t, seq_t::AbstractArray, add_t0) = begin t1 = @view(seq_t[1]) return add_t0 ? [t1 (t1 .+ t)] : t1 .+ t end - -init_magnetization(M, M0) = M0 -init_magnetization(M, M0::Real) = begin - x = similar(M, size(M,1)) - x .= M0 - return x -end - diff --git a/KomaMRICore/src/simulation/SimMethods/Bloch/BlochCPU.jl b/KomaMRICore/src/simulation/SimMethods/Bloch/BlochCPU.jl index e2a7ec17b..0f3d4739a 100644 --- a/KomaMRICore/src/simulation/SimMethods/Bloch/BlochCPU.jl +++ b/KomaMRICore/src/simulation/SimMethods/Bloch/BlochCPU.jl @@ -104,8 +104,7 @@ function run_spin_precession!( @. M.z = M.z * exp(-t_seq / p.T1) + p.ρ * (T(1) - exp(-t_seq / p.T1)) #Reset Spin-State (Magnetization). Only for FlowPath - outflow_spin_reset!(M.xy, seq.t', p.motion) - outflow_spin_reset!(M.z, seq.t', p.motion; M0=p.ρ) + outflow_spin_reset!(M, seq.t', p.motion; replace_by=p.ρ) return nothing end @@ -152,8 +151,7 @@ function run_spin_excitation!( @. M.z = M.z * exp(-s.Δt / p.T1) + p.ρ * (T(1) - exp(-s.Δt / p.T1)) #Reset Spin-State (Magnetization). Only for FlowPath - outflow_spin_reset!(M.xy, s.t, p.motion) - outflow_spin_reset!(M.z, s.t, p.motion; M0=p.ρ) + outflow_spin_reset!(M, s.t, p.motion; replace_by=p.ρ) end #Acquired signal #sig .= -1.4im #<-- This was to test if an ADC point was inside an RF block diff --git a/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl b/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl index 65b363385..8f85dfed6 100644 --- a/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl +++ b/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl @@ -152,11 +152,11 @@ function run_spin_precession!( pre.Mxy[:,1] .= M.xy pre.Mxy[:,2:end] .= M.xy .* exp.(-seq_block.tp_ADC' ./ p.T2) .* _cis.(ϕ_ADC) #Reset Spin-State (Magnetization). Only for FlowPath - outflow_spin_reset!(pre.Mxy, seq_block.tp_ADC', p.motion; seq_t=seq.t', add_t0=true) + outflow_spin_reset!(pre.Mxy, seq_block.tp_ADC', p.motion; seq_t=seq.t, add_t0=true) else pre.Mxy .= M.xy .* exp.(-seq_block.tp_ADC' ./ p.T2) .* _cis.(ϕ_ADC) #Reset Spin-State (Magnetization). Only for FlowPath - outflow_spin_reset!(pre.Mxy, seq_block.tp_ADC', p.motion; seq_t=seq.t') + outflow_spin_reset!(pre.Mxy, seq_block.tp_ADC', p.motion; seq_t=seq.t) end sig .= transpose(sum(pre.Mxy; dims=1)) @@ -167,8 +167,7 @@ function run_spin_precession!( M.xy .= M.xy .* exp.(-seq_block.dur ./ p.T2) .* _cis.(pre.ϕ[:,end]) #Reset Spin-State (Magnetization). Only for FlowPath - outflow_spin_reset!(M.xy, seq.t', p.motion) - outflow_spin_reset!(M.z, seq.t', p.motion; M0=p.ρ) + outflow_spin_reset!(M, seq.t', p.motion; replace_by=p.ρ) return nothing end @@ -201,8 +200,7 @@ function run_spin_excitation!( KA.synchronize(backend) #Reset Spin-State (Magnetization). Only for FlowPath - outflow_spin_reset!(M.xy, seq.t', p.motion) - outflow_spin_reset!(M.z, seq.t', p.motion; M0=p.ρ) # TODO: reset state inside kernel + outflow_spin_reset!(M, seq.t', p.motion; replace_by=p.ρ) # TODO: reset state inside kernel return nothing end \ No newline at end of file diff --git a/KomaMRICore/src/simulation/SimMethods/BlochDict/BlochDict.jl b/KomaMRICore/src/simulation/SimMethods/BlochDict/BlochDict.jl index 341beee45..58afbcb1b 100644 --- a/KomaMRICore/src/simulation/SimMethods/BlochDict/BlochDict.jl +++ b/KomaMRICore/src/simulation/SimMethods/BlochDict/BlochDict.jl @@ -53,22 +53,22 @@ function run_spin_precession!( tp = cumsum(seq.Δt) # t' = t - t0 dur = sum(seq.Δt) # Total length, used for signal relaxation Mxy = [M.xy M.xy .* exp.(-tp' ./ p.T2) .* (cos.(ϕ) .+ im .* sin.(ϕ))] #This assumes Δw and T2 are constant in time + M.xy .= Mxy[:, end] #Reset Spin-State (Magnetization). Only for FlowPath outflow_spin_reset!(Mxy, seq.t', p.motion) - M.xy .= Mxy[:, end] #Acquired signal sig[:, :, 1] .= transpose(Mxy[:, findall(seq.ADC)]) if sim_method.save_Mz Mz = [M.z M.z .* exp.(-tp' ./ p.T1) .+ p.ρ .* (1 .- exp.(-tp' ./ p.T1))] #Calculate intermediate points #Reset Spin-State (Magnetization). Only for FlowPath - outflow_spin_reset!(Mz, seq.t', p.motion; M0=p.ρ) + outflow_spin_reset!(Mz, seq.t', p.motion; replace_by=p.ρ) sig[:, :, 2] .= transpose(Mz[:, findall(seq.ADC)]) #Save state to signal M.z .= Mz[:, end] else M.z .= M.z .* exp.(-dur ./ p.T1) .+ p.ρ .* (1 .- exp.(-dur ./ p.T1)) #Jump to the last point - #Reset Spin-State (Magnetization). Only for FlowPath - outflow_spin_reset!(M.z, seq.t', p.motion; M0=p.ρ) end + #Reset Spin-State (Magnetization). Only for FlowPath + outflow_spin_reset!(M, seq.t', p.motion; replace_by=p.ρ) return nothing end \ No newline at end of file diff --git a/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl b/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl index 1e3dc0e17..d754d389a 100644 --- a/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl +++ b/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl @@ -44,12 +44,12 @@ function run_spin_precession!( #Mxy precession and relaxation, and Mz relaxation tp = cumsum(seq.Δt) # t' = t - t0 dur = sum(seq.Δt) # Total length, used for signal relaxation - Mxy = [M.xy M.xy .* exp.(-tp' ./ p.T2) .* (cos.(ϕ) .+ im .* sin.(ϕ))] #This assumes Δw and T2 are constant in time + Mxy = [M.xy M.xy .* exp.(-tp' ./ p.T2) .* (cos.(ϕ) .+ im .* sin.(ϕ))] #This assumes Δw and T2 are constant in time + M.xy .= Mxy[:, end] M.z .= M.z .* exp.(-dur ./ p.T1) .+ p.ρ .* (1 .- exp.(-dur ./ p.T1)) #Reset Spin-State (Magnetization). Only for FlowPath outflow_spin_reset!(Mxy, seq.t', p.motion) - outflow_spin_reset!(M.z, seq.t', p.motion; M0=p.ρ) - M.xy .= Mxy[:, end] + outflow_spin_reset!(M, seq.t', p.motion; replace_by=p.ρ) #Acquired signal sig .= transpose(sum(Mxy[:, findall(seq.ADC)]; dims=1)) #<--- TODO: add coil sensitivities return nothing @@ -95,8 +95,7 @@ function run_spin_excitation!( M.xy .= M.xy .* exp.(-s.Δt ./ p.T2) M.z .= M.z .* exp.(-s.Δt ./ p.T1) .+ p.ρ .* (1 .- exp.(-s.Δt ./ p.T1)) #Reset Spin-State (Magnetization). Only for FlowPath - outflow_spin_reset!(M.xy, s.t, p.motion) - outflow_spin_reset!(M.z, s.t, p.motion; M0=p.ρ) + outflow_spin_reset!(M, s.t, p.motion; replace_by=p.ρ) end #Acquired signal #sig .= -1.4im #<-- This was to test if an ADC point was inside an RF block From 19d54e04ee680c05d45a2f67dea422e3b576ee94 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Wed, 18 Sep 2024 14:05:05 +0200 Subject: [PATCH 86/91] Suggested changes about `plot_phantom_map` --- KomaMRIPlots/src/ui/DisplayFunctions.jl | 162 +++++++++++++++++++++++- 1 file changed, 158 insertions(+), 4 deletions(-) diff --git a/KomaMRIPlots/src/ui/DisplayFunctions.jl b/KomaMRIPlots/src/ui/DisplayFunctions.jl index df886588b..9dab997ac 100644 --- a/KomaMRIPlots/src/ui/DisplayFunctions.jl +++ b/KomaMRIPlots/src/ui/DisplayFunctions.jl @@ -1018,17 +1018,23 @@ julia> plot_phantom_map(obj2D, :ρ) julia> plot_phantom_map(obj3D, :ρ) ``` """ +function plot_phantom_map(obj::Phantom, key::Symbol; kwargs...) + plot_phantom_map(obj, key, obj.motion; kwargs...) +end + +# Plot dynamic phantom (For now, we define two different methods for static and dynamic phantoms) function plot_phantom_map( obj::Phantom, - key::Symbol; + key::Symbol, + m::MotionList; height=700, width=nothing, darkmode=false, view_2d=sum(KomaMRIBase.get_dims(obj)) < 3, colorbar=true, + max_spins=100_000, intermediate_time_samples=0, max_time_samples=100, - max_spins=100_000, frame_duration_ms=250, kwargs..., ) @@ -1118,8 +1124,8 @@ function plot_phantom_map( cmin_key = get(kwargs, :cmin, factor * cmin_key) cmax_key = get(kwargs, :cmax, factor * cmax_key) - t = process_times(obj.motion) - x, y, z = get_spin_coords(obj.motion, obj.x, obj.y, obj.z, t') + t = process_times(m) + x, y, z = get_spin_coords(m, obj.x, obj.y, obj.z, t') x0 = -maximum(abs.([x y z])) * 1e2 xf = maximum(abs.([x y z])) * 1e2 @@ -1336,6 +1342,154 @@ function plot_phantom_map( return Plot(trace, l, frames) end +# Plot static phantom (For now, we define two different methods for static and dynamic phantoms) +function plot_phantom_map( + obj::Phantom, + key::Symbol, + m::NoMotion; + height=700, + width=nothing, + darkmode=false, + view_2d=sum(KomaMRIBase.get_dims(obj)) < 3, + colorbar=true, + max_spins=100_000, + kwargs..., +) + function decimate_uniform_phantom(obj, num_points::Int) + dimx, dimy, dimz = KomaMRIBase.get_dims(obj) + ss = Int(ceil((length(obj) / num_points)^(1 / sum(KomaMRIBase.get_dims(obj))))) + ssx = dimx ? ss : 1 + ssy = dimy ? ss : 1 + ssz = dimz ? ss : 1 + ix = sortperm(obj.x)[1:ssx:end] + iy = sortperm(obj.y)[1:ssy:end] + iz = sortperm(obj.z)[1:ssz:end] + idx = intersect(ix, iy, iz) + return obj[idx] + end + + if length(obj) > max_spins + obj = decimate_uniform_phantom(obj, max_spins) + @warn "For performance reasons, the number of displayed spins was capped to `max_spins`=$(max_spins)." + end + + path = @__DIR__ + cmin_key = minimum(getproperty(obj,key)) + cmax_key = maximum(getproperty(obj,key)) + if key == :T1 || key == :T2 || key == :T2s + cmin_key = 0 + factor = 1e3 + unit = " ms" + if key == :T1 + cmax_key = 2500/factor + colors = MAT.matread(path*"/assets/T1cm.mat")["T1colormap"] + N, _ = size(colors) + idx = range(0,1;length=N) #range(0,T,N) works in Julia 1.7 + colormap = [[idx[n], "rgb($(floor(Int,colors[n,1]*255)),$(floor(Int,colors[n,2]*255)),$(floor(Int,colors[n,3]*255)))"] for n=1:N] + elseif key == :T2 || key == :T2s + if key == :T2 + cmax_key = 250/factor + end + colors = MAT.matread(path*"/assets/T2cm.mat")["T2colormap"] + N, _ = size(colors) + idx = range(0,1;length=N) #range(0,T,N) works in Julia 1.7 + colormap = [[idx[n], "rgb($(floor(Int,colors[n,1]*255)),$(floor(Int,colors[n,2]*255)),$(floor(Int,colors[n,3]*255)))"] for n=1:N] + end + elseif key == :x || key == :y || key == :z + factor = 1e2 + unit = " cm" + colormap="Greys" + elseif key == :Δw + factor = 1/(2π) + unit = " Hz" + colormap="Greys" + else + factor = 1 + cmin_key = 0 + unit="" + colormap="Greys" + end + cmin_key = get(kwargs, :cmin, factor * cmin_key) + cmax_key = get(kwargs, :cmax, factor * cmax_key) + x0 = -maximum(abs.([obj.x obj.y obj.z]))*1e2 + xf = maximum(abs.([obj.x obj.y obj.z]))*1e2 + #Layout + bgcolor, text_color, plot_bgcolor, grid_color, sep_color = theme_chooser(darkmode) + l = Layout(; + title=obj.name*": "*string(key), + xaxis_title="x", + yaxis_title="y", + plot_bgcolor=plot_bgcolor, + paper_bgcolor=bgcolor, + xaxis_gridcolor=grid_color, + yaxis_gridcolor=grid_color, + xaxis_zerolinecolor=grid_color, + yaxis_zerolinecolor=grid_color, + font_color=text_color, + scene=attr( + xaxis=attr(title="x",range=[x0,xf],ticksuffix=" cm",backgroundcolor=plot_bgcolor,gridcolor=grid_color,zerolinecolor=grid_color), + yaxis=attr(title="y",range=[x0,xf],ticksuffix=" cm",backgroundcolor=plot_bgcolor,gridcolor=grid_color,zerolinecolor=grid_color), + zaxis=attr(title="z",range=[x0,xf],ticksuffix=" cm",backgroundcolor=plot_bgcolor,gridcolor=grid_color,zerolinecolor=grid_color), + aspectmode="manual", + aspectratio=attr(x=1,y=1,z=1)), + margin=attr(t=50,l=0,r=0), + modebar=attr(orientation="h",bgcolor=bgcolor,color=text_color,activecolor=plot_bgcolor), + xaxis=attr(constrain="domain"), + yaxis=attr(scaleanchor="x"), + hovermode="closest") + if height !== nothing + l.height = height + end + if width !== nothing + l.width = width + end + if view_2d + h = scatter( + x=obj.x*1e2, + y=obj.y*1e2, + mode="markers", + marker=attr( + color=getproperty(obj,key)*factor, + showscale=colorbar, + colorscale=colormap, + colorbar=attr(ticksuffix=unit, title=string(key)), + cmin=cmin_key, + cmax=cmax_key, + size=4 + ), + text=round.(getproperty(obj,key)*factor,digits=4), + hovertemplate="x: %{x:.1f} cm
y: %{y:.1f} cm
$(string(key)): %{text}$unit" + ) + else + h = scatter3d( + x=obj.x*1e2, + y=obj.y*1e2, + z=obj.z*1e2, + mode="markers", + marker=attr( + color=getproperty(obj,key)*factor, + showscale=colorbar, + colorscale=colormap, + colorbar=attr(ticksuffix=unit, title=string(key)), + cmin=cmin_key, + cmax=cmax_key, + size=2 + ), + text=round.(getproperty(obj,key)*factor,digits=4), + hovertemplate="x: %{x:.1f} cm
y: %{y:.1f} cm
z: %{z:.1f} cm
$(string(key)): %{text}$unit" + ) + end + config = PlotConfig( + displaylogo=false, + toImageButtonOptions=attr( + format="svg", # one of png, svg, jpeg, webp + ).fields, + modeBarButtonsToRemove=["zoom", "pan", "tableRotation", "resetCameraLastSave3d", "orbitRotation", "resetCameraDefault3d"] + ) + return plot_koma(h, l; config) +end + + """ p = plot_signal(raw::RawAcquisitionData; kwargs...) From e7c4db2b1741984bce333f8fd5b854fff80fb84a Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Date: Wed, 18 Sep 2024 23:55:35 +0200 Subject: [PATCH 87/91] Remove unnecessary `T` declaration --- KomaMRICore/src/simulation/Flow.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KomaMRICore/src/simulation/Flow.jl b/KomaMRICore/src/simulation/Flow.jl index e9037d733..ac05f0f5e 100644 --- a/KomaMRICore/src/simulation/Flow.jl +++ b/KomaMRICore/src/simulation/Flow.jl @@ -24,7 +24,7 @@ function outflow_spin_reset!( replace_by=0, seq_t=0, add_t0=false, -) where T +) # Initialize time: add t0 and normalize ts = KomaMRIBase.unit_time(init_time(t, seq_t, add_t0), time_span) # Get spin state range affected by the spin span From 5780c3102174e430393cb4d082fed23ab48b4b60 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Date: Thu, 19 Sep 2024 10:51:53 +0200 Subject: [PATCH 88/91] Squashed commit of the following: commit 3662bfbcaf07669e658d0b5a9d0b000bc9b3d748 Author: Pablo Villacorta Date: Thu Sep 19 10:46:20 2024 +0200 Modify `naive_cumsum!` kernel (@rkierulf) commit 88e63e42aea4d3fe1d283b1596a94177a81aa874 Author: Pablo Villacorta Aylagas Date: Wed Sep 18 19:25:20 2024 +0200 Change ArbitraryAction.jl to allow for using scalars as query times commit 8f636623b09c05cef15f1a9cc0eaa5138ab28f88 Author: Pablo Villacorta Aylagas Date: Wed Sep 18 19:07:16 2024 +0200 Allow interpolation nodes and coefficients to have different eltype --- .../motionlist/actions/ArbitraryAction.jl | 48 ++++++++++--------- .../actions/arbitraryactions/FlowPath.jl | 4 +- KomaMRICore/ext/KomaoneAPIExt.jl | 3 +- KomaMRICore/src/simulation/Flow.jl | 4 +- KomaMRICore/test/runtests.jl | 2 +- 5 files changed, 33 insertions(+), 28 deletions(-) diff --git a/KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl b/KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl index c17aa5bee..c62a7b4bd 100644 --- a/KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/ArbitraryAction.jl @@ -11,21 +11,23 @@ # and delete the Interpolator1D and Interpolator2D definitions const Interpolator1D = Interpolations.GriddedInterpolation{ - T,1,V,Itp,K + TCoefs,1,V,Itp,K } where { - T<:Real, - V<:AbstractArray{<:Union{T,Bool}}, + TCoefs<:Real, + TNodes<:Real, + V<:AbstractArray{TCoefs}, Itp<:Interpolations.Gridded, - K<:Tuple{AbstractVector{T}}, + K<:Tuple{AbstractVector{TNodes}}, } const Interpolator2D = Interpolations.GriddedInterpolation{ - T,2,V,Itp,K + TCoefs,2,V,Itp,K } where { - T<:Real, - V<:AbstractArray{<:Union{T,Bool}}, + TCoefs<:Real, + TNodes<:Real, + V<:AbstractArray{TCoefs}, Itp<:Interpolations.Gridded, - K<:Tuple{AbstractVector{T}, AbstractVector{T}}, + K<:Tuple{AbstractVector{TNodes}, AbstractVector{TNodes}}, } abstract type ArbitraryAction{T<:Real} <: AbstractAction{T} end @@ -44,48 +46,50 @@ function GriddedInterpolation(nodes, A, ITP) return Interpolations.GriddedInterpolation{eltype(A), length(nodes), typeof(A), typeof(ITP), typeof(nodes)}(nodes, A, ITP) end -function interpolate(d::AbstractArray{T}, ITPType, Ns::Val{1}) where {T<:Real} +function interpolate(d, ITPType, Ns::Val{1}, t) _, Nt = size(d) - t = similar(d, Nt); copyto!(t, collect(range(zero(T), oneunit(T), Nt))) - return GriddedInterpolation((t, ), d[:], ITPType) + t_knots = _similar(t, Nt); copyto!(t_knots, collect(range(zero(eltype(t)), oneunit(eltype(t)), Nt))) + return GriddedInterpolation((t_knots, ), d[:], ITPType) end -function interpolate(d::AbstractArray{T}, ITPType, Ns::Val) where {T<:Real} +function interpolate(d, ITPType, Ns::Val, t) Ns, Nt = size(d) - id = similar(d, Ns); copyto!(id, collect(range(oneunit(T), T(Ns), Ns))) - t = similar(d, Nt); copyto!(t, collect(range(zero(T), oneunit(T), Nt))) - return GriddedInterpolation((id, t), d, ITPType) + id_knots = _similar(t, Ns); copyto!(id_knots, collect(range(oneunit(eltype(t)), eltype(t)(Ns), Ns))) + t_knots = _similar(t, Nt); copyto!(t_knots, collect(range(zero(eltype(t)), oneunit(eltype(t)), Nt))) + return GriddedInterpolation((id_knots, t_knots), d, ITPType) end -function resample(itp::Interpolator1D{T}, t) where {T<:Real} +function resample(itp::Interpolator1D, t) return itp.(t) end -function resample(itp::Interpolator2D{T}, t) where {T<:Real} +function resample(itp::Interpolator2D, t) Ns = size(itp.coefs, 1) - id = similar(itp.coefs, Ns) - copyto!(id, collect(range(oneunit(T), T(Ns), Ns))) + id = _similar(t, Ns) + copyto!(id, collect(range(oneunit(eltype(t)), eltype(t)(Ns), Ns))) return itp.(id, t) end function displacement_x!(ux, action::ArbitraryAction, x, y, z, t) - itp = interpolate(action.dx, Gridded(Linear()), Val(size(action.dx,1))) + itp = interpolate(action.dx, Gridded(Linear()), Val(size(action.dx,1)), t) ux .= resample(itp, t) return nothing end function displacement_y!(uy, action::ArbitraryAction, x, y, z, t) - itp = interpolate(action.dy, Gridded(Linear()), Val(size(action.dy,1))) + itp = interpolate(action.dy, Gridded(Linear()), Val(size(action.dy,1)), t) uy .= resample(itp, t) return nothing end function displacement_z!(uz, action::ArbitraryAction, x, y, z, t) - itp = interpolate(action.dz, Gridded(Linear()), Val(size(action.dz,1))) + itp = interpolate(action.dz, Gridded(Linear()), Val(size(action.dz,1)), t) uz .= resample(itp, t) return nothing end +_similar(a, N) = similar(a, N) +_similar(a::Real, N) = zeros(typeof(a), N) include("arbitraryactions/Path.jl") include("arbitraryactions/FlowPath.jl") \ No newline at end of file diff --git a/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl b/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl index 6c2051857..5faec8bdb 100644 --- a/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl +++ b/KomaMRIBase/src/motion/motionlist/actions/arbitraryactions/FlowPath.jl @@ -14,7 +14,7 @@ has a size of (``N_{spins} \times \; N_{discrete\,times}``). - `dx`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in x - `dy`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in y - `dz`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in z -- `spin_reset`: (`::AbstractArray{T<:Real}`) reset spin state flags +- `spin_reset`: (`::AbstractArray{Bool}`) reset spin state flags # Returns - `flowpath`: (`::FlowPath`) FlowPath struct @@ -33,5 +33,5 @@ julia> flowpath = FlowPath( dx::AbstractArray{T} dy::AbstractArray{T} dz::AbstractArray{T} - spin_reset::AbstractArray{T} + spin_reset::AbstractArray{Bool} end \ No newline at end of file diff --git a/KomaMRICore/ext/KomaoneAPIExt.jl b/KomaMRICore/ext/KomaoneAPIExt.jl index c58469421..e27b6cee3 100644 --- a/KomaMRICore/ext/KomaoneAPIExt.jl +++ b/KomaMRICore/ext/KomaoneAPIExt.jl @@ -47,8 +47,9 @@ end ## COV_EXCL_START @kernel function naive_cumsum!(B, @Const(A)) i = @index(Global) + T = eltype(A) - cur_val = 0.0f0 + cur_val = zero(T) for k ∈ 1:size(A, 2) @inbounds cur_val += A[i, k] @inbounds B[i, k] = cur_val diff --git a/KomaMRICore/src/simulation/Flow.jl b/KomaMRICore/src/simulation/Flow.jl index ac05f0f5e..8ad57a410 100644 --- a/KomaMRICore/src/simulation/Flow.jl +++ b/KomaMRICore/src/simulation/Flow.jl @@ -31,7 +31,7 @@ function outflow_spin_reset!( idx = KomaMRIBase.get_indexing_range(spin_span) spin_state_matrix = @view(spin_state_matrix[idx, :]) # Obtain mask - itp = KomaMRIBase.interpolate(action.spin_reset, KomaMRIBase.Gridded(KomaMRIBase.Constant{KomaMRIBase.Previous}()), Val(size(action.spin_reset, 1))) + itp = KomaMRIBase.interpolate(action.spin_reset, KomaMRIBase.Gridded(KomaMRIBase.Constant{KomaMRIBase.Previous}()), Val(size(action.spin_reset, 1)), t) mask = KomaMRIBase.resample(itp, ts) mask .= (cumsum(mask; dims=2) .== 0) # Modify spin state: reset and replace by initial value @@ -56,7 +56,7 @@ function outflow_spin_reset!( idx = KomaMRIBase.get_indexing_range(spin_span) M = @view(M[idx]) # Obtain mask - itp = KomaMRIBase.interpolate(action.spin_reset, KomaMRIBase.Gridded(KomaMRIBase.Constant{KomaMRIBase.Previous}()), Val(size(action.spin_reset, 1))) + itp = KomaMRIBase.interpolate(action.spin_reset, KomaMRIBase.Gridded(KomaMRIBase.Constant{KomaMRIBase.Previous}()), Val(size(action.spin_reset, 1)), t) mask = KomaMRIBase.resample(itp, ts) mask .= (cumsum(mask; dims=2) .== 0) mask = @view(mask[:, end]) diff --git a/KomaMRICore/test/runtests.jl b/KomaMRICore/test/runtests.jl index 739fe71c5..be10f384f 100644 --- a/KomaMRICore/test/runtests.jl +++ b/KomaMRICore/test/runtests.jl @@ -443,7 +443,7 @@ end Rotate(0.0, 0.0, 45.0, TimeRange(0.0, 1.0)), HeartBeat(-0.6, 0.0, 0.0, Periodic(1.0)), Path([0.0 0.0], [0.0 1.0], [0.0 0.0], TimeRange(0.0, 10.0)), - FlowPath([0.0 0.0], [0.0 1.0], [0.0 0.0], [0.0 0.0], TimeRange(0.0, 10.0)) + FlowPath([0.0 0.0], [0.0 1.0], [0.0 0.0], [false false], TimeRange(0.0, 10.0)) ] x0 = [0.1] From 567e406b0af7b171c28ce9e9541f66faaa864a51 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 19 Sep 2024 11:50:27 +0200 Subject: [PATCH 89/91] Add tests to new `plot_phantom_map` method --- .../test/GUI_PlotlyJS_backend_test.jl | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/KomaMRIPlots/test/GUI_PlotlyJS_backend_test.jl b/KomaMRIPlots/test/GUI_PlotlyJS_backend_test.jl index 011ecb027..ebf0d929f 100644 --- a/KomaMRIPlots/test/GUI_PlotlyJS_backend_test.jl +++ b/KomaMRIPlots/test/GUI_PlotlyJS_backend_test.jl @@ -36,6 +36,41 @@ end end + @testset "GUI_motion_phantom" begin + ph = brain_phantom2D() #2D phantom + ph.motion = MotionList(Translate(0.1, 0.1, 0.1, TimeRange(1:0), SpinRange(1:1000))) + + @testset "plot_motion_phantom_map_rho" begin + plot_phantom_map(ph, :ρ, width=800, height=600) #Plotting the phantom's rho map + @test true #If the previous line fails the test will fail + end + + @testset "plot_motion_phantom_map_T1" begin + plot_phantom_map(ph, :T1) #Plotting the phantom's rho map + @test true #If the previous line fails the test will fail + end + + @testset "plot_motion_phantom_map_T2" begin + plot_phantom_map(ph, :T2) #Plotting the phantom's rho map + @test true #If the previous line fails the test will fail + end + + @testset "plot_motion_phantom_map_x" begin + plot_phantom_map(ph, :x) #Plotting the phantom's rho map + @test true #If the previous line fails the test will fail + end + + @testset "plot_motion_phantom_map_w" begin + plot_phantom_map(ph, :Δw) #Plotting the phantom's rho map + @test true #If the previous line fails the test will fail + end + + @testset "plot_motion_phantom_map_2dview" begin + plot_phantom_map(ph, :ρ, view_2d=true) #Plotting the phantom's rho map + @test true #If the previous line fails the test will fail + end + end + @testset "GUI_seq" begin #KomaCore definition of a sequence: #RF construction From f2ad03bb72bc7c4893461ac18783add650b805f8 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Date: Thu, 19 Sep 2024 12:21:45 +0200 Subject: [PATCH 90/91] Solve bug with `TimeRange` in the KomaMRIPlots tests --- KomaMRIPlots/test/GUI_PlotlyJS_backend_test.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KomaMRIPlots/test/GUI_PlotlyJS_backend_test.jl b/KomaMRIPlots/test/GUI_PlotlyJS_backend_test.jl index ebf0d929f..f0e2f2aad 100644 --- a/KomaMRIPlots/test/GUI_PlotlyJS_backend_test.jl +++ b/KomaMRIPlots/test/GUI_PlotlyJS_backend_test.jl @@ -38,7 +38,7 @@ @testset "GUI_motion_phantom" begin ph = brain_phantom2D() #2D phantom - ph.motion = MotionList(Translate(0.1, 0.1, 0.1, TimeRange(1:0), SpinRange(1:1000))) + ph.motion = MotionList(Translate(0.1, 0.1, 0.1, TimeRange(0.0, 1.0), SpinRange(1:1000))) @testset "plot_motion_phantom_map_rho" begin plot_phantom_map(ph, :ρ, width=800, height=600) #Plotting the phantom's rho map From e416800d1165086a399412f8c4ea41b58099f1d3 Mon Sep 17 00:00:00 2001 From: Pablo Villacorta Aylagas Date: Thu, 19 Sep 2024 19:06:12 +0200 Subject: [PATCH 91/91] Suggested changes --- KomaMRIBase/src/KomaMRIBase.jl | 2 +- KomaMRIBase/src/datatypes/Phantom.jl | 4 ++-- KomaMRIBase/src/motion/{MotionSet.jl => AbstractMotion.jl} | 2 +- KomaMRIBase/src/motion/motionlist/MotionList.jl | 6 +++--- KomaMRIBase/src/motion/nomotion/NoMotion.jl | 2 +- KomaMRICore/src/simulation/Functors.jl | 3 +-- docs/src/reference/2-koma-base.md | 2 +- examples/3.tutorials/lit-05-SimpleMotion.jl | 4 ++-- 8 files changed, 12 insertions(+), 13 deletions(-) rename KomaMRIBase/src/motion/{MotionSet.jl => AbstractMotion.jl} (83%) diff --git a/KomaMRIBase/src/KomaMRIBase.jl b/KomaMRIBase/src/KomaMRIBase.jl index 7a4982c6e..385012c57 100644 --- a/KomaMRIBase/src/KomaMRIBase.jl +++ b/KomaMRIBase/src/KomaMRIBase.jl @@ -26,7 +26,7 @@ include("timing/KeyValuesCalculation.jl") include("datatypes/Sequence.jl") include("datatypes/sequence/Delay.jl") # Motion -include("motion/MotionSet.jl") +include("motion/AbstractMotion.jl") # Phantom include("datatypes/Phantom.jl") # Simulator diff --git a/KomaMRIBase/src/datatypes/Phantom.jl b/KomaMRIBase/src/datatypes/Phantom.jl index 68aa420fc..5f155f28e 100644 --- a/KomaMRIBase/src/datatypes/Phantom.jl +++ b/KomaMRIBase/src/datatypes/Phantom.jl @@ -17,7 +17,7 @@ a property value representing a spin. This struct serves as an input for the sim - `Dλ1`: (`::AbstractVector{T<:Real}`) spin Dλ1 (diffusion) parameter vector - `Dλ2`: (`::AbstractVector{T<:Real}`) spin Dλ2 (diffusion) parameter vector - `Dθ`: (`::AbstractVector{T<:Real}`) spin Dθ (diffusion) parameter vector -- `motion`: (`::AbstractMotionSet{T<:Real}`) motion set +- `motion`: (`::AbstractMotion{T<:Real}`) motion # Returns - `obj`: (`::Phantom`) Phantom struct @@ -47,7 +47,7 @@ julia> obj.ρ Dθ::AbstractVector{T} = zeros(eltype(x), size(x)) #Diff::Vector{DiffusionModel} #Diffusion map #Motion - motion::AbstractMotionSet{T} = NoMotion{eltype(x)}() + motion::AbstractMotion{T} = NoMotion{eltype(x)}() end const NON_STRING_PHANTOM_FIELDS = Iterators.filter(x -> fieldtype(Phantom, x) != String, fieldnames(Phantom)) diff --git a/KomaMRIBase/src/motion/MotionSet.jl b/KomaMRIBase/src/motion/AbstractMotion.jl similarity index 83% rename from KomaMRIBase/src/motion/MotionSet.jl rename to KomaMRIBase/src/motion/AbstractMotion.jl index c693eaba8..ca896fdcd 100644 --- a/KomaMRIBase/src/motion/MotionSet.jl +++ b/KomaMRIBase/src/motion/AbstractMotion.jl @@ -1,4 +1,4 @@ -abstract type AbstractMotionSet{T<:Real} end +abstract type AbstractMotion{T<:Real} end # NoMotion include("nomotion/NoMotion.jl") diff --git a/KomaMRIBase/src/motion/motionlist/MotionList.jl b/KomaMRIBase/src/motion/motionlist/MotionList.jl index c0b5a1094..591821ef7 100644 --- a/KomaMRIBase/src/motion/motionlist/MotionList.jl +++ b/KomaMRIBase/src/motion/motionlist/MotionList.jl @@ -27,7 +27,7 @@ julia> motionlist = MotionList( ) ``` """ -struct MotionList{T<:Real} <: AbstractMotionSet{T} +struct MotionList{T<:Real} <: AbstractMotion{T} motions::Vector{<:Motion{T}} end @@ -91,7 +91,7 @@ Calculates the position of each spin at a set of arbitrary time instants, i.e. t For each dimension (x, y, z), the output matrix has ``N_{\t{spins}}`` rows and `length(t)` columns. # Arguments -- `motionset`: (`::AbstractMotionSet{T<:Real}`) phantom motion +- `motionset`: (`::AbstractMotion{T<:Real}`) phantom motion - `x`: (`::AbstractVector{T<:Real}`, `[m]`) spin x-position vector - `y`: (`::AbstractVector{T<:Real}`, `[m]`) spin y-position vector - `z`: (`::AbstractVector{T<:Real}`, `[m]`) spin z-position vector @@ -148,7 +148,7 @@ If `motionset::NoMotion`, this function does nothing. If `motionset::MotionList`, this function sorts its motions. # Arguments -- `motionset`: (`::AbstractMotionSet{T<:Real}`) phantom motion +- `motionset`: (`::AbstractMotion{T<:Real}`) phantom motion # Returns - `nothing` diff --git a/KomaMRIBase/src/motion/nomotion/NoMotion.jl b/KomaMRIBase/src/motion/nomotion/NoMotion.jl index c9fcb209e..aba995c7c 100644 --- a/KomaMRIBase/src/motion/nomotion/NoMotion.jl +++ b/KomaMRIBase/src/motion/nomotion/NoMotion.jl @@ -11,7 +11,7 @@ NoMotion struct. It is used to create static phantoms. julia> nomotion = NoMotion{Float64}() ``` """ -struct NoMotion{T<:Real} <: AbstractMotionSet{T} end +struct NoMotion{T<:Real} <: AbstractMotion{T} end Base.getindex(mv::NoMotion, p) = mv Base.view(mv::NoMotion, p) = mv diff --git a/KomaMRICore/src/simulation/Functors.jl b/KomaMRICore/src/simulation/Functors.jl index 42b3a89ac..ab95565a0 100644 --- a/KomaMRICore/src/simulation/Functors.jl +++ b/KomaMRICore/src/simulation/Functors.jl @@ -99,9 +99,8 @@ f64(m) = paramtype(Float64, m) # Koma motion-related adapts adapt_storage(backend::KA.GPU, xs::MotionList) = MotionList(gpu.(xs.motions, Ref(backend))) -adapt_storage(T::Type{<:Real}, xs::NoMotion) = NoMotion{T}() adapt_storage(T::Type{<:Real}, xs::MotionList) = MotionList(paramtype.(T, xs.motions)) -adapt_storage(T::Type{<:Real}, xs::Motion) = Motion(paramtype(T, xs.action), paramtype(T, xs.time), xs.spins) +adapt_storage(T::Type{<:Real}, xs::NoMotion) = NoMotion{T}() #The functor macro makes it easier to call a function in all the parameters # Phantom diff --git a/docs/src/reference/2-koma-base.md b/docs/src/reference/2-koma-base.md index d410c0836..9d2d4bb0b 100644 --- a/docs/src/reference/2-koma-base.md +++ b/docs/src/reference/2-koma-base.md @@ -22,7 +22,7 @@ heart_phantom ## `Motion`-related functions -### `AbstractMotionSet` types and related functions +### `AbstractMotion` types and related functions ```@docs NoMotion MotionList diff --git a/examples/3.tutorials/lit-05-SimpleMotion.jl b/examples/3.tutorials/lit-05-SimpleMotion.jl index 31f14dc1b..24b7d7ad2 100644 --- a/examples/3.tutorials/lit-05-SimpleMotion.jl +++ b/examples/3.tutorials/lit-05-SimpleMotion.jl @@ -4,7 +4,7 @@ using KomaMRI # hide sys = Scanner() # hide # It can also be interesting to see the effect of the patient's motion during an MRI scan. -# For this, Koma provides the ability to add `motion <: AbstractMotionSet` to the phantom. +# For this, Koma provides the ability to add `motion <: AbstractMotion` to the phantom. # In this tutorial, we will show how to add a [`Translate`](@ref) motion to a 2D brain phantom. # First, let's load the 2D brain phantom used in the previous tutorials: @@ -67,7 +67,7 @@ p2 = plot_image(abs.(image1[:, :, 1]); height=400) # hide # S_{\mathrm{MC}}\left(t\right)=S\left(t\right)\cdot\mathrm{e}^{\mathrm{i}\Delta\phi_{\mathrm{corr}}}=S\left(t\right)\cdot\mathrm{e}^{\mathrm{i}2\pi\boldsymbol{k}\left(t\right)\cdot\boldsymbol{u}\left(t\right)} # ``` -# In practice, we would need to estimate or measure the motion before performing a motion-corrected reconstruction, but for this example, we will directly use the displacement functions ``\boldsymbol{u}(\boldsymbol{x}, t)`` defined by `obj.motion::SimpleAction`. +# In practice, we would need to estimate or measure the motion before performing a motion-corrected reconstruction, but for this example, we will directly use the displacement functions ``\boldsymbol{u}(\boldsymbol{x}, t)`` defined by `obj.motion::MotionList`. # Since translations are rigid motions (``\boldsymbol{u}(\boldsymbol{x}, t)=\boldsymbol{u}(t)`` no position dependence), we can obtain the required displacements by calculating ``\boldsymbol{u}(\boldsymbol{x}=\boldsymbol{0},\ t=t_{\mathrm{adc}})``. sample_times = get_adc_sampling_times(seq1) displacements = hcat(get_spin_coords(obj.motion, [0.0], [0.0], [0.0], sample_times)...)