Repository: JuliaDynamics/ComplexityMeasures.jl (julia)

Issue:
Deprecate passing `m` as keyword; use type parameter in cases where embeddings occur
At the moment things like `OrdinalPatterns` accept `m` as a keyword. This "parameter" `m` is then cast into a type parameter. However, because `m` corresponds to the length of a static vector, it is better to provide it explicitly as a type paraemter, rather than deciding the type by the value of `m`.

So, this means, change
```julia
OrdinalPattern(; m, kwargs...)
```
to
```julia
OrdinalPattern{m}(; kwargs...)
```
this ensures that the type `m` of `OrdinalPattern` can be statically deduced and propagated without a type instability.

The method with `m` as keyword needs to be deprecated and throw a deprecation warning.

This change will increase performance and compilation speed.

Pull request:
Closes #309

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5294896d4..5f01b5f05 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,6 +35,10 @@ Further additions to the library in v3:
 - `SymbolicAmplitudeAwarePermutation` is now `AmplitudeAwareOrdinalPatterns`.
 - `SpatialSymbolicPermutation` is now `SpatialOrdinalPatterns`.
 
+### Deprecations
+
+- Passing `m` as a positional or keyword argument to ordinal pattern outcome space or encoding is deprecated. It is given as a type parameter now, e.g., `OrdinalPatterns{m}(...)` instead of `OrdinalPatterns(m = ..., ...)`.
+
 ## 2.7.1
 
 - Fix bug in calculation of statistical complexity
diff --git a/src/deprecations.jl b/src/deprecations.jl
index a9bd911b1..2016e414f 100644
--- a/src/deprecations.jl
+++ b/src/deprecations.jl
@@ -76,3 +76,22 @@ end
 @deprecate SymbolicPermutation OrdinalPatterns
 @deprecate SymbolicWeightedPermutation WeightedOrdinalPatterns
 @deprecate SymbolicAmplitudeAwarePermutation AmplitudeAwareOrdinalPatterns
+
+function OrdinalPatternEncoding(m::Int, lt::F = isless_rand) where {F}
+    @warn "Passing `m` as an argument to `OrdinalPattern...(m = ...)` is deprecated. "*
+    "Pass it as a type parameter instead: `OrdinalPattern...{m}`."
+    return OrdinalPatternEncoding{m, F}(zero(MVector{m, Int}), lt)
+end
+# Initializations
+function OrdinalPatterns(; τ::Int = 1, m::Int = 3, lt::F=isless_rand) where {F}
+    m >= 2 || throw(ArgumentError("Need order m ≥ 2."))
+    return OrdinalPatterns{m, F}(OrdinalPatternEncoding{m}(lt), τ)
+end
+function WeightedOrdinalPatterns(; τ::Int = 1, m::Int = 3, lt::F=isless_rand) where {F}
+    m >= 2 || throw(ArgumentError("Need order m ≥ 2."))
+    return WeightedOrdinalPatterns{m, F}(OrdinalPatternEncoding{m}(lt), τ)
+end
+function AmplitudeAwareOrdinalPatterns(; A = 0.5, τ::Int = 1, m::Int = 3, lt::F=isless_rand) where {F}
+    m >= 2 || throw(ArgumentError("Need order m ≥ 2."))
+    return AmplitudeAwareOrdinalPatterns{m, F}(OrdinalPatternEncoding{m}(lt), τ, A)
+end
\ No newline at end of file
diff --git a/src/encoding_implementations/ordinal_pattern.jl b/src/encoding_implementations/ordinal_pattern.jl
index c1e5ee32d..a2328427b 100644
--- a/src/encoding_implementations/ordinal_pattern.jl
+++ b/src/encoding_implementations/ordinal_pattern.jl
@@ -5,7 +5,7 @@ export OrdinalPatternEncoding
 
 """
     OrdinalPatternEncoding <: Encoding
-    OrdinalPatternEncoding(m::Int, lt = ComplexityMeasures.isless_rand)
+    OrdinalPatternEncoding{m}(lt = ComplexityMeasures.isless_rand)
 
 An encoding scheme that [`encode`](@ref)s length-`m` vectors into
 their permutation/ordinal patterns and then into the integers based on the Lehmer
@@ -49,12 +49,10 @@ struct OrdinalPatternEncoding{M, F} <: Encoding
     perm::MVector{M, Int}
     lt::F
 end
-function OrdinalPatternEncoding{m}(lt::F) where {m,F}
+function OrdinalPatternEncoding{m}(lt::F = isless_rand) where {m,F}
     OrdinalPatternEncoding{m, F}(zero(MVector{m, Int}), lt)
 end
-function OrdinalPatternEncoding(m = 3, lt::F = isless_rand) where {F}
-    return OrdinalPatternEncoding{m, F}(zero(MVector{m, Int}), lt)
-end
+OrdinalPatternEncoding() = OrdinalPatternEncoding{3}(isless_rand)
 
 # So that SymbolicPerm stuff fallback here
 total_outcomes(::OrdinalPatternEncoding{m}) where {m} = factorial(m)
diff --git a/src/outcome_spaces/symbolic_permutation.jl b/src/outcome_spaces/ordinal_patterns.jl
similarity index 85%
rename from src/outcome_spaces/symbolic_permutation.jl
rename to src/outcome_spaces/ordinal_patterns.jl
index c0a92be28..eab04af4e 100644
--- a/src/outcome_spaces/symbolic_permutation.jl
+++ b/src/outcome_spaces/ordinal_patterns.jl
@@ -12,18 +12,19 @@ Subtypes must implement fields:
 - `lt::Function`: A function determining how ties are to be broken when constructing
     permutation patterns from embedding vectors.
 """
-abstract type PermutationOutcomeSpace{m} <: CountBasedOutcomeSpace end
-const PermProbEst = PermutationOutcomeSpace
+abstract type OrdinalOutcomeSpace{m} <: CountBasedOutcomeSpace end
 
 ###########################################################################################
 # Types and docstrings
 ###########################################################################################
 """
     OrdinalPatterns <: OutcomeSpace
-    OrdinalPatterns(; m = 3, τ = 1, lt::Function = ComplexityMeasures.isless_rand)
+    OrdinalPatterns{m}(τ = 1, lt::Function = ComplexityMeasures.isless_rand)
 
-An [`OutcomeSpace`](@ref) based on ordinal permutation patterns, originally introduced in
-[BandtPompe2002](@citet)'s paper on permutation entropy.
+An [`OutcomeSpace`](@ref) based on lengh-`m` ordinal permutation patterns, originally
+introduced in [BandtPompe2002](@citet)'s paper on permutation entropy.
+Note that `m` is given as a type parameter, so that when it is a literal integer
+there are performance accelerations.
 
 When passed to [`probabilities`](@ref) the output depends on the input data type:
 
@@ -83,17 +84,13 @@ For example
 ```julia
 using ComplexityMeasures
 m, N = 2, 100
-est = OrdinalPatterns(; m, τ)
+est = OrdinalPatterns{m}(τ)
 x = StateSpaceSet(rand(N, m)) # some input dataset
 πs_ts = zeros(Int, N) # length must match length of `x`
 p = probabilities!(πs_ts, est, x)
 ```
-
-## Implements
-
-- [`symbolize`](@ref). Used for encoding time series.
 """
-struct OrdinalPatterns{M,F} <: PermutationOutcomeSpace{M}
+struct OrdinalPatterns{M,F} <: OrdinalOutcomeSpace{M}
     encoding::OrdinalPatternEncoding{M,F}
     τ::Int
 end
@@ -120,11 +117,11 @@ end
 
 """
     WeightedOrdinalPatterns <: OutcomeSpace
-    WeightedOrdinalPatterns(; τ = 1, m = 3, lt::Function = ComplexityMeasures.isless_rand)
+    WeightedOrdinalPatterns{m}(τ = 1, lt::Function = ComplexityMeasures.isless_rand)
 
 A variant of [`OrdinalPatterns`](@ref) that also incorporates amplitude information,
 based on the weighted permutation entropy [Fadlallah2013](@cite). The outcome space and
-keywords are the same as in [`OrdinalPatterns`](@ref).
+arguments are the same as in [`OrdinalPatterns`](@ref).
 
 ## Description
 
@@ -147,7 +144,7 @@ of the same pattern.
     correctly for each vector of the input dataset (which may be a delay-embedded
     timeseries).
 """
-struct WeightedOrdinalPatterns{M,F} <: PermutationOutcomeSpace{M}
+struct WeightedOrdinalPatterns{M,F} <: OrdinalOutcomeSpace{M}
     encoding::OrdinalPatternEncoding{M,F}
     τ::Int
 end
@@ -156,11 +153,11 @@ is_counting_based(o::WeightedOrdinalPatterns) = false
 
 """
     AmplitudeAwareOrdinalPatterns <: OutcomeSpace
-    AmplitudeAwareOrdinalPatterns(; τ = 1, m = 3, A = 0.5, lt = ComplexityMeasures.isless_rand)
+    AmplitudeAwareOrdinalPatterns{m}(τ = 1, A = 0.5, lt = ComplexityMeasures.isless_rand)
 
 A variant of [`OrdinalPatterns`](@ref) that also incorporates amplitude information,
 based on the amplitude-aware permutation entropy [Azami2016](@cite). The outcome space and
-keywords are the same as in [`OrdinalPatterns`](@ref).
+arguments are the same as in [`OrdinalPatterns`](@ref).
 
 ## Description
 
@@ -178,7 +175,7 @@ elements of
 ``\\mathbf{x}_i`` are weighted. Only mean amplitude of the state vector
 elements are weighted when ``A=1``. With, ``0<A<1``, a combined weighting is used.
 """
-struct AmplitudeAwareOrdinalPatterns{M,F} <: PermutationOutcomeSpace{M}
+struct AmplitudeAwareOrdinalPatterns{M,F} <: OrdinalOutcomeSpace{M}
     encoding::OrdinalPatternEncoding{M,F}
     τ::Int
     A::Float64
@@ -187,33 +184,33 @@ end
 is_counting_based(o::AmplitudeAwareOrdinalPatterns) = false
 
 # Initializations
-function OrdinalPatterns(; τ::Int = 1, m::Int = 3, lt::F=isless_rand) where {F}
+function OrdinalPatterns{m}(τ::Int = 1, lt::F=isless_rand) where {m, F}
     m >= 2 || throw(ArgumentError("Need order m ≥ 2."))
     return OrdinalPatterns{m, F}(OrdinalPatternEncoding{m}(lt), τ)
 end
-function WeightedOrdinalPatterns(; τ::Int = 1, m::Int = 3, lt::F=isless_rand) where {F}
+function WeightedOrdinalPatterns{m}(τ::Int = 1, lt::F=isless_rand) where {m, F}
     m >= 2 || throw(ArgumentError("Need order m ≥ 2."))
     return WeightedOrdinalPatterns{m, F}(OrdinalPatternEncoding{m}(lt), τ)
 end
-function AmplitudeAwareOrdinalPatterns(; A = 0.5, τ::Int = 1, m::Int = 3, lt::F=isless_rand) where {F}
+function AmplitudeAwareOrdinalPatterns{m}(τ::Int = 1, A = 0.5, lt::F=isless_rand) where {m, F}
     m >= 2 || throw(ArgumentError("Need order m ≥ 2."))
     return AmplitudeAwareOrdinalPatterns{m, F}(OrdinalPatternEncoding{m}(lt), τ, A)
 end
 
 ###########################################################################################
-# Implementation of the whole `probabilities` API on abstract `PermProbEst`
+# Implementation of the whole `probabilities` API on abstract `OrdinalOutcomeSpace`
 ###########################################################################################
 # Probabilities etc. simply initialize the datasets and containers of the encodings
 # and just map everythihng using `encode`. The only difference between the three
 # types is whether they compute some additional weights that are affecting
 # how the probabilities are counted.
 
-function probabilities(est::PermProbEst{m}, x::AbstractVector{T}) where {m, T<:Real}
+function probabilities(est::OrdinalOutcomeSpace{m}, x::AbstractVector{T}) where {m, T<:Real}
     dataset::StateSpaceSet{m,T} = embed(x, m, est.τ)
     return probabilities(est, dataset)
 end
 
-function probabilities(est::PermProbEst{m}, x::AbstractStateSpaceSet{D}) where {m, D}
+function probabilities(est::OrdinalOutcomeSpace{m}, x::AbstractStateSpaceSet{D}) where {m, D}
     m != D && throw(ArgumentError(
         "Order of ordinal patterns and dimension of `StateSpaceSet` must match!"
     ))
@@ -223,7 +220,7 @@ function probabilities(est::PermProbEst{m}, x::AbstractStateSpaceSet{D}) where {
     return probs
 end
 
-function probabilities!(::Vector{Int}, ::PermProbEst, ::AbstractVector)
+function probabilities!(::Vector{Int}, ::OrdinalOutcomeSpace, ::AbstractVector)
     error("""
     In-place `probabilities!` for `OrdinalPatterns` can only be used by
     StateSpaceSet input, not timeseries. First embed the timeseries or use the
@@ -232,7 +229,7 @@ function probabilities!(::Vector{Int}, ::PermProbEst, ::AbstractVector)
 end
 
 
-function fasthist!(πs::Vector{Int}, est::PermProbEst{m}, x::AbstractStateSpaceSet{m}) where {m}
+function fasthist!(πs::Vector{Int}, est::OrdinalOutcomeSpace{m}, x::AbstractStateSpaceSet{m}) where {m}
     # TODO: The following loop can probably be parallelized!
     @inbounds for (i, χ) in enumerate(x)
         πs[i] = encode(est.encoding, χ)
@@ -244,7 +241,7 @@ function fasthist!(πs::Vector{Int}, est::PermProbEst{m}, x::AbstractStateSpaceS
     return cts
 end
 
-function codify(est::PermProbEst{m}, x) where m
+function codify(est::OrdinalOutcomeSpace{m}, x) where m
     if x isa AbstractVector
         dataset = embed(x, m, est.τ)
     else
@@ -270,7 +267,7 @@ function probabilities!(πs::Vector{Int}, est::OrdinalPatterns{m}, x::AbstractSt
 end
 
 # The scaled versions
-function probabilities!(πs::Vector{Int}, est::PermProbEst{m}, x::AbstractStateSpaceSet{m}) where {m}
+function probabilities!(πs::Vector{Int}, est::OrdinalOutcomeSpace{m}, x::AbstractStateSpaceSet{m}) where {m}
     # This sorts πs in-place. For `WeightedOrdinalPatterns` and
     # `AmplitudeAwareOrdinalPatterns`, this returned normalized counts (i.e. numbers on
     # [0, 1]).
@@ -283,7 +280,7 @@ end
 # `OrdinalPatterns` is counting-compatible (i.e. returns a true histogram `Vector{Int}`),
 # while `WeightedOrdinalPatterns` and `AmplitudeAwareOrdinalPatterns` returns
 # a normalized histogram, and are thus *not* counting compatible.
-function weighted_counts_and_outcomes(est::PermProbEst{m}, x::Vector_or_SSSet) where {m}
+function weighted_counts_and_outcomes(est::OrdinalOutcomeSpace{m}, x::Vector_or_SSSet) where {m}
     # A bit of code duplication here, because we actually need the processed
     # `πs` to invert it with `decode`. This can surely be optimized with some additional
     # function that both maps to integers with `decode` but also keeps track of
@@ -304,18 +301,18 @@ function weighted_counts_and_outcomes(est::PermProbEst{m}, x::Vector_or_SSSet) w
     return freqs, outcomes
 end
 
-function probabilities_and_outcomes(est::PermProbEst{m}, x::Vector_or_SSSet) where {m}
+function probabilities_and_outcomes(est::OrdinalOutcomeSpace{m}, x::Vector_or_SSSet) where {m}
     freqs, outcomes = weighted_counts_and_outcomes(est, x)
     return Probabilities(freqs), outcomes
 end
 
 # fallback
-total_outcomes(est::PermutationOutcomeSpace) = total_outcomes(est.encoding)
-outcome_space(est::PermutationOutcomeSpace) = outcome_space(est.encoding)
+total_outcomes(est::OrdinalOutcomeSpace) = total_outcomes(est.encoding)
+outcome_space(est::OrdinalOutcomeSpace) = outcome_space(est.encoding)
 
 # If `x` is state space set, delay embedding is ignored and all elements
 # are mapped to outcomes. Otherwise, delay embedding is done.
-function encoded_space_cardinality(o::PermProbEst{m}, x::AbstractArray) where {m}
+function encoded_space_cardinality(o::OrdinalOutcomeSpace{m}, x::AbstractArray) where {m}
     N = length(x)
     return N - (m - 1)*o.τ
 end
diff --git a/src/outcome_spaces/outcome_spaces.jl b/src/outcome_spaces/outcome_spaces.jl
index c47663dd7..080be6310 100644
--- a/src/outcome_spaces/outcome_spaces.jl
+++ b/src/outcome_spaces/outcome_spaces.jl
@@ -1,6 +1,6 @@
 include("unique_elements.jl")
 include("value_binning.jl")
-include("symbolic_permutation.jl")
+include("ordinal_patterns.jl")
 include("kernel_density.jl")
 include("power_spectrum.jl")
 include("wavelet_overlap.jl")


Test patch:
diff --git a/test/deprecations.jl b/test/deprecations.jl
index 319e2529e..17c42e331 100644
--- a/test/deprecations.jl
+++ b/test/deprecations.jl
@@ -1,27 +1,28 @@
 using ComplexityMeasures, Test
 
-x = randn(1000)
+@testset "2.0 deprecations" begin
+    x = randn(1000)
 
-# Convenience
-@test permentropy(x) == entropy_permutation(x; base=MathConstants.e)
-msg ="`permentropy(x; τ, m, base)` is deprecated.\nUse instead: `entropy_permutation(x; τ, m, base)`, or even better, use the\ndirect syntax discussed in the docstring of `entropy_permutation`.\n"
-@test_logs (:warn, msg) permentropy(x)
+    # Convenience
+    @test permentropy(x) == entropy_permutation(x; base=MathConstants.e)
+    msg ="`permentropy(x; τ, m, base)` is deprecated.\nUse instead: `entropy_permutation(x; τ, m, base)`, or even better, use the\ndirect syntax discussed in the docstring of `entropy_permutation`.\n"
+    @test_logs (:warn, msg) permentropy(x)
 
-# Generalized entropies
-@test genentropy(x, 0.1) == information(Shannon(MathConstants.e), ValueBinning(0.1), x)
-msg = "`genentropy(probs::Probabilities; q, base)` deprecated.\nUse instead: `information(Renyi(q, base), probs)`.\n"
-@test_logs (:warn, msg) genentropy(Probabilities(rand(3)))
+    # Generalized entropies
+    @test genentropy(x, 0.1) == information(Shannon(MathConstants.e), ValueBinning(0.1), x)
+    msg = "`genentropy(probs::Probabilities; q, base)` deprecated.\nUse instead: `information(Renyi(q, base), probs)`.\n"
+    @test_logs (:warn, msg) genentropy(Probabilities(rand(3)))
 
-msg = "`genentropy(x::Array_or_SSSet, est::ProbabilitiesEstimator; q, base)` is deprecated.\nUse instead: `information(Renyi(q, base), est, x)`.\n"
-@test_logs (:warn, msg) genentropy(x, ValueBinning(0.1))
+    msg = "`genentropy(x::Array_or_SSSet, est::ProbabilitiesEstimator; q, base)` is deprecated.\nUse instead: `information(Renyi(q, base), est, x)`.\n"
+    @test_logs (:warn, msg) genentropy(x, ValueBinning(0.1))
 
-@test probabilities(x, 0.1) == probabilities(ValueBinning(0.1), x)
-msg = "`probabilities(x, est::OutcomeSpace)`\nis deprecated, use `probabilities(est::OutcomeSpace, x) instead`.\n"
-@test_logs (:warn, msg) probabilities(x, ValueBinning(0.1))
-
-x = StateSpaceSet(rand(100, 3))
-@test genentropy(x, 4) == information(Shannon(MathConstants.e), ValueBinning(4), x)
+    @test probabilities(x, 0.1) == probabilities(ValueBinning(0.1), x)
+    msg = "`probabilities(x, est::OutcomeSpace)`\nis deprecated, use `probabilities(est::OutcomeSpace, x) instead`.\n"
+    @test_logs (:warn, msg) probabilities(x, ValueBinning(0.1))
 
+    x = StateSpaceSet(rand(100, 3))
+    @test genentropy(x, 4) == information(Shannon(MathConstants.e), ValueBinning(4), x)
+end
 
 @testset "deprecations: binning" begin
     @test FixedRectangularBinning((0, 1), (0, 1), 2) isa FixedRectangularBinning
@@ -31,7 +32,7 @@ end
 
 
 @testset "2.9 deprecations" begin
-    # For
+    x = StateSpaceSet(rand(100, 3))
 
     @test entropy_maximum(Shannon(MathConstants.e), ValueBinning(4), x) ==
         information_maximum(Shannon(MathConstants.e), ValueBinning(4), x)
@@ -42,4 +43,11 @@ end
     @test SymbolicPermutation() isa OrdinalPatterns
     @test SymbolicWeightedPermutation() isa WeightedOrdinalPatterns
     @test SymbolicAmplitudeAwarePermutation() isa AmplitudeAwareOrdinalPatterns
+
+    for f in (OrdinalPatterns, WeightedOrdinalPatterns, AmplitudeAwareOrdinalPatterns)
+        a = f(; m = 3, τ = 2)
+        b = f{3}(2)
+        @test entropy(a, x) == entropy(b, x)
+    end
+
 end