Skip to content

Commit cebb4e3

Browse files
committed
Add Interpolation module and Core utilities from CTModels
- Interpolation module: ctinterpolate, ctinterpolate_constant - Core utilities: matrix_utils, function_utils, macros - Tests for all new modules
1 parent e801b6b commit cebb4e3

9 files changed

Lines changed: 534 additions & 0 deletions

File tree

src/Core/function_utils.jl

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""
2+
$(TYPEDSIGNATURES)
3+
4+
Convert an in-place function `f!` to an out-of-place function `f`.
5+
6+
The resulting function `f` returns a vector of type `T` and length `n` by first allocating
7+
memory and then calling `f!` to fill it.
8+
9+
# Arguments
10+
- `f!`: An in-place function of the form `f!(result, args...)`.
11+
- `n`: The length of the output vector.
12+
- `T`: The element type of the output vector (default is `Float64`).
13+
14+
# Returns
15+
An out-of-place function `f(args...; kwargs...)` that returns the result as a vector or
16+
scalar, depending on `n`.
17+
18+
# Example
19+
```julia-repl
20+
julia> f!(r, x) = (r[1] = sin(x); r[2] = cos(x))
21+
julia> f = to_out_of_place(f!, 2)
22+
julia> f(π/4) # returns approximately [0.707, 0.707]
23+
```
24+
"""
25+
function to_out_of_place(f!, n; T=Float64)
26+
function f(args...; kwargs...)
27+
r = zeros(T, n)
28+
f!(r, args...; kwargs...)
29+
return n == 1 ? r[1] : r
30+
end
31+
return isnothing(f!) ? nothing : f
32+
end

src/Core/macros.jl

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""
2+
@ensure condition exception
3+
4+
Throws the provided `exception` if `condition` is false.
5+
6+
# Usage
7+
```julia-repl
8+
julia> @ensure true Exceptions.IncorrectArgument("This won't throw")
9+
julia> @ensure false Exceptions.IncorrectArgument("This will throw")
10+
ERROR: IncorrectArgument("This will throw")
11+
```
12+
13+
# Arguments
14+
- `condition`: A Boolean expression to test.
15+
- `exception`: An instance of an exception to throw if `condition` is false.
16+
17+
# Throws
18+
- The provided `exception` if the condition is not satisfied.
19+
"""
20+
macro ensure(cond, exc)
21+
return esc(:(
22+
if !($cond)
23+
throw($exc)
24+
end
25+
))
26+
end

src/Core/matrix_utils.jl

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""
2+
$(TYPEDSIGNATURES)
3+
4+
Return the default dimension for matrix storage.
5+
"""
6+
__matrix_dimension_storage() = 1
7+
8+
"""
9+
$(TYPEDSIGNATURES)
10+
11+
Transform a matrix into a vector of vectors along the specified dimension.
12+
13+
Each row or column of the matrix `A` is extracted and stored as an individual vector,
14+
depending on `dim`.
15+
16+
# Arguments
17+
- `A`: A matrix of elements of type `<:ctNumber`.
18+
- `dim`: The dimension along which to split the matrix (`1` for rows, `2` for columns).
19+
Defaults to `1`.
20+
21+
# Returns
22+
A `Vector` of `Vector`s extracted from the rows or columns of `A`.
23+
24+
# Note
25+
This is useful when data needs to be represented as a sequence of state or control vectors
26+
in optimal control problems.
27+
28+
# Example
29+
```julia-repl
30+
julia> A = [1 2 3; 4 5 6]
31+
julia> matrix2vec(A, 1) # splits into rows: [[1, 2, 3], [4, 5, 6]]
32+
julia> matrix2vec(A, 2) # splits into columns: [[1, 4], [2, 5], [3, 6]]
33+
```
34+
"""
35+
function matrix2vec(
36+
A::Matrix{<:ctNumber}, dim::Int=__matrix_dimension_storage()
37+
)::Vector{<:Vector{<:ctNumber}}
38+
return dim == 1 ? [A[i, :] for i in 1:size(A, 1)] : [A[:, i] for i in 1:size(A, 2)]
39+
end

src/Interpolation/Interpolation.jl

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""
2+
Interpolation
3+
4+
Interpolation utilities for the Control Toolbox (CT) ecosystem.
5+
6+
# Public API
7+
- `ctinterpolate`: linear interpolation with flat extrapolation
8+
- `ctinterpolate_constant`: piecewise-constant (steppost) interpolation
9+
"""
10+
module Interpolation
11+
12+
import DocStringExtensions: TYPEDSIGNATURES
13+
14+
include(joinpath(@__DIR__, "ctinterpolate.jl"))
15+
16+
export ctinterpolate, ctinterpolate_constant
17+
18+
end # module Interpolation

src/Interpolation/ctinterpolate.jl

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""
2+
$(TYPEDSIGNATURES)
3+
4+
Return a linear interpolation function for the data `f` defined at points `x`.
5+
6+
This function creates a one-dimensional linear interpolant with flat extrapolation
7+
beyond the bounds of `x` (returns `f[1]` for `t < x[1]` and `f[end]` for `t >= x[end]`).
8+
9+
# Arguments
10+
- `x`: A vector of points at which the values `f` are defined.
11+
- `f`: A vector of values to interpolate.
12+
13+
# Returns
14+
A callable interpolation object that can be evaluated at new points.
15+
16+
# Example
17+
```julia-repl
18+
julia> x = [0.0, 1.0, 2.0]
19+
julia> f = [1.0, 2.0, 3.0]
20+
julia> interp = ctinterpolate(x, f)
21+
julia> interp(0.5) # Returns 1.5 (linear interpolation)
22+
julia> interp(-1.0) # Returns 1.0 (flat extrapolation)
23+
julia> interp(3.0) # Returns 3.0 (flat extrapolation)
24+
```
25+
"""
26+
function ctinterpolate(x, f)
27+
function linear_interp(t)
28+
if t < x[1]
29+
return f[1]
30+
elseif t >= x[end]
31+
return f[end]
32+
else
33+
i = searchsortedlast(x, t)
34+
if i == length(x)
35+
return f[end]
36+
end
37+
α = (t - x[i]) / (x[i + 1] - x[i])
38+
return f[i] + α * (f[i + 1] - f[i])
39+
end
40+
end
41+
return linear_interp
42+
end
43+
44+
"""
45+
$(TYPEDSIGNATURES)
46+
47+
Return a piecewise-constant interpolation function for the data `f` defined at points `x`.
48+
49+
This function creates a right-continuous piecewise constant interpolant:
50+
the value at knot `x[i]` is held constant on the interval `[x[i], x[i+1})`.
51+
52+
This implements the standard steppost behavior for optimal control:
53+
- `u(t_i) = u_i` (value at the knot)
54+
- `u(t) = u_i` for all `t ∈ [t_i, t_{i+1})`
55+
- Right-continuous: `lim_{t→t_i^+} u(t) = u(t_i)`
56+
57+
# Arguments
58+
- `x`: A vector of points at which the values `f` are defined.
59+
- `f`: A vector of values to interpolate.
60+
61+
# Returns
62+
A callable interpolation object that can be evaluated at new points.
63+
64+
# Example
65+
```julia-repl
66+
julia> x = [0.0, 1.0, 2.0]
67+
julia> f = [1.0, 2.0, 3.0]
68+
julia> interp = ctinterpolate_constant(x, f)
69+
julia> interp(0.0) # Returns 1.0 (value at x[1])
70+
julia> interp(0.5) # Returns 1.0 (held from x[1] on [0.0, 1.0))
71+
julia> interp(1.0) # Returns 2.0 (value at x[2], right-continuous)
72+
julia> interp(1.5) # Returns 2.0 (held from x[2] on [1.0, 2.0))
73+
```
74+
"""
75+
function ctinterpolate_constant(x, f)
76+
function steppost_interp(t)
77+
if t < x[1]
78+
return f[1]
79+
elseif t >= x[end]
80+
return f[end]
81+
else
82+
i = searchsortedlast(x, t)
83+
return f[i]
84+
end
85+
end
86+
return steppost_interp
87+
end
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
module TestCoreFunctionUtils
2+
3+
import Test
4+
import CTBase.Core
5+
6+
const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true
7+
const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true
8+
9+
function test_function_utils()
10+
Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "to_out_of_place" begin
11+
12+
Test.@testset "to_out_of_place - basic conversion" begin
13+
f!(r, x) = (r[1] = sin(x); r[2] = cos(x))
14+
f = Core.to_out_of_place(f!, 2)
15+
result = f/ 4)
16+
Test.@test result isa Vector
17+
Test.@test length(result) == 2
18+
Test.@test result[1] sin/ 4)
19+
Test.@test result[2] cos/ 4)
20+
end
21+
22+
Test.@testset "to_out_of_place - scalar output (n=1)" begin
23+
g!(r, x) = (r[1] = x^2)
24+
g = Core.to_out_of_place(g!, 1)
25+
result = g(3.0)
26+
Test.@test result isa Float64
27+
Test.@test result 9.0
28+
end
29+
30+
Test.@testset "to_out_of_place - with kwargs" begin
31+
h!(r, x; scale=1.0) = (r[1] = x * scale; r[2] = x^2 * scale)
32+
h = Core.to_out_of_place(h!, 2)
33+
Test.@test h(2.0)[1] 2.0
34+
Test.@test h(2.0)[2] 4.0
35+
Test.@test h(2.0; scale=3.0)[1] 6.0
36+
Test.@test h(2.0; scale=3.0)[2] 12.0
37+
end
38+
39+
Test.@testset "to_out_of_place - multiple arguments" begin
40+
k!(r, x, y) = (r[1] = x + y; r[2] = x * y)
41+
k = Core.to_out_of_place(k!, 2)
42+
result = k(3.0, 4.0)
43+
Test.@test result[1] 7.0
44+
Test.@test result[2] 12.0
45+
end
46+
47+
Test.@testset "to_out_of_place - custom type" begin
48+
m!(r, x) = (r[1] = x + 1; r[2] = x + 2)
49+
m = Core.to_out_of_place(m!, 2; T=Int)
50+
result = m(5)
51+
Test.@test result isa Vector{Int}
52+
Test.@test result[1] == 6
53+
Test.@test result[2] == 7
54+
end
55+
56+
Test.@testset "to_out_of_place - nothing input" begin
57+
Test.@test Core.to_out_of_place(nothing, 2) === nothing
58+
end
59+
60+
Test.@testset "to_out_of_place - larger output" begin
61+
big!(r, x) = (for i in 1:5; r[i] = x * i; end)
62+
big = Core.to_out_of_place(big!, 5)
63+
result = big(2.0)
64+
Test.@test length(result) == 5
65+
Test.@test result == [2.0, 4.0, 6.0, 8.0, 10.0]
66+
end
67+
68+
end
69+
return nothing
70+
end
71+
72+
end # module TestCoreFunctionUtils
73+
74+
test_function_utils() = TestCoreFunctionUtils.test_function_utils()

test/suite/core/test_macros.jl

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
module TestCoreMacros
2+
3+
import Test
4+
import CTBase.Core
5+
import CTBase.Exceptions
6+
7+
const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true
8+
const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true
9+
10+
function test_macros()
11+
Test.@testset verbose = VERBOSE showtiming = SHOWTIMING "@ensure" begin
12+
13+
Test.@testset "@ensure - condition true" begin
14+
x = 5
15+
Test.@test_nowarn Core.@ensure x > 0 Exceptions.IncorrectArgument("x must be positive")
16+
Test.@test_nowarn Core.@ensure x == 5 Exceptions.IncorrectArgument("x must be 5")
17+
end
18+
19+
Test.@testset "@ensure - condition false" begin
20+
x = -5
21+
Test.@test_throws Exceptions.IncorrectArgument Core.@ensure x > 0 Exceptions.IncorrectArgument("x must be positive")
22+
y = 10
23+
Test.@test_throws Exceptions.IncorrectArgument Core.@ensure y < 0 Exceptions.IncorrectArgument("y must be negative")
24+
end
25+
26+
Test.@testset "@ensure - with different exception types" begin
27+
x = 0
28+
Test.@test_throws ArgumentError Core.@ensure x != 0 ArgumentError("x cannot be zero")
29+
Test.@test_throws DomainError Core.@ensure x > 0 DomainError(x, "x must be positive")
30+
end
31+
32+
Test.@testset "@ensure - complex conditions" begin
33+
x = 5
34+
y = 10
35+
Test.@test_nowarn Core.@ensure x < y Exceptions.IncorrectArgument("x must be less than y")
36+
Test.@test_throws Exceptions.IncorrectArgument Core.@ensure x > y Exceptions.IncorrectArgument("x must be greater than y")
37+
Test.@test_nowarn Core.@ensure (x > 0 && y > 0) Exceptions.IncorrectArgument("both must be positive")
38+
Test.@test_throws Exceptions.IncorrectArgument Core.@ensure (x < 0 || y < 0) Exceptions.IncorrectArgument("at least one must be negative")
39+
end
40+
41+
Test.@testset "@ensure - exception message verification" begin
42+
x = -5
43+
try
44+
Core.@ensure x > 0 Exceptions.IncorrectArgument("x must be positive")
45+
Test.@test false
46+
catch e
47+
Test.@test e isa Exceptions.IncorrectArgument
48+
Test.@test e.msg == "x must be positive"
49+
end
50+
end
51+
52+
Test.@testset "@ensure - with type checks" begin
53+
x = 5
54+
Test.@test_nowarn Core.@ensure x isa Int Exceptions.IncorrectArgument("x must be an Int")
55+
Test.@test_throws Exceptions.IncorrectArgument Core.@ensure x isa String Exceptions.IncorrectArgument("x must be a String")
56+
end
57+
58+
end
59+
return nothing
60+
end
61+
62+
end # module TestCoreMacros
63+
64+
test_macros() = TestCoreMacros.test_macros()

0 commit comments

Comments
 (0)