diff --git a/docs/make.jl b/docs/make.jl index 5a6ec58..d785c95 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -13,6 +13,12 @@ makedocs(; ), pages=[ "Home" => "index.md", + "Layers" => [ + "Transformation" => "transformation.md", + "Arithmetic" => "arithmetic.md", + "Aggregation" => "aggregation.md", + "Comparison" => "comparison.md", + ], ], ) diff --git a/docs/src/aggregation.md b/docs/src/aggregation.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/src/arithmetic.md b/docs/src/arithmetic.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/src/comparison.md b/docs/src/comparison.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/src/transformation.md b/docs/src/transformation.md new file mode 100644 index 0000000..e69de29 diff --git a/src/CompositionalNetworks.jl b/src/CompositionalNetworks.jl index f38e988..89f8f1f 100644 --- a/src/CompositionalNetworks.jl +++ b/src/CompositionalNetworks.jl @@ -1,5 +1,15 @@ module CompositionalNetworks -# Write your package code here. +# Exports utilities +export lazy, lazy_param + +# Include utils +include("utils.jl") + +# Includes layers +include("transformation.jl") +include("arithmetic.jl") +include("aggregation.jl") +include("comparison.jl") end diff --git a/src/aggregation.jl b/src/aggregation.jl new file mode 100644 index 0000000..4b76070 --- /dev/null +++ b/src/aggregation.jl @@ -0,0 +1,11 @@ +""" + _sum(x::V) +Aggregate through `+` a vector into a single scalar. +""" +_sum(x::V) where {T <: Number, V <: AbstractVector{T}} = reduce(+, x) + +""" + _count_positive(x::V) +Count the number of strictly positive elements of `x`. +""" +_count_positive(x::V) where {T <: Number, V <: AbstractVector{T}} = count(y -> y > 0.0, x) \ No newline at end of file diff --git a/src/arithmetic.jl b/src/arithmetic.jl new file mode 100644 index 0000000..6d6ffc6 --- /dev/null +++ b/src/arithmetic.jl @@ -0,0 +1,11 @@ +""" + _sum(x::W) where {T <: Number, V <: AbstractVector{T}, W <: AbstractVector{V}} + _prod(x::W) where {T <: Number, V <: AbstractVector{T}, W <: AbstractVector{V}} +Reduce `k = length(x)` vectors through sum/product to a single vector. +""" +function _sum(x::W) where {T <: Number, V <: AbstractVector{T}, W <: AbstractVector{V}} + return reduce((y, z) -> y .+ z, x) +end +function _prod(x::W) where {T <: Number, V <: AbstractVector{T}, W <: AbstractVector{V}} + return reduce((y, z) -> y .* z, x) +end diff --git a/src/comparison.jl b/src/comparison.jl new file mode 100644 index 0000000..5f38c80 --- /dev/null +++ b/src/comparison.jl @@ -0,0 +1,48 @@ +# doc in transformation.jl +_identity(x::T) where T <: Number = identity(x) + +""" + _abs_diff_val_param(x::T, param::T) +Return the absolute difference between `x` and `param`. +""" +_abs_diff_val_param(x::T, param::T) where T <: Number = abs(x - param) + +""" + _val_minus_param(x::T, param::T) + _param_minus_val(x::T, param::T) +Return the difference `x - param` (resp. `param - x`) if positive, `0.0` otherwise. +""" +_val_minus_param(x::T, param::T) where T <: Number = max(0.0, x - param) +_param_minus_val(x::T, param::T) where T <: Number = max(0.0, param - x) + +""" + _euclidian_param(x::T, param::T, dom_size::T2) + _euclidian(x::T, dom_size::T2) +Compute an euclidian norm , possibly weigthed by `param`, on a scalar. +""" +function _euclidian_param(x::T, param::T, dom_size::T2) where {T <: Number, T2 <: Number} + return x == param ? 0.0 : (1.0 + abs(x - param) \ dom_size) +end +function _euclidian(x::T, dom_size::T2) where {T <: Number, T2 <: Number} + return _euclidian_param(x, zero(T), dom_size) +end + +""" + _abs_diff_val_vars(x::T, vars::V) +Return the absolute difference between `x` and the number of variables. +""" +function _abs_diff_val_vars(x::T, vars::V) where {T <: Number, V <: AbstractVector{T}} + return abs(x - length(vars)) +end + +""" + _val_minus_vars(x::T, vars::V) + _vars_minus_val(x::T, vars::V) +Return the difference `x - length(vars)` (resp. `length(vars) - x`) if positive, `0.0` otherwise. +""" +function _val_minus_vars(x::T, vars::V) where {T <: Number, V <: AbstractVector{T}} + return _val_minus_param(x, length(vars)) +end +function _vars_minus_val(x::T, vars::V) where {T <: Number, V <: AbstractVector{T}} + return _param_minus_val(x, length(vars)) +end diff --git a/src/transformation.jl b/src/transformation.jl new file mode 100644 index 0000000..e6050ba --- /dev/null +++ b/src/transformation.jl @@ -0,0 +1,115 @@ +""" + _identity(x::V) where {T <: Number,V <: AbstractVector{T}} + _identity(x::T) where T <: Number = identity(x) +Identity function. Already defined in Julia as `identity`, specialized for vectors and scalars. +""" +_identity(x::V) where {T <: Number,V <: AbstractVector{T}} = identity(x) + +""" + _count_eq(i::Int, x::V) + _count_eq_right(i::Int, x::V) + _count_eq_left(i::Int, x::V) +Count the number of elements equal to `x[i]` (optionally to the right/left of `x[i]`). Extended method to vector `x::V` are generated. +""" +function _count_eq(i::Int, x::V) where {T <: Number,V <: AbstractVector{T}} + return count(y -> x[i] == y, x) - 1 +end +function _count_eq_right(i::Int, x::V) where {T <: Number,V <: AbstractVector{T}} + return _count_eq(1, @view x[i:end]) +end +function _count_eq_left(i::Int, x::V) where {T <: Number,V <: AbstractVector{T}} + return _count_eq(i, @view x[1:i]) +end +# Generating vetorized versions +lazy(_count_eq, _count_eq_left, _count_eq_right) + +""" + _count_greater(i::Int, x::V) + _count_lesser(i::Int, x::V) + _count_g_left(i::Int, x::V) + _count_l_left(i::Int, x::V) + _count_g_right(i::Int, x::V) + _count_l_right(i::Int, x::V) +Count the number of elements greater/lesser than `x[i]` (optionally to the left/right of `x[i]`). Extended method to vector with sig `(x::V)` are generated. +""" +function _count_greater(i::Int, x::V) where {T <: Number,V <: AbstractVector{T}} + return count(y -> x[i] < y, x) +end +function _count_lesser(i::Int, x::V) where {T <: Number,V <: AbstractVector{T}} + return count(y -> x[i] > y, x) +end +function _count_g_left(i::Int, x::V) where {T <: Number,V <: AbstractVector{T}} + return _count_greater(i, @view x[1:i]) +end +function _count_l_left(i::Int, x::V) where {T <: Number,V <: AbstractVector{T}} + return _count_lesser(i, @view x[1:i]) +end +function _count_g_right(i::Int, x::V) where {T <: Number,V <: AbstractVector{T}} + return _count_greater(1, @view x[i:end]) +end +function _count_l_right(i::Int, x::V) where {T <: Number,V <: AbstractVector{T}} + return _count_lesser(1, @view x[i:end]) +end +# Generating vetorized versions +lazy(_count_greater, _count_g_left, _count_g_right) +lazy(_count_lesser, _count_l_left, _count_l_right) + +""" + _count_eq_param(i::Int, x::V, param::T) + _count_l_param(i::Int, x::V, param::T) + _count_g_param(i::Int, x::V, param::T) +Count the number of elements equal to (resp. lesser/greater than) `x[i] + param`. Extended method to vector with sig `(x::V, param::T)` are generated. +""" +function _count_eq_param(i::Int, x::V, param::T) where {T <: Number,V <: AbstractVector{T}} + return count(y -> y == x[i] + param, x) +end +function _count_l_param(i::Int, x::V, param::T) where {T <: Number,V <: AbstractVector{T}} + return count(y -> y < x[i] + param, x) +end +function _count_g_param(i::Int, x::V, param::T) where {T <: Number,V <: AbstractVector{T}} + return count(y -> y > x[i] + param, x) +end +# Generating vetorized versions +lazy_param(_count_eq_param, _count_l_param, _count_g_param) + +""" + _count_bounding_param(i::Int, x::V, param::T) +Count the number of elements bounded (not strictly) by `x[i]` and `x[i] + param`. An extended method to vector with sig `(x::V, param::T)` is generated. +""" +function _count_bounding_param(i::Int, x::V, param::T +) where {T <: Number,V <: AbstractVector{T}} + return count(y -> x[i] ≤ y ≤ x[i] + param, x) +end +# Generating vetorized versions +lazy_param(_count_bounding_param) + +""" + _val_minus_param(i::Int, x::V, param::T) + _param_minus_val(i::Int, x::V, param::T) +Return the difference `x[i] - param` (resp. `param - x[i]`) if positive, `0.0` otherwise. Extended method to vector with sig `(x::V, param::T)` are generated. +""" +function _val_minus_param(i::Int, x::V, param::T +) where {T <: Number,V <: AbstractVector{T}} + return max(0, x[i] - param) +end +function _param_minus_val(i::Int, x::V, param::T +) where {T <: Number,V <: AbstractVector{T}} + return max(0, param - x[i]) +end +# Generating vetorized versions +lazy_param(_val_minus_param, _param_minus_val) + +""" + _contiguous_vals_minus(i::Int, x::V) + _contiguous_vals_minus_rev(i::Int, x::V) +Return the difference `x[i] - x[i + 1]` (resp. `x[i + 1] - x[i]`) if positive, `0.0` otherwise. Extended method to vector with sig `(x::V)` are generated. +""" +function _contiguous_vals_minus(i::Int, x::V) where {T <: Number,V <: AbstractVector{T}} + return length(x) == i ? 0 : _val_minus_param(i, x, x[i + 1]) +end +function _contiguous_vals_minus_rev(i::Int, x::V +) where {T <: Number,V <: AbstractVector{T}} + return length(x) == i ? 0 : _param_minus_val(i, x, x[i + 1]) +end +# Generating vetorized versions +lazy(_contiguous_vals_minus, _contiguous_vals_minus_rev) diff --git a/src/utils.jl b/src/utils.jl new file mode 100644 index 0000000..0afbcac --- /dev/null +++ b/src/utils.jl @@ -0,0 +1,20 @@ + +""" + _map_tr(f, x) + _map_tr(f, x, param) +Return an anonymous function that applies `f` to all elements of `x`, with an optional parameter `param`. +""" +_map_tr(f, x) = ((g, y) -> map(i -> g(i, y), 1:length(y)))(f, x) +_map_tr(f, x, param) = ((g, y, p) -> map(i -> g(i, y, p), 1:length(y)))(f, x, param) + +""" + lazy(funcs::Function...) + lazy_param(funcs::Function...) +Generate methods extended to a vector instead of one of its components. For `lazy` (resp. `lazy_param`) a function `f` should have the following signature: `f(i::Int, x::V)` (resp. `f(i::Int, x::V, param::T)`). +""" +function lazy(funcs::Function...) + foreach(f -> eval(:($f(x) = (y -> _map_tr($f, y))(x))), map(Symbol, funcs)) +end +function lazy_param(funcs::Function...) + foreach(f -> eval(:($f(x, param) = (y -> _map_tr($f, y, param))(x))), map(Symbol, funcs)) +end diff --git a/test/layers.jl b/test/layers.jl new file mode 100644 index 0000000..72bd50a --- /dev/null +++ b/test/layers.jl @@ -0,0 +1,154 @@ +# Transformation layer +data = [ + [1, 5, 2, 4, 3] => 2, + [1, 2, 3, 2, 1] => 2, +] + +# Test transformations without parameters +funcs = Dict( + ICN._identity => [ + data[1].first, + data[2].first, + ], + ICN._count_eq => [ + [0, 0, 0, 0, 0], + [1, 1, 0, 1, 1], + ], + ICN._count_eq_right => [ + [0, 0, 0, 0, 0], + [1, 1, 0, 0, 0], + ], + ICN._count_eq_left => [ + [0, 0, 0, 0, 0], + [0, 0, 0, 1, 1], + ], + ICN._count_greater => [ + [4, 0, 3, 1, 2], + [3, 1, 0, 1, 3], + ], + ICN._count_lesser => [ + [0, 4, 1, 3, 2], + [0, 2, 4, 2, 0], + ], + ICN._count_g_left => [ + [0, 0, 1, 1, 2], + [0, 0, 0, 1, 3], + ], + ICN._count_l_left => [ + [0, 1, 1, 2, 2], + [0, 1, 2, 1, 0], + ], + ICN._count_g_right => [ + [4, 0, 2, 0, 0], + [3, 1, 0, 0, 0], + ], + ICN._count_l_right => [ + [0, 3, 0, 1, 0], + [0, 1, 2, 1, 0], + ], + ICN._contiguous_vals_minus => [ + [0, 3, 0, 1, 0], + [0, 0, 1, 1, 0], + ], + ICN._contiguous_vals_minus_rev => [ + [4, 0, 2, 0, 0], + [1, 1, 0, 0, 0], + ], +) + +for (f, results) in funcs, (key, vals) in enumerate(data) + @test f(vals.first) == results[key] +end + +# Test transformations with parameter +funcs_param = Dict( + ICN._count_eq_param => [ + [1, 0, 1, 0, 1], + [1, 0, 0, 0, 1], + ], + ICN._count_l_param => [ + [2, 5, 3, 5, 4], + [4, 5, 5, 5, 4], + ], + ICN._count_g_param => [ + [2, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + ], + ICN._count_bounding_param => [ + [3, 1, 3, 2, 3], + [5, 3, 1, 3, 5], + ], + ICN._val_minus_param => [ + [0, 3, 0, 2, 1], + [0, 0, 1, 0, 0], + ], + ICN._param_minus_val => [ + [1, 0, 0, 0, 0], + [1, 0, 0, 0, 1], + ], +) + +for (f, results) in funcs_param, (key, vals) in enumerate(data) + @test f(vals.first, vals.second) == results[key] +end + +# arithmetic layer +@test ICN._sum(map(p -> p.first, data)) == [2, 7, 5, 6, 4] +@test ICN._prod(map(p -> p.first, data)) == [1, 10, 6, 8, 3] + +# aggregation layer +@test ICN._sum(data[1].first) == 15 +@test ICN._sum(data[2].first) == 9 + +@test ICN._count_positive(data[1].first) == 5 +@test ICN._count_positive(data[2].first) == 5 +@test ICN._count_positive([1, 0, 1, 0, 1]) == 3 + +# Comparison layer +data = [3 => (1, 5), 5 => (10, 5)] + +funcs = [ + ICN._identity => [3, 5], +] + +# test no param/vars +for (f, results) in funcs, (key, vals) in enumerate(data) + @test f(vals.first) == results[key] +end + +funcs_param = [ + ICN._abs_diff_val_param => [2, 5], + ICN._val_minus_param => [2, 0], + ICN._param_minus_val => [0, 5], +] + +for (f, results) in funcs_param, (key, vals) in enumerate(data) + @test f(vals.first, vals.second[1]) == results[key] +end + +funcs_vars = [ + ICN._abs_diff_val_vars => [2, 0], + ICN._val_minus_vars => [0, 0], + ICN._vars_minus_val => [2, 0], +] + +for (f, results) in funcs_vars, (key, vals) in enumerate(data) + @test f(vals.first, rand(1:10, vals.second[2])) == results[key] +end + + +funcs_param_dom = [ + ICN._euclidian_param => [3.5, 2.0], +] + +for (f, results) in funcs_param_dom, (key, vals) in enumerate(data) + @test f(vals.first, vals.second[1], vals.second[2]) ≈ results[key] +end + +funcs_dom = [ + ICN._euclidian => [8/3, 2.0], +] + +for (f, results) in funcs_dom, (key, vals) in enumerate(data) + @test f(vals.first, vals.second[2]) ≈ results[key] +end diff --git a/test/runtests.jl b/test/runtests.jl index 1d3b8a0..324e9cf 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,8 @@ using CompositionalNetworks using Test +ICN = CompositionalNetworks + @testset "CompositionalNetworks.jl" begin - # Write your tests here. + include("layers.jl") end