Skip to content

Commit 782e543

Browse files
authored
Merge pull request #531 from JuliaHealth/time-curve
Flexibilize the definition of motion time spans
2 parents 99dc53a + c20de8f commit 782e543

File tree

21 files changed

+2436
-1037
lines changed

21 files changed

+2436
-1037
lines changed

KomaMRIBase/src/KomaMRIBase.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export MotionList, NoMotion, Motion
5353
export Translate, TranslateX, TranslateY, TranslateZ
5454
export Rotate, RotateX, RotateY, RotateZ
5555
export HeartBeat, Path, FlowPath
56-
export TimeRange, Periodic
56+
export TimeRange, Periodic, TimeCurve
5757
export SpinRange, AllSpins
5858
export get_spin_coords
5959
# Secondary
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# We defined two types of Interpolation objects: Interpolator1D and Interpolator2D
2+
# 1D is for interpolating for 1 spin
3+
# 2D is for interpolating for 2 or more spins
4+
# This dispatch based on the number of spins wouldn't be necessary if it weren't for this:
5+
# https://github.com/JuliaMath/Interpolations.jl/issues/603
6+
#
7+
# Once this issue is solved, this file should be simpler.
8+
# We should then be able to define a single method for functions:
9+
# - interpolate
10+
# - resample
11+
# and delete the Interpolator1D and Interpolator2D definitions
12+
13+
const Interpolator1D = Interpolations.GriddedInterpolation{
14+
TCoefs,1,V,Itp,K
15+
} where {
16+
TCoefs<:Real,
17+
TNodes<:Real,
18+
V<:AbstractArray{TCoefs},
19+
Itp<:Interpolations.Gridded,
20+
K<:Tuple{AbstractVector{TNodes}},
21+
}
22+
23+
const Interpolator2D = Interpolations.GriddedInterpolation{
24+
TCoefs,2,V,Itp,K
25+
} where {
26+
TCoefs<:Real,
27+
TNodes<:Real,
28+
V<:AbstractArray{TCoefs},
29+
Itp<:Interpolations.Gridded,
30+
K<:Tuple{AbstractVector{TNodes}, AbstractVector{TNodes}},
31+
}
32+
function GriddedInterpolation(nodes, A, ITP)
33+
return Interpolations.GriddedInterpolation{eltype(A), length(nodes), typeof(A), typeof(ITP), typeof(nodes)}(nodes, A, ITP)
34+
end
35+
36+
function interpolate(d, ITPType, Ns::Val{1}, t)
37+
_, Nt = size(d)
38+
t_knots = _similar(t, Nt); copyto!(t_knots, collect(range(zero(eltype(t)), oneunit(eltype(t)), Nt)))
39+
return GriddedInterpolation((t_knots, ), d[:], ITPType)
40+
end
41+
42+
function interpolate(d, ITPType, Ns::Val, t)
43+
Ns, Nt = size(d)
44+
id_knots = _similar(t, Ns); copyto!(id_knots, collect(range(oneunit(eltype(t)), eltype(t)(Ns), Ns)))
45+
t_knots = _similar(t, Nt); copyto!(t_knots, collect(range(zero(eltype(t)), oneunit(eltype(t)), Nt)))
46+
return GriddedInterpolation((id_knots, t_knots), d, ITPType)
47+
end
48+
49+
function resample(itp::Interpolator1D, t)
50+
return itp.(t)
51+
end
52+
53+
function resample(itp::Interpolator2D, t)
54+
Ns = size(itp.coefs, 1)
55+
id = _similar(t, Ns)
56+
copyto!(id, collect(range(oneunit(eltype(t)), eltype(t)(Ns), Ns)))
57+
return itp.(id, t)
58+
end
59+
60+
function interpolate_times(t, t_unit, periodic, tq)
61+
itp = GriddedInterpolation((t, ), t_unit, Gridded(Linear()))
62+
return extrapolate(itp, periodic ? Interpolations.Periodic() : Flat()).(tq)
63+
end
64+
65+
_similar(a, N) = similar(a, N)
66+
_similar(a::Real, N) = zeros(typeof(a), N)

KomaMRIBase/src/motion/Motion.jl

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ that are affected by that motion.
1111
1212
# Arguments
1313
- `action`: (`::AbstractAction{T<:Real}`) action, such as [`Translate`](@ref) or [`Rotate`](@ref)
14-
- `time`: (`::AbstractTimeSpan{T<:Real}`, `=TimeRange(0.0)`) time information about the motion
14+
- `time`: (`::TimeCurve{T<:Real}`, `=TimeRange(0.0)`) time information about the motion
1515
- `spins`: (`::AbstractSpinSpan`, `=AllSpins()`) spin indexes affected by the motion
1616
1717
# Returns
@@ -28,22 +28,22 @@ julia> motion = Motion(
2828
"""
2929
@with_kw mutable struct Motion{T<:Real}
3030
action::AbstractAction{T}
31-
time ::AbstractTimeSpan{T} = TimeRange(zero(typeof(action).parameters[1]))
32-
spins ::AbstractSpinSpan = AllSpins()
31+
time ::TimeCurve{T} = TimeRange(t_start=zero(typeof(action).parameters[1]), t_end=eps(typeof(action).parameters[1]))
32+
spins ::AbstractSpinSpan = AllSpins()
3333
end
3434

3535
# Main constructors
3636
function Motion(action)
3737
T = first(typeof(action).parameters)
38-
return Motion(action, TimeRange(zero(T)), AllSpins())
38+
return Motion(action, TimeRange(t_start=zero(T), t_end=eps(T)), AllSpins())
3939
end
40-
function Motion(action, time::AbstractTimeSpan)
40+
function Motion(action, time::TimeCurve)
4141
T = first(typeof(action).parameters)
4242
return Motion(action, time, AllSpins())
4343
end
4444
function Motion(action, spins::AbstractSpinSpan)
4545
T = first(typeof(action).parameters)
46-
return Motion(action, TimeRange(zero(T)), spins)
46+
return Motion(action, TimeRange(t_start=zero(T), t_end=eps(T)), spins)
4747
end
4848

4949
# Custom constructors
@@ -54,7 +54,7 @@ end
5454
- `dx`: (`::Real`, `[m]`) translation in x
5555
- `dy`: (`::Real`, `[m]`) translation in y
5656
- `dz`: (`::Real`, `[m]`) translation in z
57-
- `time`: (`::AbstractTimeSpan{T<:Real}`) time information about the motion
57+
- `time`: (`::TimeCurve{T<:Real}`) time information about the motion
5858
- `spins`: (`::AbstractSpinSpan`) spin indexes affected by the motion
5959
6060
# Returns
@@ -65,7 +65,7 @@ end
6565
julia> translate = Translate(0.01, 0.02, 0.03, TimeRange(0.0, 1.0), SpinRange(1:10))
6666
```
6767
"""
68-
function Translate(dx, dy, dz, time=TimeRange(zero(eltype(dx))), spins=AllSpins())
68+
function Translate(dx, dy, dz, time=TimeRange(t_start=zero(eltype(dx)), t_end=eps(eltype(dx))), spins=AllSpins())
6969
return Motion(Translate(dx, dy, dz), time, spins)
7070
end
7171

@@ -76,7 +76,7 @@ end
7676
- `pitch`: (`::Real`, `[º]`) rotation in x
7777
- `roll`: (`::Real`, `[º]`) rotation in y
7878
- `yaw`: (`::Real`, `[º]`) rotation in z
79-
- `time`: (`::AbstractTimeSpan{T<:Real}`) time information about the motion
79+
- `time`: (`::TimeCurve{T<:Real}`) time information about the motion
8080
- `spins`: (`::AbstractSpinSpan`) spin indexes affected by the motion
8181
8282
# Returns
@@ -87,7 +87,7 @@ end
8787
julia> rotate = Rotate(15.0, 0.0, 20.0, TimeRange(0.0, 1.0), SpinRange(1:10))
8888
```
8989
"""
90-
function Rotate(pitch, roll, yaw, time=TimeRange(zero(eltype(pitch))), spins=AllSpins())
90+
function Rotate(pitch, roll, yaw, time=TimeRange(t_start=zero(eltype(pitch)), t_end=eps(eltype(pitch))), spins=AllSpins())
9191
return Motion(Rotate(pitch, roll, yaw), time, spins)
9292
end
9393

@@ -98,7 +98,7 @@ end
9898
- `circumferential_strain`: (`::Real`) contraction parameter
9999
- `radial_strain`: (`::Real`) contraction parameter
100100
- `longitudinal_strain`: (`::Real`) contraction parameter
101-
- `time`: (`::AbstractTimeSpan{T<:Real}`) time information about the motion
101+
- `time`: (`::TimeCurve{T<:Real}`) time information about the motion
102102
- `spins`: (`::AbstractSpinSpan`) spin indexes affected by the motion
103103
104104
# Returns
@@ -109,7 +109,7 @@ end
109109
julia> heartbeat = HeartBeat(-0.3, -0.2, 0.0, TimeRange(0.0, 1.0), SpinRange(1:10))
110110
```
111111
"""
112-
function HeartBeat(circumferential_strain, radial_strain, longitudinal_strain, time=TimeRange(zero(eltype(circumferential_strain))), spins=AllSpins())
112+
function HeartBeat(circumferential_strain, radial_strain, longitudinal_strain, time=TimeRange(t_start=zero(eltype(circumferential_strain)), t_end=eps(eltype(circumferential_strain))), spins=AllSpins())
113113
return Motion(HeartBeat(circumferential_strain, radial_strain, longitudinal_strain), time, spins)
114114
end
115115

@@ -120,7 +120,7 @@ end
120120
- `dx`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in x
121121
- `dy`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in y
122122
- `dz`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in z
123-
- `time`: (`::AbstractTimeSpan{T<:Real}`) time information about the motion
123+
- `time`: (`::TimeCurve{T<:Real}`) time information about the motion
124124
- `spins`: (`::AbstractSpinSpan`) spin indexes affected by the motion
125125
126126
# Returns
@@ -137,7 +137,7 @@ julia> path = Path(
137137
)
138138
```
139139
"""
140-
function Path(dx, dy, dz, time=TimeRange(zero(eltype(dx))), spins=AllSpins())
140+
function Path(dx, dy, dz, time=TimeRange(t_start=zero(eltype(dx)), t_end=eps(eltype(dx))), spins=AllSpins())
141141
return Motion(Path(dx, dy, dz), time, spins)
142142
end
143143

@@ -149,7 +149,7 @@ end
149149
- `dy`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in y
150150
- `dz`: (`::AbstractArray{T<:Real}`, `[m]`) displacements in z
151151
- `spin_reset`: (`::AbstractArray{Bool}`) reset spin state flags
152-
- `time`: (`::AbstractTimeSpan{T<:Real}`) time information about the motion
152+
- `time`: (`::TimeCurve{T<:Real}`) time information about the motion
153153
- `spins`: (`::AbstractSpinSpan`) spin indexes affected by the motion
154154
155155
# Returns
@@ -167,7 +167,7 @@ julia> flowpath = FlowPath(
167167
)
168168
```
169169
"""
170-
function FlowPath(dx, dy, dz, spin_reset, time=TimeRange(zero(eltype(dx))), spins=AllSpins())
170+
function FlowPath(dx, dy, dz, spin_reset, time=TimeRange(t_start=zero(eltype(dx)), t_end=eps(eltype(dx))), spins=AllSpins())
171171
return Motion(FlowPath(dx, dy, dz, spin_reset), time, spins)
172172
end
173173

@@ -192,7 +192,7 @@ function get_spin_coords(
192192
m::Motion{T}, x::AbstractVector{T}, y::AbstractVector{T}, z::AbstractVector{T}, t
193193
) where {T<:Real}
194194
ux, uy, uz = x .* (0*t), y .* (0*t), z .* (0*t) # Buffers for displacements
195-
t_unit = unit_time(t, m.time)
195+
t_unit = unit_time(t, m.time.t, m.time.t_unit, m.time.periodic, m.time.periods)
196196
idx = get_indexing_range(m.spins)
197197
displacement_x!(@view(ux[idx, :]), m.action, @view(x[idx]), @view(y[idx]), @view(z[idx]), t_unit)
198198
displacement_y!(@view(uy[idx, :]), m.action, @view(x[idx]), @view(y[idx]), @view(z[idx]), t_unit)
@@ -201,7 +201,7 @@ function get_spin_coords(
201201
end
202202

203203
# Auxiliary functions
204-
times(m::Motion) = times(m.time)
204+
times(m::Motion) = times(m.time.t, m.time.periods)
205205
is_composable(m::Motion) = is_composable(m.action)
206206
add_jump_times!(t, m::Motion) = add_jump_times!(t, m.action, m.time)
207-
add_jump_times!(t, ::AbstractAction, ::AbstractTimeSpan) = nothing
207+
add_jump_times!(t, ::AbstractAction, ::TimeCurve) = nothing

KomaMRIBase/src/motion/MotionList.jl

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
include("Interpolation.jl")
12
include("SpinSpan.jl")
2-
include("TimeSpan.jl")
3+
include("TimeCurve.jl")
34
include("Action.jl")
45
include("Motion.jl")
56

@@ -156,7 +157,7 @@ function get_spin_coords(
156157
ux, uy, uz = xt .* zero(T), yt .* zero(T), zt .* zero(T)
157158
# Composable motions: they need to be run sequentially. Note that they depend on xt, yt, and zt
158159
for m in Iterators.filter(is_composable, ml.motions)
159-
t_unit = unit_time(t, m.time)
160+
t_unit = unit_time(t, m.time.t, m.time.t_unit, m.time.periodic, m.time.periods)
160161
idx = get_indexing_range(m.spins)
161162
displacement_x!(@view(ux[idx, :]), m.action, @view(xt[idx, :]), @view(yt[idx, :]), @view(zt[idx, :]), t_unit)
162163
displacement_y!(@view(uy[idx, :]), m.action, @view(xt[idx, :]), @view(yt[idx, :]), @view(zt[idx, :]), t_unit)
@@ -166,7 +167,7 @@ function get_spin_coords(
166167
end
167168
# Additive motions: these motions can be run in parallel
168169
for m in Iterators.filter(!is_composable, ml.motions)
169-
t_unit = unit_time(t, m.time)
170+
t_unit = unit_time(t, m.time.t, m.time.t_unit, m.time.periodic, m.time.periods)
170171
idx = get_indexing_range(m.spins)
171172
displacement_x!(@view(ux[idx, :]), m.action, @view(x[idx]), @view(y[idx]), @view(z[idx]), t_unit)
172173
displacement_y!(@view(uy[idx, :]), m.action, @view(x[idx]), @view(y[idx]), @view(z[idx]), t_unit)
@@ -199,7 +200,7 @@ If `motionset::MotionList`, this function sorts its motions.
199200
- `nothing`
200201
"""
201202
function sort_motions!(m::MotionList)
202-
sort!(m.motions; by=m -> times(m)[1])
203+
sort!(m.motions; by=m -> m.time.t_start)
203204
return nothing
204205
end
205206

KomaMRIBase/src/motion/TimeCurve.jl

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
@doc raw"""
2+
timecurve = TimeCurve(t, t_unit, periodic, periods)
3+
4+
TimeCurve struct. It is a specialized type that defines a time curve, which represents
5+
the temporal behavior of motion. This curve is defined by two vectors:
6+
`t` and `t_unit`, which represent the horizontal (x-axis) and vertical (y-axis) axes
7+
of the curve, respectively. To some extent, this curve can be associated with animation curves,
8+
commonly found in software for video editing, 3D scene creation, or video game development.
9+
10+
Additionally, the TimeCurve struct contains two more fields, independent of each other:
11+
`periodic` is a Boolean that indicates whether the time curve should be repeated periodically.
12+
`periods` contains as many elements as repetitions are desired in the time curve.
13+
Each element specifies the scaling factor for that repetition.
14+
15+
# Arguments
16+
- `t`: (`::AbstractVector{<:Real}`, `[s]`) time vector
17+
- `t_unit`: (`::AbstractVector{<:Real}`) y vector, it needs to be scaled between 0 and 1
18+
- `periodic`: (`::Bool`, `=false`) indicates whether the time curve should be periodically repeated
19+
- `periods`: (`::Union{<:Real,AbstractVector{<:Real}}`, `=1.0`): represents the relative duration
20+
of each period with respect to the baseline duration defined by `t[end] - t[1]`.
21+
In other words, it acts as a scaling factor to lengthen or shorten specific periods.
22+
This allows for the creation of patterns such as arrhythmias or other variations in periodicity.
23+
24+
# Returns
25+
- `timecurve`: (`::TimeCurve`) TimeCurve struct
26+
27+
# Examples
28+
1\. Non-periodic motion with a single repetition:
29+
```julia-repl
30+
julia> timecurve = TimeCurve(t=[0.0, 0.2, 0.4, 0.6], t_unit=[0.0, 0.2, 0.5, 1.0])
31+
```
32+
![Time Curve 1](../assets/time-curve-1.svg)
33+
34+
2\. Periodic motion with a single repetition:
35+
```julia-repl
36+
julia> timecurve = TimeCurve(t=[0.0, 0.2, 0.4, 0.6], t_unit=[0.0, 1.0, 1.0, 0.0], periodic=true)
37+
```
38+
![Time Curve 2](../assets/time-curve-2.svg)
39+
40+
3\. Non-periodic motion with multiple repetitions:
41+
```julia-repl
42+
julia> timecurve = TimeCurve(t=[0.0, 0.2, 0.4, 0.6], t_unit=[0.0, 1.0, 1.0, 0.0], periods=[1.0, 0.5, 1.5])
43+
```
44+
![Time Curve 3](../assets/time-curve-3.svg)
45+
46+
4\. Periodic motion with multiple repetitions:
47+
```julia-repl
48+
julia> timecurve = TimeCurve(t=[0.0, 0.2, 0.4, 0.6], t_unit=[0.0, 1.0, 1.0, 0.0], periods=[1.0, 0.5, 1.5], periodic=true)
49+
```
50+
![Time Curve 4](../assets/time-curve-4.svg)
51+
"""
52+
@with_kw struct TimeCurve{T<:Real}
53+
t::AbstractVector{T}
54+
t_unit::AbstractVector{T}
55+
periodic::Bool = false
56+
periods::Union{T,AbstractVector{T}} = oneunit(eltype(t))
57+
t_start::T = t[1]
58+
t_end::T = t[end]
59+
@assert check_unique(t) "Vector t=$(t) contains duplicate elements. Please ensure all elements in t are unique and try again"
60+
end
61+
62+
check_unique(t) = true
63+
check_unique(t::Vector) = length(t) == length(unique(t))
64+
65+
# Main Constructors
66+
TimeCurve(t, t_unit, periodic, periods) = TimeCurve(t=t, t_unit=t_unit, periodic=periodic, periods=periods)
67+
TimeCurve(t, t_unit) = TimeCurve(t=t, t_unit=t_unit)
68+
# Custom constructors
69+
"""
70+
timerange = TimeRange(t_start, t_end)
71+
72+
The `TimeRange` function is a custom constructor for the `TimeCurve` struct.
73+
It allows defining a simple time interval, with start and end times.
74+
75+
# Arguments
76+
- `t_start`: (`::Real`, `[s]`, `=0.0`) start time
77+
- `t_end`: (`::Real`, `[s]`, `=1.0`) end time
78+
79+
# Returns
80+
- `timerange`: (`::TimeCurve`) TimeCurve struct
81+
82+
# Examples
83+
```julia-repl
84+
julia> timerange = TimeRange(t_start=0.6, t_end=1.4)
85+
```
86+
![Time Range](../assets/time-range.svg)
87+
"""
88+
TimeRange(t_start::T, t_end::T) where T = TimeCurve(t=[t_start, t_end], t_unit=[zero(T), oneunit(T)])
89+
TimeRange(; t_start=0.0, t_end=1.0) = TimeRange(t_start, t_end)
90+
"""
91+
periodic = Periodic(period, asymmetry)
92+
93+
The `Periodic` function is a custom constructor for the `TimeCurve` struct.
94+
It allows defining time intervals that repeat periodically with a triangular period.
95+
It includes a measure of asymmetry in order to recreate a asymmetric period.
96+
97+
# Arguments
98+
- `period`: (`::Real`, `[s]`, `=1.0`) period duration
99+
- `asymmetry`: (`::Real`, `=0.5`) temporal asymmetry factor. Between 0 and 1.
100+
101+
# Returns
102+
- `periodic`: (`::TimeCurve`) TimeCurve struct
103+
104+
# Examples
105+
```julia-repl
106+
julia> periodic = Periodic(period=1.0, asymmetry=0.2)
107+
```
108+
![Periodic](../assets/periodic.svg)
109+
"""
110+
Periodic(period::T, asymmetry::T) where T = TimeCurve(t=[zero(T), period*asymmetry, period], t_unit=[zero(T), oneunit(T), zero(T)])
111+
Periodic(; period=1.0, asymmetry=0.5) = Periodic(period, asymmetry)
112+
113+
""" Compare two TimeCurves """
114+
Base.:(==)(t1::TimeCurve, t2::TimeCurve) = reduce(&, [getfield(t1, field) == getfield(t2, field) for field in fieldnames(typeof(t1))])
115+
Base.:()(t1::TimeCurve, t2::TimeCurve) = reduce(&, [getfield(t1, field) getfield(t2, field) for field in fieldnames(typeof(t1))])
116+
117+
""" times & unit_time """
118+
# Although the implementation of these two functions when `per` is a vector is valid
119+
# for all cases, it performs unnecessary and costly operations when `per` is a scalar.
120+
# Therefore, it has been decided to use method dispatch between these two cases.
121+
function times(t, per::Real)
122+
return per .* t
123+
end
124+
function times(t, per::AbstractVector)
125+
tr = repeat(t, length(per))
126+
scale = repeat(per, inner=[length(t)])
127+
offsets = repeat(vcat(0, cumsum(per)[1:end-1]), inner=[length(t)])
128+
tr .= (tr .* scale) .+ offsets
129+
return tr
130+
end
131+
function unit_time(tq, t, t_unit, periodic, per::Real)
132+
return interpolate_times(t .* per, t_unit, periodic, tq)
133+
end
134+
function unit_time(tq, t, t_unit, periodic, per::AbstractVector)
135+
return interpolate_times(times(t, per), repeat(t_unit, length(per)), periodic, tq)
136+
end

0 commit comments

Comments
 (0)