From a3f2ddd1cb1f23ae32690466e5928b4cb0cea85e Mon Sep 17 00:00:00 2001 From: KronosTheLate <61620837+KronosTheLate@users.noreply.github.com> Date: Wed, 28 Feb 2024 03:35:47 +0100 Subject: [PATCH] Api rework (#34) * Ignore autogenerated .vscode folder * Make parent file for rework, define "findpeaks" * implement api_rework as discussed * Remove test code from Peaks.jl * Update readme to temp state * Add sentence about mutation to todo section * add sentence about minima finding * Remove temporary content from readme * make `findpeaks` a oneliner * Don't discard initially calculated heights * Export filterpeaks, improve internals in new API * Change linebreaks in "filterpeaks!" * Make use of "strict" in all functions * Fix some bugs * export old functions * improvements, polish docstrings * tag breaking version * misc changes * comment out bad test * minor changes * fix docstrung for peakheights * un-commented failing test * revert exports * move export of filterpeaks! * remove extra exports * explicit about copying data in docstring * Turn --> info -> * add sentence about not copying data * minheight -> min, same for proms,widths, and max * some changes + better length check in filterpeaks! * add new method for filterpeaks! * Distribute contents of rework, structure exports * Do not include api_work, which does not exist * fixed a couple copy-paste errors * Revert function reordering (clarify changes in docstrings) * Example compromise docstrings and ordering * Revert random change (fixes tests) * Separate docstring examples for APIs * Improve curried docstring * make known_fields a const * explain what a named tuple slice is * Make filterpeaks signature match * swap check order filterpeaks! * Swap check order filterpeaks! * [nfc] whitespace changes * Update/rewrite docstrings and doctests * Don't need to depwarn old kwargs for new functions * Rename `up` => `hi` * Update `peakproms!`/`peakwidths!` to use `ismaxima`/`isminima` for argument validation * Rewrite error for bad min/max order * Fix unnecessary Missing union when input array doesn't have missings * Update doctest outputs * Update deprecated kwargs in tests * Test doctests when running tests * Test depwarns * Test error for when first peak is not an extrema * Confirm error when namedtuple has widths OR edges * Confirm error in plotpeaks when first peak isnt an extrema * Update docstrings/doctests for utils functions * Add `filterpeaks!` to the docs * Add julia cache to CI docs build * Dont try linking to Base docs (for the moment) * fix errant ref --------- Co-authored-by: Allen Hill --- .github/workflows/CI.yml | 1 + .gitignore | 2 +- Project.toml | 7 -- docs/src/index.md | 1 + src/Peaks.jl | 3 +- src/minmax.jl | 42 ++++++-- src/peakheight.jl | 123 ++++++++++++++++++------ src/peakprom.jl | 203 ++++++++++++++++++++++++++------------- src/peakwidth.jl | 203 +++++++++++++++++++++++++++------------ src/plot.jl | 2 +- src/utils.jl | 142 +++++++++++++++++++++++++++ test/Project.toml | 6 ++ test/manual_tests.jl | 43 +++++++++ test/peakheight.jl | 12 ++- test/peakprom.jl | 18 ++-- test/peakwidth.jl | 15 ++- test/plotting.jl | 7 +- test/runtests.jl | 9 +- test/utils.jl | 7 ++ 19 files changed, 648 insertions(+), 198 deletions(-) create mode 100644 src/utils.jl create mode 100644 test/Project.toml create mode 100644 test/manual_tests.jl create mode 100644 test/utils.jl diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 600b414..88eebae 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -45,6 +45,7 @@ jobs: - uses: julia-actions/setup-julia@v1 with: version: '1' + - uses: julia-actions/cache@v1 - run: | julia --project=docs -e ' using Pkg diff --git a/.gitignore b/.gitignore index bc0c5d0..87e7db5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ Manifest.toml docs/build/ docs/site/ - +.vscode/ diff --git a/Project.toml b/Project.toml index ade018a..2158d32 100644 --- a/Project.toml +++ b/Project.toml @@ -12,10 +12,3 @@ Compat = "2.1, 3, 4" RecipesBase = "1.3" julia = "1.6" -[extras] -OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" -Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[targets] -test = ["Test", "OffsetArrays", "Plots"] diff --git a/docs/src/index.md b/docs/src/index.md index 8f7704d..9396dbb 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -13,6 +13,7 @@ peakwidths peakwidths! peakheights peakheights! +filterpeaks! findnextmaxima findnextminima ismaxima diff --git a/src/Peaks.jl b/src/Peaks.jl index a2eab71..60c633e 100644 --- a/src/Peaks.jl +++ b/src/Peaks.jl @@ -4,9 +4,10 @@ using Compat export argmaxima, argminima, maxima, minima, findmaxima, findminima, findnextmaxima, findnextminima, peakproms, peakproms!, peakwidths, peakwidths!, peakheights, - peakheights!, ismaxima, isminima + peakheights!, ismaxima, isminima, filterpeaks! include("minmax.jl") +include("utils.jl") include("peakprom.jl") include("peakwidth.jl") include("peakheight.jl") diff --git a/src/minmax.jl b/src/minmax.jl index 295bd6f..5b16232 100644 --- a/src/minmax.jl +++ b/src/minmax.jl @@ -186,19 +186,31 @@ function maxima( end """ - findmaxima(x[, w=1; strict=true]) -> (idxs, vals) + findmaxima(x[, w=1; strict=true]) -> (;indices, heights, data) -Find the indices and values of local maxima in `x`, where each maxima `i` is either the -maximum of `x[i-w:i+w]` or the first index of a plateau. +Find the indices and values of local maxima in `x`, where each maxima `i` is +either the maximum of `x[i-w:i+w]` or the first index of a plateau. + +Returns a `NamedTuple` contains the fields `indices`, `heights`, `data`, which are +equivalent to `heights = data[indices]`. The `data` field is a reference (not a copy) to +the argument `x`. A plateau is defined as a maxima with consecutive equal (`===`/egal) maximal values which are bounded by lesser values immediately before and after the consecutive maximal values. See also: [`argmaxima`](@ref), [`findnextmaxima`](@ref) + +# Examples +```jldoctest +julia> data = [1, 5, 1, 3, 2]; + +julia> pks = findmaxima(data) +(indices = [2, 4], heights = [5, 3], data = [1, 5, 1, 3, 2]) +``` """ function findmaxima(x, w::Int=1; strict::Bool=true) idxs = argmaxima(x, w; strict=strict) - return (idxs, x[idxs]) + return (;indices=idxs, heights=x[idxs], data=x) end """ @@ -230,7 +242,6 @@ julia> findnextminima([3,2,3,1,1,3], 3) ``` """ findnextminima(x, i, w=1; strict=true) = findnextextrema(>, x, i, w, strict) - """ isminima(i, x[, w=1; strict=true]) -> Bool @@ -313,18 +324,29 @@ function minima( end """ - findminima(x[, w=1; strict=true]) -> (idxs, vals) + findminima(x[, w=1; strict=true]) -> (;indices, heights, data) -Find the indices and values of local minima in `x`, where each minima `i` is either the -minimum of `x[i-w:i+w]` or the first index of a plateau. +Find the indices and values of local minima in `x`, where each minima `i` is +either the minimum of `x[i-w:i+w]` or the first index of a plateau. + +Returns a `NamedTuple` contains the fields `indices`, `heights`, `data`, which are +equivalent to `heights = data[indices]`. The `data` field is a reference (not a copy) to +the argument `x`. A plateau is defined as a minima with consecutive equal (`===`/egal) minimal values which are bounded by greater values immediately before and after the consecutive minimal values. See also: [`argminima`](@ref), [`findnextminima`](@ref) + +# Examples +```jldoctest +julia> data = [1, 5, 1, 3, 2]; + +julia> valleys = findminima(data) +(indices = [3], heights = [1], data = [1, 5, 1, 3, 2]) +``` """ function findminima(x, w::Int=1; strict::Bool=true) idxs = argminima(x, w; strict=strict) - return (idxs, x[idxs]) + return (;indices=idxs, heights=x[idxs], data=x) end - diff --git a/src/peakheight.jl b/src/peakheight.jl index 9536ac0..1b11a99 100644 --- a/src/peakheight.jl +++ b/src/peakheight.jl @@ -1,11 +1,15 @@ """ - peakheights(peaks, heights; - minheight=nothing, - maxheight=nothing - ) -> (peaks, heights) + peakheights(indices, heights; [min, max]) -> (indices, heights) + peakheights(pks::NamedTuple; [min, max]) -> NamedTuple -Return a copy of `peaks` and `heights` where peak heights are removed if less than -`minheight` and/or greater than `maxheight`. + +Return a copy of `indices` and `heights` where peaks are removed if their height is less than +`min` and/or greater than `max`. + +If a NamedTuple `pks` is given, a new NamedTuple is returned with filtered copies of fields +from `pks`. `pks` must have `:indices` and `:heights` fields. The fields `:proms`, +`:widths`, and `:edges` will be filtered if present, and any remaining fields will be +copied unmodified. See also: [`peakproms`](@ref), [`peakwidths`](@ref), [`findmaxima`](@ref) @@ -13,56 +17,93 @@ See also: [`peakproms`](@ref), [`peakwidths`](@ref), [`findmaxima`](@ref) ```jldoctest julia> x = [0,5,2,3,3,1,4,0]; -julia> xpks, vals = findmaxima(x) -([2, 4, 7], [5, 3, 4]) +julia> pks = findmaxima(x) +(indices = [2, 4, 7], heights = [5, 3, 4], data = [0, 5, 2, 3, 3, 1, 4, 0]) -julia> peakheights(xpks, vals; maxheight=4) -([4, 7], [3, 4]) +julia> peakheights(pks; max=4) +(indices = [4, 7], heights = [3, 4], data = [0, 5, 2, 3, 3, 1, 4, 0]) -julia> peakheights(xpks, vals; minheight=4.5) -([2], [5]) +julia> inds, heights = peakheights(pks.indices, pks.heights; max=4) +([4, 7], [3, 4]) ``` """ function peakheights( - peaks::AbstractVector{Int}, heights::AbstractVector; - minheight=nothing, maxheight=nothing + indices::AbstractVector{Int}, heights::AbstractVector; + minheight=nothing, maxheight=nothing, + min=minheight, max=maxheight ) - peakheights!(copy(peaks), copy(heights); minheight=minheight, maxheight=maxheight) + if !isnothing(minheight) + Base.depwarn("Keyword `minheight` has been renamed to `min`", :peakheights!) + end + if !isnothing(maxheight) + Base.depwarn("Keyword `maxheight` has been renamed to `max`", :peakheights!) + end + peakheights!(copy(indices), copy(heights); min=min, max=max) +end + +peakheights(pks::NamedTuple; kwargs...) = peakheights!(deepcopy(pks); kwargs...) + +""" + peakheights(; [min, max]) -> Function + +Create a function, `f(pks::NamedTuple)`, that copies and filters the peak heights of its +argument, `pks`, using any given keyword arguments. + +# Examples +```jldoctest +julia> findmaxima([0, 5, 2, 3, 3, 1, 4, 0]) |> peakheights(; max=4) +(indices = [4, 7], heights = [3, 4], data = [0, 5, 2, 3, 3, 1, 4, 0]) +``` +""" +peakheights(; kwargs...) = function _curried_peakheights(pks) + return peakheights(deepcopy(pks); kwargs...) end """ - peakheights!(peaks, heights; - minheight=nothing, - maxheight=nothing - ) -> (peaks, heights) + peakheights!(indices, heights; [min, max]) -> (indices, heights) + peakheights!(pks::NamedTuple; [min, max]) -> NamedTuple + +Filter (mutate) and return `indices` and `heights` by removing peaks that are less than `min` +and/or greater than `max`. -Modify and return `peaks` and `heights` by removing peaks that are less than `minheight` or greater -than `maxheight`. +If a NamedTuple `pks` is given, a new NamedTuple is returned with the same fields +(references) from `pks`. `pks` must have `:indices` and `:heights` fields. The fields +`:proms`, `:widths`, and `:edges` will be filtered (mutated) if present, and any remaining +fields will be referenced unmodified. See also: [`peakproms`](@ref), [`peakwidths`](@ref), [`findmaxima`](@ref) +[`filterpeaks!`](@ref) # Examples ```jldoctest julia> x = [0,5,2,3,3,1,4,0]; -julia> xpks, vals = findmaxima(x) -([2, 4, 7], [5, 3, 4]) +julia> pks = findmaxima(x) +(indices = [2, 4, 7], heights = [5, 3, 4], data = [0, 5, 2, 3, 3, 1, 4, 0]) -julia> peakheights!(xpks, vals; maxheight=4); +julia> peakheights!(pks; max=4) +(indices = [4, 7], heights = [3, 4], data = [0, 5, 2, 3, 3, 1, 4, 0]) -julia> xpks, vals -([4, 7], [3, 4]) +julia> inds, heights = peakheights!(pks.indices, pks.heights; min=3.5) +([7], [4]) ``` """ function peakheights!( peaks::Vector{Int}, heights::AbstractVector{T}; - minheight=nothing, maxheight=nothing + minheight=nothing, maxheight=nothing, + min=minheight, max=maxheight ) where {T} + if !isnothing(minheight) + Base.depwarn("Keyword `minheight` has been renamed to `min`", :peakheights!) + end + if !isnothing(maxheight) + Base.depwarn("Keyword `maxheight` has been renamed to `max`", :peakheights!) + end length(peaks) == length(heights) || throw(DimensionMismatch("length of `peaks`, $(length(peaks)), does not match the length of `heights`, $(length(heights))")) - if !isnothing(minheight) || !isnothing(maxheight) - lo = something(minheight, typemin(Base.nonmissingtype(T))) - up = something(maxheight, typemax(Base.nonmissingtype(T))) - matched = findall(x -> !(lo ≤ x ≤ up), heights) + if !isnothing(min) || !isnothing(max) + lo = something(min, typemin(Base.nonmissingtype(T))) + hi = something(max, typemax(Base.nonmissingtype(T))) + matched = findall(x -> !(lo ≤ x ≤ hi), heights) deleteat!(peaks, matched) deleteat!(heights, matched) end @@ -70,4 +111,24 @@ function peakheights!( return peaks, heights end +function peakheights!(pks::NamedTuple; min=nothing, max=nothing) + filterpeaks!(pks, :heights; min, max) + return pks +end + +""" + peakheights!(; [min, max]) -> Function + +Create a function, `f(pks::NamedTuple)`, that calculates peak heights and then filters +(mutates) the fields of its argument, `pks`, using any given keyword arguments. + +# Examples +```jldoctest +julia> findmaxima([0, 5, 2, 3, 3, 1, 4, 0]) |> peakheights!(; max=4) +(indices = [4, 7], heights = [3, 4], data = [0, 5, 2, 3, 3, 1, 4, 0]) +``` +""" +peakheights!(; kwargs...) = function _curried_peakheights!(pks) + return peakheights!(pks; kwargs...) +end diff --git a/src/peakprom.jl b/src/peakprom.jl index f89000c..ae3be5e 100644 --- a/src/peakprom.jl +++ b/src/peakprom.jl @@ -1,91 +1,129 @@ """ - peakproms(peaks, x; - strict=true, - minprom=nothing, - maxprom=nothing - ) -> (peaks, proms) + peakproms(indices, x; [strict=true, min, max]) -> (indices, proms) + peakproms(pks::NamedTuple; [strict=true, min, max]) -> NamedTuple -Calculate the prominences of `peaks` in `x`, and removing peaks with prominences less than -`minprom` and/or greater than `maxprom`. +Calculate the prominences of peak `indices` in `x`, and remove peaks with prominences less +than `min` and/or greater than `max`. -Peak prominence is the absolute height difference between the current peak and the larger of -the two adjacent smallest magnitude points between the current peak and adjacent larger -peaks or signal ends. +Peak prominence is the absolute height (value) difference between the current peak and the +larger of the two adjacent smallest magnitude points between the current peak and adjacent +larger peaks or signal ends. -The prominence for a peak with a `NaN` or `missing` between the current peak and either -adjacent larger peaks will be `NaN` or `missing` if `strict == true`, or it will be -the larger of the smallest non-`NaN` or `missing` values between the current peak and -adjacent larger peaks for `strict == false`. +If a NamedTuple `pks` is given, a new NamedTuple is returned with filtered copies of fields +from `pks`. `pks` must have `:indices` and `:heights` fields. If `pks` has a `:proms` field, +prominences will only be filtered, and not be recalculated. The fields `:widths` and +`:edges` will also be filtered if present, and any remaining fields will be copied +unmodified. -See also: [`findminima`](@ref), [`findmaxima`](@ref), [`peakproms!`](@ref) +If `strict == true`, the prominence for a peak with a `NaN` or `missing` between the current +peak and either adjacent larger peaks will be `NaN` or `missing`, otherwise, it will be the +larger of the smallest non-`NaN` or `missing` values between the current peak and adjacent +larger peaks for `strict == false`. + +See also: [`peakproms!`](@ref), [`findmaxima`](@ref) # Examples ```jldoctest -julia> x = [0,5,2,3,3,1,4,0]; - -julia> xpks = argmaxima(x) -3-element Vector{Int64}: - 2 - 4 - 7 - -julia> peakproms(xpks, x) -([2, 4, 7], Union{Missing, Int64}[5, 1, 3]) +julia> pks = findmaxima([0,5,2,3,3,1,4,0]); -julia> x = [missing,5,2,3,3,1,4,0]; +julia> pks = peakproms(pks; min=2) +(indices = [2, 7], heights = [5, 4], data = [0, 5, 2, 3, 3, 1, 4, 0], proms = Union{Missing, Int64}[5, 3]) -julia> peakproms(xpks, x) -([2, 4, 7], Union{Missing, Int64}[missing, 1, 3]) - -julia> peakproms(xpks, x; strict=false) -([2, 4, 7], Union{Missing, Int64}[5, 1, 3]) +julia> inds, proms = peakproms(pks.indices, pks.data; max=4) +([7], Union{Missing, Int64}[3]) ``` """ function peakproms(peaks::AbstractVector{Int}, x::AbstractVector{T}; - strict=true, minprom=nothing, maxprom=nothing -) where T - if !isnothing(minprom) || !isnothing(maxprom) + strict=true, minprom=nothing, maxprom=nothing, + min=minprom, max=maxprom +) where {T} + if !isnothing(minprom) + Base.depwarn("Keyword `minprom` has been renamed to `min`", :peakproms) + end + if !isnothing(maxprom) + Base.depwarn("Keyword `maxprom` has been renamed to `max`", :peakproms) + end + if !isnothing(min) || !isnothing(max) _peaks = copy(peaks) else # peaks will not be modified _peaks = peaks end - return peakproms!(_peaks, x; strict=strict, minprom=minprom, maxprom=maxprom) + return peakproms!(_peaks, x; strict=strict, min=min, max=max) +end + +peakproms(pks::NamedTuple; kwargs...) = peakproms!(deepcopy(pks); kwargs...) + +""" + peakproms(; [strict, min, max]) -> Function + +Create a function, `f(pks::NamedTuple)`, that calculates and filters the peak +prominences of a copy of its argument, `pks`, using any given keyword arguments. + +# Examples +```jldoctest +julia> findmaxima([0,5,2,3,3,1,4,0]) |> peakproms(; min=2) +(indices = [2, 7], heights = [5, 4], data = [0, 5, 2, 3, 3, 1, 4, 0], proms = Union{Missing, Int64}[5, 3]) +``` +""" +peakproms(; kwargs...) = function _curried_peakproms(pks) + return peakproms(pks; kwargs...) end """ - peakproms!(peaks, x; - strict=true, - minprom=nothing, - maxprom=nothing - ) -> (peaks, proms) + peakproms!(indices, x; [strict=true, min, max]) -> (indices, proms) + peakproms!(pks::NamedTuple; [strict=true, min, max]) -> NamedTuple + +Calculate the prominences of peak `indices` in `x`, and remove peaks with prominences less +than `min` and/or greater than `max`. + +If a NamedTuple `pks` is given, a new NamedTuple is returned with the same fields +(references) from `pks`. `pks` must have `:indices` and `:heights` fields. If `pks` has a +`:proms` field, prominences will only be filtered, and not be recalculated. The fields +`:widths` and `:edges` will also be filtered (mutated) if present, and any remaining fields +will be copied unmodified. -Calculate the prominences of `peaks` in `x`, and removing `peaks` with prominences less than -`minprom` and/or greater than `maxprom`. Returns the modified arrays peaks and their -prominences. +See also: [`peakproms`](@ref), [`findmaxima`](@ref) +# +# Examples +```jldoctest +julia> pks = findmaxima([0,5,2,3,3,1,4,0]); + +julia> pks = peakproms!(pks; min=2) +(indices = [2, 7], heights = [5, 4], data = [0, 5, 2, 3, 3, 1, 4, 0], proms = Union{Missing, Int64}[5, 3]) -See also: [`peakproms`](@ref), [`findminima`](@ref), [`findmaxima`](@ref) +julia> inds, proms = peakproms!(pks.indices, pks.data; max=4) +([7], Union{Missing, Int64}[3]) +``` """ function peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; - strict=true, minprom=nothing, maxprom=nothing -) where T - if !isnothing(minprom) && !isnothing(maxprom) - minprom < maxprom || throw(ArgumentError("minprom must be less than maxprom")) + strict=true, minprom=nothing, maxprom=nothing, + min=minprom, max=maxprom +) where {T} + if !isnothing(minprom) + Base.depwarn("Keyword `minprom` has been renamed to `min`", :peakproms!) + end + if !isnothing(maxprom) + Base.depwarn("Keyword `maxprom` has been renamed to `max`", :peakproms!) + end + if !isnothing(min) && !isnothing(max) + min < max || throw(ArgumentError("Keyword `min` must be less than `max`")) end all(∈(eachindex(x)), peaks) || throw(ArgumentError("peaks contains invalid indices to x")) isempty(peaks) && return peaks, T[] # if peaks was calculated with strict=false, first(peaks) could be minima at firstindex - fp = length(peaks) > 1 ? peaks[2] : first(peaks) - if fp > 1 && ((x[fp] < x[fp-1]) === true) - pktype = :minima + if ismaxima(first(peaks), x; strict=false) + maxima = true + elseif isminima(first(peaks), x; strict=false) + maxima = false else - pktype = :maxima + throw(ArgumentError("The first peak in `indices` is not a local extrema")) end - cmp = pktype === :maxima ? (≥) : (≤) - exm = pktype === :maxima ? minimum : maximum - exa = pktype === :maxima ? max : min + cmp = maxima ? (≥) : (≤) + exm = maxima ? minimum : maximum + exa = maxima ? Base.max : Base.min _ref = Missing <: T ? missing : Float64 <: T ? NaN : @@ -93,7 +131,7 @@ function peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; Float16 <: T ? NaN16 : missing - proms = similar(peaks,promote_type(T,typeof(_ref))) + proms = similar(peaks, promote_type(T, typeof(_ref))) if strict lbegin, lend = firstindex(x), lastindex(x) @@ -106,16 +144,16 @@ function peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; lend) # Find extremum of left and right bounds - if isempty(lb:(peaks[i] - 1)) + if isempty(lb:(peaks[i]-1)) lref = _ref else - lref = exm(view(x, lb:(peaks[i] - 1))) + lref = exm(view(x, lb:(peaks[i]-1))) end - if isempty((peaks[i] + 1):rb) + if isempty((peaks[i]+1):rb) rref = _ref else - rref = exm(view(x, (peaks[i] + 1):rb)) + rref = exm(view(x, (peaks[i]+1):rb)) end proms[i] = abs(x[peaks[i]] - exa(lref, rref)) @@ -126,7 +164,7 @@ function peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; # finding all peaks/reverse peaks should be mitigated by the fact that # the same peaks/reverse peaks will be the pivotal elements for # numerous peaks. - if pktype === :maxima + if maxima peaks′ = argmaxima(x, 1; strict=false) notm = argminima(x, 1; strict=false) else @@ -140,15 +178,15 @@ function peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; j = searchsorted(peaks′, peaks[i]) # Find left and right bounding peaks - _lb = findprev(y -> cmp(x[y], x[peaks[i]]) === true, peaks′, first(j)-1) + _lb = findprev(y -> cmp(x[y], x[peaks[i]]) === true, peaks′, first(j) - 1) peaks′[j] === peaks[i] && (j += 1) - _rb = findnext(y -> cmp(x[y], x[peaks[i]]) === true, peaks′, last(j)+1) + _rb = findnext(y -> cmp(x[y], x[peaks[i]]) === true, peaks′, last(j) + 1) # Find left and right reverse peaks just inside the bounding peaks lb = isnothing(_lb) ? firstindex(notm) : - searchsortedfirst(notm, peaks′[_lb]) + searchsortedfirst(notm, peaks′[_lb]) rb = isnothing(_rb) ? lastindex(notm) : - searchsortedlast(notm, peaks′[_rb]) + searchsortedlast(notm, peaks′[_rb]) k = searchsortedfirst(notm, peaks[i]) @@ -169,10 +207,10 @@ function peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; end end - if !isnothing(minprom) || !isnothing(maxprom) - lo = something(minprom, zero(eltype(x))) - up = something(maxprom, typemax(Base.nonmissingtype(eltype(x)))) - matched = findall(x -> !ismissing(x) && !(lo ≤ x ≤ up), proms) + if !isnothing(min) || !isnothing(max) + lo = something(min, zero(eltype(x))) + hi = something(max, typemax(Base.nonmissingtype(eltype(x)))) + matched = findall(x -> !ismissing(x) && !(lo ≤ x ≤ hi), proms) deleteat!(peaks, matched) deleteat!(proms, matched) end @@ -180,3 +218,30 @@ function peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; return peaks, proms end +function peakproms!(pks::NamedTuple; strict=true, min=nothing, max=nothing) + if !hasproperty(pks, :proms) + # Avoid filtering by min/max/strict here, so that it always happens outside if-statement. + # Pro: one less edge case. Con: More internal allocations + _, proms = peakproms(pks.indices, pks.data; strict) + pks = merge(pks, (; proms)) + end + filterpeaks!(pks, :proms; min, max) + return pks +end + +""" + peakproms!(; [strict, min, max]) -> Function + +Create a function, `f(pks::NamedTuple)`, that calculates and filters (mutates) the peak +prominences and other fields of its argument, `pks`, using any given keyword arguments. + +# Examples +```jldoctest +julia> findmaxima([0,5,2,3,3,1,4,0]) |> peakproms!(; min=2) +(indices = [2, 7], heights = [5, 4], data = [0, 5, 2, 3, 3, 1, 4, 0], proms = Union{Missing, Int64}[5, 3]) +``` +""" +peakproms!(; kwargs...) = function _curried_peakproms!(pks) + return peakproms!(pks; kwargs...) +end + diff --git a/src/peakwidth.jl b/src/peakwidth.jl index 47fd620..f24dddb 100644 --- a/src/peakwidth.jl +++ b/src/peakwidth.jl @@ -1,98 +1,144 @@ """ - peakwidths(peaks, x, proms; - strict=true, - relheight=0.5, - minwidth=nothing, - maxwidth=nothing - ) -> (peaks, widths, leftedge, rightedge) + peakwidths(indices, x, proms; [strict=true, relheight=0.5, min, max]) -> (indices, widths, ledge, redge) + peakwidths(pks::NamedTuple; [strict=true, relheight=0.5, min, max]) -> NamedTuple -Calculate the widths of `peaks` in `x` at a reference level based on `proms` and -`relheight`, and removing peaks with widths less than `minwidth` and/or greater than -`maxwidth`. Returns the peaks, widths, and the left and right edges at the reference level. +Calculate the widths of peak `indices` in `x` at a reference level based on `proms` and +`relheight`, and removing peaks with widths less than `min` and/or greater than +`max`. Returns the peaks, widths, and the left and right edges at the reference level. Peak width is the distance between the signal crossing a reference level before and after the peak. Signal crossings are linearly interpolated between indices. The reference level is the difference between the peak height and `relheight` times the peak prominence. Width cannot be calculated for a `NaN` or `missing` prominence. -The width for a peak with a gap in the signal (e.g. `NaN`, `missing`) at the reference level -will match the value/type of the signal gap if `strict == true`. For `strict == -false`, the signal crossing will be linearly interpolated between the edges of the gap. +If a NamedTuple `pks` is given, a new NamedTuple is returned with filtered copies of fields +from `pks`. `pks` must have `:indices`, `:heights`, and `:proms` fields. If `pks` has +`:widths` and `:edges` fields, they will not be recalculated, but filtered only. Any +remaining fields will be copied unmodified. -See also: [`peakproms`](@ref), [`findminima`](@ref), [`findmaxima`](@ref) +If `strict == true`, the width for a peak with a gap in the signal (e.g. `NaN`, `missing`) +at the reference level will match the value/type of the signal gap. Otherwise, the signal +crossing will be linearly interpolated between the edges of the gap. + +See also: [`peakwidths!`](@ref), [`peakproms`](@ref), [`findmaxima`](@ref) # Examples ```jldoctest -julia> x = [0,1,0,-1.]; +julia> x = Float64[0,5,2,2,3,3,1,4,0]; -julia> xpks = argmaxima(x) -1-element Vector{Int64}: - 2 +julia> pks = findmaxima(x) |> peakproms!(;max=2); -julia> peakwidths(xpks, x, [1]) -([2], [1.0], [1.5], [2.5]) +julia> peakwidths(pks) +(indices = [5], heights = [3.0], data = [0.0, 5.0, 2.0, 2.0, 3.0, 3.0, 1.0, 4.0, 0.0], proms = [1.0], widths = [1.75], edges = [(4.5, 6.25)]) -julia> x[3] = NaN; +julia> x[4] = NaN; -julia> peakwidths(xpks, x, [1]) -([2], [NaN], [1.5], [NaN]) +julia> peakwidths(pks.indices, x, pks.proms) +([5], [NaN], [NaN], [6.25]) -julia> peakwidths(xpks, x, [1]; strict=false) -([2], [1.0], [1.5], [2.5]) +julia> peakwidths(pks.indices, x, pks.proms; strict=false) +([5], [2.25], [4.0], [6.25]) ``` """ function peakwidths( peaks::AbstractVector{Int}, x::AbstractVector, proms::AbstractVector; strict=true, relheight=0.5, minwidth=nothing, maxwidth=nothing, + min=minwidth, max=maxwidth ) - if !isnothing(minwidth) || !isnothing(maxwidth) + if !isnothing(minwidth) + Base.depwarn("Keyword `minwidth` has been renamed to `min`", :peakwidths) + end + if !isnothing(maxwidth) + Base.depwarn("Keyword `maxwidth` has been renamed to `max`", :peakwidths) + end + if !isnothing(min) || !isnothing(max) _peaks = copy(peaks) else # peaks will not be modified _peaks = peaks end peakwidths!(_peaks, x, proms; strict=strict, relheight=relheight, - minwidth=minwidth, maxwidth=maxwidth) + min=min, max=max) +end + +peakwidths(pks::NamedTuple; kwargs...) = peakwidths!(deepcopy(pks); kwargs...) + +""" + peakwidths(; [strict, relheight, min, max]) -> Function + +Create a function, `f(pks::NamedTuple)`, that calculates and filters the peak widths of a +copy of its argument, `pks`, using any given keyword arguments. + +# Examples +```jldoctest +julia> findmaxima([0,5,2,3,3,1,4,0]) |> peakproms() |> peakwidths(; min=1.5) +(indices = [4], heights = [3], data = [0, 5, 2, 3, 3, 1, 4, 0], proms = Union{Missing, Int64}[1], widths = Union{Missing, Float64}[1.75], edges = Tuple{Union{Missing, Float64}, Union{Missing, Float64}}[(3.5, 5.25)]) +``` +""" +peakwidths(; kwargs...) = function _curried_peakwidths(pks) + return peakwidths(pks; kwargs...) end """ - peakwidths!(peaks, x, proms; - strict=true, - relheight=0.5, - minwidth=nothing, - maxwidth=nothing - ) -> (peaks, widths, leftedge, rightedge) - -Calculate the widths of `peaks` in `x` at a reference level based on `proms` and -`relheight`, removing peaks with widths less than `minwidth` and/or greater than `maxwidth`. + peakwidths!(indices, x; [strict=true, relheight=0.5, min, max]) -> (indices, widths, ledge, redge) + peakwidths!(pks::NamedTuple; [strict=true, relheight=0.5, min, max]) -> NamedTuple + +Calculate the widths of peak `indices` in `x` at a reference level based on `proms` and +`relheight`, removing peaks with widths less than `min` and/or greater than `max`. Returns the modified peaks, widths, and the left and right edges at the reference level. -See also: [`peakwidths`](@ref), [`peakproms`](@ref), [`findminima`](@ref), [`findmaxima`](@ref) +If a NamedTuple `pks` is given, a new NamedTuple is returned with the same fields (references) +from `pks`. `pks` must have `:indices`, `:heights`, and `:proms` fields. If `pks` has +`:widths` and `:edges` fields, they will not be recalculated, but filtered only. Any +remaining fields will be copied unmodified. + +See also: [`peakwidths`](@ref), [`peakproms`](@ref), [`findmaxima`](@ref) +# +# Examples +```jldoctest ; filter = r"(\\d*)\\.(\\d{3})\\d*" => s"\\1.\\2***" +julia> x = Float64[0,5,2,2,3,3,1,4,0]; + +julia> pks = findmaxima(x) |> peakproms!(); + +julia> peakwidths!(pks; min=1) +(indices = [2, 5], heights = [5.0, 3.0], data = [0.0, 5.0, 2.0, 2.0, 3.0, 3.0, 1.0, 4.0, 0.0], proms = [5.0, 1.0], widths = [1.333, 1.75], edges = [(1.5, 2.833), (4.5, 6.25)]) + +julia> peakwidths!(pks.indices, pks.data, pks.proms; min=1) +([2, 5], [1.333, 1.75], [1.5, 4.5], [2.833, 6.25]) +``` """ function peakwidths!( peaks::AbstractVector{Int}, x::AbstractVector{T}, proms::AbstractVector{U}; strict=true, relheight=0.5, minwidth=nothing, maxwidth=nothing, -) where {T, U} - if !isnothing(minwidth) && !isnothing(maxwidth) - minwidth < maxwidth || throw(ArgumentError("maxwidth must be greater than minwidth")) + min=minwidth, max=maxwidth +) where {T,U} + if !isnothing(minwidth) + Base.depwarn("Keyword `minwidth` has been renamed to `min`", :peakwidths!) + end + if !isnothing(maxwidth) + Base.depwarn("Keyword `maxwidth` has been renamed to `max`", :peakwidths!) + end + if !isnothing(min) && !isnothing(max) + min < max || throw(ArgumentError("Keyword `min` must be less than `max`")) end all(∈(eachindex(x)), peaks) || throw(ArgumentError("peaks contains invalid indices to x")) # if peaks was calculated with strict=false, first(peaks) could be minima at firstindex - fp = length(peaks) > 1 ? peaks[2] : first(peaks) - if fp > 1 && ((x[fp] < x[fp-1]) === true) - pktype = :minima + if ismaxima(first(peaks), x; strict=false) + maxima = true + elseif isminima(first(peaks), x; strict=false) + maxima = false else - pktype = :maxima + throw(ArgumentError("The first peak in `indices` is not a local extrema")) end - cmp = pktype === :maxima ? (≤) : (≥) - op = pktype === :maxima ? (-) : (+) + cmp = maxima ? (≤) : (≥) + op = maxima ? (-) : (+) - V1 = promote_type(T,U) + V1 = promote_type(T, U) _bad = Missing <: V1 ? missing : float(Int)(NaN) - V = promote_type(V1, typeof(_bad)) + V = promote_type(V1, float(Int)) ledge = similar(proms, V) redge = similar(proms, V) @@ -109,35 +155,34 @@ function peakwidths!( redge[i] = _bad ledge[i] = _bad else - ht = op(x[peaks[i]], relheight*proms[i]) - lo = findprev(v -> !ismissing(v) && cmp(v,ht), x, peaks[i]) - up = findnext(v -> !ismissing(v) && cmp(v,ht), x, peaks[i]) + ht = op(x[peaks[i]], relheight * proms[i]) + lo = findprev(v -> !ismissing(v) && cmp(v, ht), x, peaks[i]) + hi = findnext(v -> !ismissing(v) && cmp(v, ht), x, peaks[i]) if !strict if !isnothing(lo) - lo1 = findnext(v -> !ismissing(v) && cmp(ht,v), x, lo+1) - lo += (ht - x[lo])/(x[lo1] - x[lo])*(lo1 - lo) + lo1 = findnext(v -> !ismissing(v) && cmp(ht, v), x, lo + 1) + lo += (ht - x[lo]) / (x[lo1] - x[lo]) * (lo1 - lo) end - if !isnothing(up) - up1 = findprev(v -> !ismissing(v) && cmp(ht,v), x, up-1) - up -= (ht - x[up])/(x[up1] - x[up])*(up - up1) + if !isnothing(hi) + hi1 = findprev(v -> !ismissing(v) && cmp(ht, v), x, hi - 1) + hi -= (ht - x[hi]) / (x[hi1] - x[hi]) * (hi - hi1) end else - !isnothing(lo) && (lo += (ht - x[lo])/(x[lo+1] - x[lo])) - !isnothing(up) && (up -= (ht - x[up])/(x[up-1] - x[up])) + !isnothing(lo) && (lo += (ht - x[lo]) / (x[lo+1] - x[lo])) + !isnothing(hi) && (hi -= (ht - x[hi]) / (x[hi-1] - x[hi])) end - - redge[i] = something(up, lst) + redge[i] = something(hi, lst) ledge[i] = something(lo, fst) end end widths::Vector{V} = redge - ledge - if !isnothing(minwidth) || !isnothing(maxwidth) - lo = something(minwidth, zero(eltype(widths))) - up = something(maxwidth, typemax(Base.nonmissingtype(eltype(widths)))) - matched = findall(x -> !ismissing(x) && !(lo ≤ x ≤ up), widths) + if !isnothing(min) || !isnothing(max) + lo = something(min, zero(eltype(widths))) + hi = something(max, typemax(Base.nonmissingtype(eltype(widths)))) + matched = findall(x -> !ismissing(x) && !(lo ≤ x ≤ hi), widths) deleteat!(peaks, matched) deleteat!(ledge, matched) deleteat!(redge, matched) @@ -147,3 +192,35 @@ function peakwidths!( return peaks, widths, ledge, redge end +function peakwidths!(pks::NamedTuple; strict=true, relheight=0.5, min=nothing, max=nothing) + !haskey(pks, :proms) && throw(ArgumentError( + "Argument `pks` is expected to have prominences (`:proms`) already calculated")) + if xor(hasproperty(pks, :widths), hasproperty(pks, :edges)) + throw(ArgumentError("Argument `pks` is expected have neither or both of the fields `:widths` and `:edges`.")) + end + if !hasproperty(pks, :widths) + # Avoid filtering by min/max/strict here, so that it always happens outside if-statement. + # Pro: one less edge case. Con: More internal allocations + _, widths, leftedges, rightedges = peakwidths(pks.indices, pks.data, pks.proms; relheight, strict) + pks = merge(pks, (; widths, edges=collect(zip(leftedges, rightedges)))) + end + filterpeaks!(pks, :widths; min, max) + return pks +end + +""" + peakwidths!(; [strict, relheight, min, max]) -> Function + +Create a function, `f(pks::NamedTuple)`, that calculates and filters (mutates) the peak +widths and other fields of its argument, `pks`, using any given keyword arguments. + +# Examples +```jldoctest +julia> findmaxima([0,5,2,3,3,1,4,0]) |> peakproms!() |> peakwidths!(; min=1.5) +(indices = [4], heights = [3], data = [0, 5, 2, 3, 3, 1, 4, 0], proms = Union{Missing, Int64}[1], widths = Union{Missing, Float64}[1.75], edges = Tuple{Union{Missing, Float64}, Union{Missing, Float64}}[(3.5, 5.25)]) +``` +""" +peakwidths!(; kwargs...) = function _curried_peakwidths!(pks) + return peakwidths!(pks; kwargs...) +end + diff --git a/src/plot.jl b/src/plot.jl index e9bb512..e13a717 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -27,7 +27,7 @@ end elseif isminima(first(peaks), y; strict=false) maxima = false else - throw(error("The first peak in `peaks` is not a local extrema")) + throw(ArgumentError("The first peak in `peaks` is not a local extrema")) end sgn = maxima ? -1 : +1 ext_color = maxima ? :Red : :Green diff --git a/src/utils.jl b/src/utils.jl new file mode 100644 index 0000000..7254029 --- /dev/null +++ b/src/utils.jl @@ -0,0 +1,142 @@ +const known_fields = (:indices, :proms, :heights, :widths, :edges) + +function check_known_fields_equal_length(pks::NamedTuple) + features_to_filter = known_fields + + feature_lengths = [length(pks[feature]) + for feature in features_to_filter if hasproperty(pks, feature)] + + # We refrain from using `allequal` to support Julia < 1.8 + if !all(first(feature_lengths) == feature_lengths[i] + for i in eachindex(feature_lengths)) + length_pairs = [feature=>length(pks[feature]) + for feature in features_to_filter if hasproperty(pks, feature)] + throw(DimensionMismatch("Expected all known fields of `pks` to be of equal length. Instead found the following pairs of known field and length: $length_pairs")) + end + return nothing +end + +function check_has_required_fields(pks::NamedTuple) + !haskey(pks, :indices) && throw(ArgumentError( + "`pks` is missing required field `:indices`")) + return nothing +end + +""" + filterpeaks!(pks::NT, feature; [min, max]) where {NT<:NamedTuple} -> pks::NT + filterpeaks!(pks::NT, mask) -> pks::NT + +Filter the standard `pks` fields where peaks are removed if `pks.\$feature` is less than +`min` and/or greater than `max`. If a `mask` is given, peaks are filtered (removed) if `mask[i] == false`. + +Standard Peaks.jl fields of `pks` are `:indices`, `:proms`, `:heights`, `:widths`, `:edges`. + +# Examples +```jldoctest +julia> pks = findmaxima([0,5,2,3,3,1,4,0]) +(indices = [2, 4, 7], heights = [5, 3, 4], data = [0, 5, 2, 3, 3, 1, 4, 0]) + +julia> filterpeaks!(pks, :heights; max=4) +(indices = [4, 7], heights = [3, 4], data = [0, 5, 2, 3, 3, 1, 4, 0]) + +julia> pks = findmaxima([0,5,2,3,3,1,4,0]) |> peakproms!(); + +julia> mask = [pks.heights[i] < 5 && pks.proms[i] > 2 for i in eachindex(pks.indices)] +3-element Vector{Bool}: + 0 + 0 + 1 + +julia> filterpeaks!(pks, mask) +(indices = [7], heights = [4], data = [0, 5, 2, 3, 3, 1, 4, 0], proms = Union{Missing, Int64}[3]) +``` +""" +function filterpeaks!(pks::NamedTuple, mask::Union{BitVector, Vector{Bool}}) + # Check lengths first to avoid a dimension mismatch + # after having filtered some features. + # feature_mask = hasproperty.(pks, features_to_filter) + check_has_required_fields(pks) + check_known_fields_equal_length(pks) + + if length(pks.indices) != length(mask) + throw(DimensionMismatch( + "Length of `mask` is $(length(mask)), but the length of each of the known fields of `pks` is $(length(pks[1])). + This means that the given mask can not be used to filter the given named tuple `pks`." + )) + end + + for field in known_fields # Only risk mutating fields added by this package + hasproperty(pks, field) || continue # Do nothing if field is not present + deleteat!(pks[field], .!mask) + end + return pks +end + +function filterpeaks!(pks::NamedTuple, feature::Symbol; min=nothing, max=nothing) + if !isnothing(min) || !isnothing(max) + lo = something(min, zero(eltype(pks.data))) + hi = something(max, typemax(Base.nonmissingtype(eltype(pks.data)))) + mask = map(x -> !ismissing(x) && (lo ≤ x ≤ hi), pks[feature]) + filterpeaks!(pks, mask) + end + return pks +end + +""" + filterpeaks!(pred, pks) -> NamedTuple + +Apply a predicate function `pred` to NamedTuple slices to get a filter-mask (i.e. the scalar +values related to each peak, such as `(indices=5, heights=3, proms=2)`). A peak is removed +if `pred` returns `false`. + +# Examples +```jldoctest +julia> pks = findmaxima([0,5,2,3,3,1,4,0]) +(indices = [2, 4, 7], heights = [5, 3, 4], data = [0, 5, 2, 3, 3, 1, 4, 0]) + +julia> filterpeaks!(pks) do nt + return nt.heights > 4.5 || nt.heights < 3.5 + end +(indices = [2, 4], heights = [5, 3], data = [0, 5, 2, 3, 3, 1, 4, 0]) +``` +""" +function filterpeaks!(pred::Function, pks::NamedTuple) + check_has_required_fields(pks) + check_known_fields_equal_length(pks) + + # `pks` key's except for `data`, which isn't filtered + pks_keys = filter(!(==(:data)), keys(pks)) + mask = map(eachindex(pks.indices)) do i + nt_slice = NamedTuple{pks_keys}(ntuple(j -> getindex(pks[pks_keys[j]], i), length(pks_keys))) + return !pred(nt_slice) + end + + for field in known_fields # Only risk mutating fields added by this package + hasproperty(pks, field) || continue # Do nothing if field is not present + deleteat!(pks[field], mask) + end + return pks +end + + +#==================================================================== +We store a version of findpeak here, as it might be implemented soon, +and I did not want to throw away this implementation +""" + findpeaks(x) -> NamedTuple + findpeaks(x, w=1; strict=true) -> NamedTuple + +Find the peaks in a vector `x`, where each maxima i is either +the maximum of x[i-w:i+w] or the first index of a plateau. +A `NamedTuple` is returned with the original vector +in the field `data`, and the indices of the peaks +in the field `indices`. + +This function serves as the entry-point for other +functions such as `peakproms!` and `peakwidths!` +""" +function findpeaks(x::AbstractVector, w::Int=1; strict=true) + indices, heights = findmaxima(x, w; strict) + return (data=x, indices=indices, heights=heights) +end +=# diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 0000000..c7ee348 --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,6 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" +Peaks = "18e31ff7-3703-566c-8e60-38913d67486b" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/manual_tests.jl b/test/manual_tests.jl new file mode 100644 index 0000000..c6241f1 --- /dev/null +++ b/test/manual_tests.jl @@ -0,0 +1,43 @@ +# These tests are intended to be run manually and interactively +# to investigate the current functioning of the package. + +data = [1, 2, 3, 4, 5, 4, 3, 2, 1, 6, 1] +pks = findmaxima(data) +pks = peakproms!(pks) +pks = peakwidths!(pks) + +##! Below is code intended to generate docstring examples +data = [1, 5, 1, 3, 2]; +pks = findmaxima(data) + +data = [1, 5, 1, 3, 2]; +valleys = findminima(data) + +data = [1, 5, 1, 3, 2]; +pks = findmaxima(data); +pks = peakproms!(pks) +data|>findmaxima|>peakproms! + +data = [1, 5, 1, 3, 2]; +pks = findmaxima(data); +pks = peakwidths!(pks) +data|>findmaxima|>peakwidths! + +data = [1, 5, 1, 3, 2]; +pks = findmaxima(data); +pks = peakheights!(pks, min=4) +data|>findmaxima|>peakheights! + +##! Manually run the tests in a dedicated environment +run_tests = true +if run_tests + using Pkg + pkg_path = joinpath(homedir(), ".julia", "dev", "Peaks.jl") + Pkg.activate("PeaksTestEnv"; shared=true) + Pkg.develop(path=pkg_path) + Pkg.add(["OffsetArrays", "Plots"]) + include(joinpath(pkg_path, "test", "runtests.jl")) +end + +##! The line that caused an error in the tests: +_, widths, _, _ = peakwidths([2], [0.,1.,0.], [missing]) \ No newline at end of file diff --git a/test/peakheight.jl b/test/peakheight.jl index 74a7579..280e8ab 100644 --- a/test/peakheight.jl +++ b/test/peakheight.jl @@ -4,16 +4,22 @@ @test_throws DimensionMismatch peakheights(pks, heights[1:end-1]) + # TODO: Remove after next breaking release (v0.5) + @test_logs (:warn, r"renamed") peakheights(pks, heights; maxheight=1) + @test_logs (:warn, r"renamed") peakheights(pks, heights; minheight=1) + @test_logs (:warn, r"renamed") peakheights!(copy(pks), copy(heights); maxheight=1) + @test_logs (:warn, r"renamed") peakheights!(copy(pks), copy(heights); minheight=1) + filtpks, Y = peakheights(pks, heights) @test pks == filtpks @test Y == heights - _, Y = peakheights(pks, heights; maxheight=4) + _, Y = peakheights(pks, heights; max=4) @test all(≤(4), Y) - _, Y = peakheights(pks, heights; minheight=4) + _, Y = peakheights(pks, heights; min=4) @test all(≥(4), Y) - _, Y = peakheights(pks, heights; maxheight=4.5, minheight=3.5) + _, Y = peakheights(pks, heights; max=4.5, min=3.5) @test all(x -> 3.5 ≤ x ≤ 4.5, Y) end diff --git a/test/peakprom.jl b/test/peakprom.jl index 4ff30fb..7ae56ad 100644 --- a/test/peakprom.jl +++ b/test/peakprom.jl @@ -63,19 +63,25 @@ x1 = a*sin.(2*pi*f1*T*t)+b*sin.(2*pi*f2*T*t)+c*sin.(2*pi*f3*T*t); p5 = [-1,6,3,4,2,4,2,5,-2,0] @test last(peakproms(argmaxima(p5, 3; strict=false), p5; strict=false)) == [7,3] @test last(peakproms(argmaxima(reverse(p5), 3; strict=false), reverse(p5); strict=false)) == [3,7] - - end @testset "Min/max prominence" begin sint = sin.(T:T:6pi) maxs = argmaxima(sint) - @test length(first(peakproms(maxs, sint; minprom=1.5))) == 2 - @test length(first(peakproms(maxs, sint; maxprom=1.5))) == 1 + @test length(first(peakproms(maxs, sint; min=1.5))) == 2 + @test length(first(peakproms(maxs, sint; max=1.5))) == 1 + + @test_throws ArgumentError peakproms([1,2,3], sint; max=0.1, min=1) - @test_throws ArgumentError peakproms([1,2,3], sint; maxprom=0.1, minprom=1) + # TODO: Remove after next breaking release (v0.5) + @test_logs (:warn, r"renamed") peakproms(maxs, sint; maxprom=1) + @test_logs (:warn, r"renamed") peakproms(maxs, sint; minprom=1) + @test_logs (:warn, r"renamed") peakproms!(copy(maxs), copy(sint); maxprom=1) + @test_logs (:warn, r"renamed") peakproms!(copy(maxs), copy(sint); minprom=1) end + @test_throws ArgumentError peakproms([2], 1:10) + # issue #4 let i, p i, p = peakproms(Int[], zeros(10)) @@ -88,5 +94,5 @@ x1 = a*sin.(2*pi*f1*T*t)+b*sin.(2*pi*f2*T*t)+c*sin.(2*pi*f3*T*t); issue91ddaa9 = [1,2,3,2,3,1] @test all(!iszero, last(peakproms(argminima(issue91ddaa9; strict=false), issue91ddaa9; strict=false))) -end +end diff --git a/test/peakwidth.jl b/test/peakwidth.jl index 3a2fa93..229ebae 100644 --- a/test/peakwidth.jl +++ b/test/peakwidth.jl @@ -38,10 +38,19 @@ sinpks = argmaxima(sint) _, proms = peakproms(sinpks, sint) - @test length(first(peakwidths(sinpks, sint, proms; minwidth=pi*75))) == 2 - @test length(first(peakwidths(sinpks, sint, proms; maxwidth=pi*75))) == 1 + @test length(first(peakwidths(sinpks, sint, proms; min=pi*75))) == 2 + @test length(first(peakwidths(sinpks, sint, proms; max=pi*75))) == 1 - @test_throws ArgumentError peakwidths(1:3, ones(3), ones(3); maxwidth=0.1, minwidth=1) + @test_throws ArgumentError peakwidths(1:3, ones(3), ones(3); max=0.1, min=1) + + # TODO: Remove after next breaking release (v0.5) + @test_logs (:warn, r"renamed") peakwidths(sinpks, sint, proms; maxwidth=1) + @test_logs (:warn, r"renamed") peakwidths(sinpks, sint, proms; minwidth=1) + @test_logs (:warn, r"renamed") peakwidths!(copy(sinpks), copy(sint), copy(proms); maxwidth=1) + @test_logs (:warn, r"renamed") peakwidths!(copy(sinpks), copy(sint), copy(proms); minwidth=1) end + @test_throws ArgumentError peakwidths([2], 1:10, [1]) + @test_throws ArgumentError peakwidths!((;proms=[1], widths=[2])) + @test_throws ArgumentError peakwidths!((;proms=[1], edges=[2])) end diff --git a/test/plotting.jl b/test/plotting.jl index ae39585..a0b7c06 100644 --- a/test/plotting.jl +++ b/test/plotting.jl @@ -6,17 +6,20 @@ let # find and plot maxima pks, vals = findmaxima(y) - pks, proms = peakproms!(pks, y; minprom=1) + pks, proms = peakproms!(pks, y; min=1) plt = plotpeaks(t, y, peaks=pks, prominences=true, widths=true) @test plt isa Plots.Plot # add minima to plot pks, vals = findminima(y) - pks, proms = peakproms!(pks, y; minprom=1) + pks, proms = peakproms!(pks, y; min=1) plt = plotpeaks!(t, y, peaks=pks, prominences=true, widths=true) @test plt isa Plots.Plot + # first peak isn't an extrema + @test_throws ArgumentError plotpeaks(t, y; peaks=[2]) + # plt = plotpeaks(t, y, peaks=pks, prominences=true, widths=true) # savepath_png = abspath(joinpath(@__DIR__, "..", "docs", "src", "assets", "images", "minima_prom_width.png")) # savefig(plt, savepath_png) diff --git a/test/runtests.jl b/test/runtests.jl index fe4a297..08dbb0e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,8 +1,15 @@ using Peaks -using Test, OffsetArrays, Plots +using Test, OffsetArrays, Plots, Documenter + +DocMeta.setdocmeta!(Peaks, :DocTestSetup, :(using Peaks); recursive=true) include("minmax.jl") include("peakprom.jl") include("peakwidth.jl") include("peakheight.jl") include("plotting.jl") +include("utils.jl") + +@testset "Doctests" begin + doctest(Peaks; manual=false) +end diff --git a/test/utils.jl b/test/utils.jl new file mode 100644 index 0000000..0c3e8bc --- /dev/null +++ b/test/utils.jl @@ -0,0 +1,7 @@ +using Peaks: check_known_fields_equal_length, check_has_required_fields +@testset "Utility functions" begin + @test_throws DimensionMismatch check_known_fields_equal_length((;indices=rand(4), heights=rand(3))) + @test_throws ArgumentError check_has_required_fields((;nonstandard=1)) + + @test_throws DimensionMismatch filterpeaks!((;indices=rand(4), heights=rand(4)), rand(Bool, 5)) +end