From baa439ea868d107a4cdb1a8f598aa54aab3a6a57 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Thu, 14 Sep 2023 15:40:33 +0200 Subject: [PATCH 01/63] Ignore autogenerated .vscode folder --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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/ From d235238e5c70119040fee58166a4177086e1e483 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Thu, 14 Sep 2023 15:52:40 +0200 Subject: [PATCH 02/63] Make parent file for rework, define "findpeaks" --- src/Peaks.jl | 3 ++- src/api_rework.jl | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 src/api_rework.jl diff --git a/src/Peaks.jl b/src/Peaks.jl index a2eab71..fa8cc6b 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, findpeaks include("minmax.jl") +include("api_rework.jl") include("peakprom.jl") include("peakwidth.jl") include("peakheight.jl") diff --git a/src/api_rework.jl b/src/api_rework.jl new file mode 100644 index 0000000..298c8a3 --- /dev/null +++ b/src/api_rework.jl @@ -0,0 +1,9 @@ +# I am putting everything in here for now. The contents of this file should be moved around in the future. +""" + Docs here +""" +function findpeaks(x) + data = x + inds, _ = findmaxima(x) + return (data=x, inds=inds) +end \ No newline at end of file From ccb918eea88b8d0b0b6faa9cd7c79a1e49ed75ca Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Thu, 14 Sep 2023 18:46:30 +0200 Subject: [PATCH 03/63] implement api_rework as discussed --- src/Peaks.jl | 23 +++++++-- src/api_rework.jl | 117 ++++++++++++++++++++++++++++++++++++++++++++-- src/peakheight.jl | 6 +-- src/peakprom.jl | 30 ++++++------ src/peakwidth.jl | 32 ++++++------- 5 files changed, 167 insertions(+), 41 deletions(-) diff --git a/src/Peaks.jl b/src/Peaks.jl index fa8cc6b..09d0c14 100644 --- a/src/Peaks.jl +++ b/src/Peaks.jl @@ -2,15 +2,30 @@ module Peaks using Compat -export argmaxima, argminima, maxima, minima, findmaxima, findminima, findnextmaxima, - findnextminima, peakproms, peakproms!, peakwidths, peakwidths!, peakheights, - peakheights!, ismaxima, isminima, findpeaks +# Old exports: +# export argmaxima, argminima, maxima, minima, findmaxima, findminima, findnextmaxima, findnextminima, peakproms, peakproms!, peakwidths, peakwidths!, peakheights, peakheights!, ismaxima, isminima, findpeaks +# Proposed exports: +export findpeaks, peakproms!, peakwidths!, peakheights! include("minmax.jl") -include("api_rework.jl") include("peakprom.jl") include("peakwidth.jl") include("peakheight.jl") +include("api_rework.jl") include("plot.jl") +function bla(x) + pks = findpeaks(x) + pks = peakproms!(pks, min=0.5) + pks = peakwidths!(pks) + pks = peakheights!(pks) + return pks +end + +function bla2(x) + pks = findpeaks(x) |> peakproms!(min=0.5) |> peakwidths!() |> peakheights!() + return pks +end + +export bla, bla2 end # module Peaks diff --git a/src/api_rework.jl b/src/api_rework.jl index 298c8a3..9fd8d7a 100644 --- a/src/api_rework.jl +++ b/src/api_rework.jl @@ -1,9 +1,120 @@ # I am putting everything in here for now. The contents of this file should be moved around in the future. """ - Docs here + findpeaks(x) -> NamedTuple + +Find the peaks in a vector `x`. +A `NamedTuple` is returned with the original vector +in the field `data`, and the indices of the peaks +in the field `inds`. + +This function serves as the entry-point for other +functions such as `peakproms!` and `peakwidths!` """ -function findpeaks(x) +function findpeaks(x::AbstractVector) data = x inds, _ = findmaxima(x) return (data=x, inds=inds) -end \ No newline at end of file +end + +""" + _filter_fields!(pks, new_inds) + +Internal function to mutate the vectors in all fields of `pks` +to remove any element with a corresponding `pks.inds` entry +not present in `new_inds`. +""" +function _filter_fields!(pks, new_inds) + # Add checks on `pks` to see if it has fields "data" and "inds"? + mask = pks.inds .∉ Ref(new_inds) + for field in filter(!=(:data), propertynames(pks)) # Do not want to mutate data vector + v_to_be_mutated = getfield(pks, field) + if length(v_to_be_mutated) != length(mask) + error("Internal error: The length of the vector in field `$field` is $(length(v_to_be_mutated)), but was expected to be $(length(mask))") + end + deleteat!(getfield(pks, field), mask) + end + return nothing +end + +""" + peakproms!(pks::NamedTuple; min=0, max=Inf, strict=true) + peakproms!(; min=0, max=Inf, strict=true) + +Find the prominences of the peaks in `pks`, and filter out any peak +with a prominence smaller than `min` or greater than `max`. +The prominences are returned in the field `:proms` of the returned named tuple. + +If the positional argument `pks` is omitted, an anonymus function is returned +that performs the action (adding field `:proms` and filtering) to its input. +""" +function peakproms!(pks::NamedTuple; min=0, max=Inf, strict=true) + 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.inds, pks.data) + pks = merge(pks, (; proms)) + end + new_inds, _ = _peakproms(pks.inds, pks.data; minprom=min, maxprom=max, strict) + _filter_fields!(pks, new_inds) + return pks +end +peakproms!(; kwargs...) = pks -> peakproms!(pks; kwargs...) + + +""" + peakwidths!(pks::NamedTuple; min=0, max=Inf, relheight=0.5, strict=true) + peakwidths!(; min=0, max=Inf, relheight=0.5, strict=true) + +Find the widths of the peaks in `pks`, and filter out any peak +with a width smaller than `min` or greater than `max`. +The widths are returned in the field `:widths` of the returned named tuple. +The edges of the peaks are also added in the field `:edges`. + +If the positional argument `pks` is omitted, an anonymus function is returned +that performs the action (adding fields `:widths` and `:edges` and filtering) to its input. + +Note: If `pks` does not have a field `proms`, it is added. This is +because it is needed to calculate the peak width. +""" +function peakwidths!(pks::NamedTuple; min=0, max=Inf, relheight=0.5, strict=true) + if !hasproperty(pks, :proms) # Add proms if needed + pks = peakproms!(pks) + end + if xor(hasproperty(pks, :widths), hasproperty(pks, :edges)) + throw(ArgumentError("The named tuple `pks` (first argument to `peakwidths!` is expected have both the fields `:widths` and `:edges`, or to have neither of them. The provided `pks` only has one of them.")) + 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.inds, pks.data, pks.proms) + pks = merge(pks, (; widths, edges=collect(zip(leftedges, rightedges)))) + end + new_inds, _ = _peakwidths(pks.inds, pks.data, pks.proms; minwidth=min, maxwidth=max, relheight, strict) + _filter_fields!(pks, new_inds) + return pks +end +peakwidths!(; kwargs...) = pks -> peakwidths!(pks; kwargs...) + +""" + peakheights!(pks::NamedTuple; min=0, max=Inf) + peakheights!(; min=0, max=Inf) + +Find the heights of the peaks in `pks`, and filter out any peak +with a heights smaller than `min` or greater than `max`. +The heights are returned in the field `:heights` of the returned named tuple. + +If the positional argument `pks` is omitted, an anonymus function is returned +that performs the action (adding field `heights` and filtering) to its input. +""" +function peakheights!(pks::NamedTuple; min=0, max=Inf) + if !hasproperty(pks, :heights) + # Avoid filtering by min/max here, so that it always happens outside if-statement. + # Pro: one less edge case. Con: More internal allocations + heights = pks.data[pks.inds] + pks = merge(pks, (; heights)) + end + new_inds, _ = _peakheights(pks.inds, pks.heights; minheight=min, maxheight=max) + _filter_fields!(pks, new_inds) + return pks +end +peakheights!(; kwargs...) = pks -> peakheights!(pks; kwargs...) \ No newline at end of file diff --git a/src/peakheight.jl b/src/peakheight.jl index a14d95d..8fb605b 100644 --- a/src/peakheight.jl +++ b/src/peakheight.jl @@ -23,11 +23,11 @@ julia> peakheights(xpks, vals; minheight=4.5) ([2], [5]) ``` """ -function peakheights( +function _peakheights( peaks::AbstractVector{Int}, heights::AbstractVector; minheight=nothing, maxheight=nothing ) - peakheights!(copy(peaks), copy(heights); minheight=minheight, maxheight=maxheight) + _peakheights!(copy(peaks), copy(heights); minheight=minheight, maxheight=maxheight) end """ @@ -54,7 +54,7 @@ julia> xpks, vals ([4, 7], [3, 4]) ``` """ -function peakheights!( +function _peakheights!( peaks::Vector{Int}, heights::AbstractVector{T}; minheight=nothing, maxheight=nothing ) where {T} diff --git a/src/peakprom.jl b/src/peakprom.jl index f89000c..3a5c30c 100644 --- a/src/peakprom.jl +++ b/src/peakprom.jl @@ -41,16 +41,16 @@ julia> peakproms(xpks, x; strict=false) ([2, 4, 7], Union{Missing, Int64}[5, 1, 3]) ``` """ -function peakproms(peaks::AbstractVector{Int}, x::AbstractVector{T}; +function _peakproms(peaks::AbstractVector{Int}, x::AbstractVector{T}; strict=true, minprom=nothing, maxprom=nothing -) where T +) where {T} if !isnothing(minprom) || !isnothing(maxprom) _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, minprom=minprom, maxprom=maxprom) end """ @@ -66,9 +66,9 @@ prominences. See also: [`peakproms`](@ref), [`findminima`](@ref), [`findmaxima`](@ref) """ -function peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; +function _peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; strict=true, minprom=nothing, maxprom=nothing -) where T +) where {T} if !isnothing(minprom) && !isnothing(maxprom) minprom < maxprom || throw(ArgumentError("minprom must be less than maxprom")) end @@ -91,9 +91,9 @@ function peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; Float64 <: T ? NaN : Float32 <: T ? NaN32 : Float16 <: T ? NaN16 : - missing + 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 +106,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)) @@ -140,15 +140,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]) diff --git a/src/peakwidth.jl b/src/peakwidth.jl index df0bee9..f133c6d 100644 --- a/src/peakwidth.jl +++ b/src/peakwidth.jl @@ -41,9 +41,9 @@ julia> peakwidths(xpks, x, [1]; strict=false) ([2], [1.0], [1.5], [2.5]) ``` """ -function peakwidths( +function _peakwidths( peaks::AbstractVector{Int}, x::AbstractVector, proms::AbstractVector; - strict=true, relheight=0.5, minwidth=nothing, maxwidth=nothing, + strict=true, relheight=0.5, minwidth=nothing, maxwidth=nothing ) if !isnothing(minwidth) || !isnothing(maxwidth) _peaks = copy(peaks) @@ -51,7 +51,7 @@ function peakwidths( # peaks will not be modified _peaks = peaks end - peakwidths!(_peaks, x, proms; strict=strict, relheight=relheight, + _peakwidths!(_peaks, x, proms; strict=strict, relheight=relheight, minwidth=minwidth, maxwidth=maxwidth) end @@ -69,10 +69,10 @@ Returns the modified peaks, widths, and the left and right edges at the referenc See also: [`peakwidths`](@ref), [`peakproms`](@ref), [`findminima`](@ref), [`findmaxima`](@ref) """ -function peakwidths!( +function _peakwidths!( peaks::AbstractVector{Int}, x::AbstractVector{T}, proms::AbstractVector{U}; - strict=true, relheight=0.5, minwidth=nothing, maxwidth=nothing, -) where {T, 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")) end @@ -89,7 +89,7 @@ function peakwidths!( cmp = pktype === :maxima ? (≤) : (≥) op = pktype === :maxima ? (-) : (+) - V1 = promote_type(T,U) + V1 = promote_type(T, U) _bad = Missing <: V1 ? missing : float(Int)(NaN) V = promote_type(V1, typeof(_bad)) @@ -109,22 +109,22 @@ 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]) + up = 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) + up1 = findprev(v -> !ismissing(v) && cmp(ht, v), x, up - 1) + up -= (ht - x[up]) / (x[up1] - x[up]) * (up - up1) 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(up) && (up -= (ht - x[up]) / (x[up-1] - x[up])) end redge[i] = something(up, lst) From 5cb66c34168f38ec28888b8f21463b4bf2579628 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Thu, 14 Sep 2023 19:42:38 +0200 Subject: [PATCH 04/63] Remove test code from Peaks.jl --- src/Peaks.jl | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/Peaks.jl b/src/Peaks.jl index 09d0c14..a4bb684 100644 --- a/src/Peaks.jl +++ b/src/Peaks.jl @@ -14,18 +14,4 @@ include("peakheight.jl") include("api_rework.jl") include("plot.jl") -function bla(x) - pks = findpeaks(x) - pks = peakproms!(pks, min=0.5) - pks = peakwidths!(pks) - pks = peakheights!(pks) - return pks -end - -function bla2(x) - pks = findpeaks(x) |> peakproms!(min=0.5) |> peakwidths!() |> peakheights!() - return pks -end - -export bla, bla2 end # module Peaks From 80b828098de5022f9980603a98ffc6f1bcaa1f62 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Thu, 14 Sep 2023 19:47:25 +0200 Subject: [PATCH 05/63] Update readme to temp state --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 55d0f53..6dd2be0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,16 @@ # Peaks.jl +## Temporary content +ToDo: +* Agree on API (exported functions, function names etc) +* Improve docstrings (Add examples, refs) +* Clean up internals (Remove move content of `_function` inside `function`) +* Optimize internals (Remove redundant allocations, make checks) +* Add/update tests +* Update readme +* Write docs + +## Old content below [![version](https://juliahub.com/docs/Peaks/version.svg)](https://juliahub.com/ui/Packages/Peaks/3TWUM) [![pkgeval](https://juliahub.com/docs/Peaks/pkgeval.svg)](https://juliahub.com/ui/Packages/Peaks/3TWUM) [![stable-docs](https://img.shields.io/badge/docs-stable-blue.svg)](https://halleysfifthinc.github.io/Peaks.jl/stable) From 7609635f1cc75388b9a488cf94620ef8e594b03d Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Thu, 14 Sep 2023 20:02:54 +0200 Subject: [PATCH 06/63] Add sentence about mutation to todo section --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6dd2be0..b82088f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # Peaks.jl -## Temporary content -ToDo: +## ToDo for api_rework * Agree on API (exported functions, function names etc) * Improve docstrings (Add examples, refs) * Clean up internals (Remove move content of `_function` inside `function`) @@ -10,6 +9,8 @@ ToDo: * Update readme * Write docs +For the last 2 points make it clear that the exclamation mark indicates that the vectors (values) of the tuple `pks` are mutated, but that the tuple itself is not, which is why you need to rebind the variable you bind to the tuple. Just use better words than I did here. + ## Old content below [![version](https://juliahub.com/docs/Peaks/version.svg)](https://juliahub.com/ui/Packages/Peaks/3TWUM) [![pkgeval](https://juliahub.com/docs/Peaks/pkgeval.svg)](https://juliahub.com/ui/Packages/Peaks/3TWUM) From 24e38925a39d30ed7cfc4ba52192f9a4b9b1a8fa Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Thu, 14 Sep 2023 20:04:24 +0200 Subject: [PATCH 07/63] add sentence about minima finding --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b82088f..072aa25 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Peaks.jl ## ToDo for api_rework -* Agree on API (exported functions, function names etc) +* Agree on API (exported functions, function names etc. Should Peaks.jl provide minima-finding functionality? Is it sufficient to ask the user to swap sign of the data?) * Improve docstrings (Add examples, refs) * Clean up internals (Remove move content of `_function` inside `function`) * Optimize internals (Remove redundant allocations, make checks) From aac8d5a8569c1b344544ce08b36c50848769ae84 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Fri, 22 Sep 2023 16:05:48 +0200 Subject: [PATCH 08/63] Remove temporary content from readme --- README.md | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/README.md b/README.md index 072aa25..74cfa7c 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,4 @@ # Peaks.jl - -## ToDo for api_rework -* Agree on API (exported functions, function names etc. Should Peaks.jl provide minima-finding functionality? Is it sufficient to ask the user to swap sign of the data?) -* Improve docstrings (Add examples, refs) -* Clean up internals (Remove move content of `_function` inside `function`) -* Optimize internals (Remove redundant allocations, make checks) -* Add/update tests -* Update readme -* Write docs - -For the last 2 points make it clear that the exclamation mark indicates that the vectors (values) of the tuple `pks` are mutated, but that the tuple itself is not, which is why you need to rebind the variable you bind to the tuple. Just use better words than I did here. - -## Old content below [![version](https://juliahub.com/docs/Peaks/version.svg)](https://juliahub.com/ui/Packages/Peaks/3TWUM) [![pkgeval](https://juliahub.com/docs/Peaks/pkgeval.svg)](https://juliahub.com/ui/Packages/Peaks/3TWUM) [![stable-docs](https://img.shields.io/badge/docs-stable-blue.svg)](https://halleysfifthinc.github.io/Peaks.jl/stable) From 9ccd30a24ae483abf6f2d59811d67e2ae51ae0ed Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Sun, 24 Sep 2023 08:57:06 +0200 Subject: [PATCH 09/63] make `findpeaks` a oneliner --- src/api_rework.jl | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/api_rework.jl b/src/api_rework.jl index 9fd8d7a..260a73c 100644 --- a/src/api_rework.jl +++ b/src/api_rework.jl @@ -10,11 +10,7 @@ in the field `inds`. This function serves as the entry-point for other functions such as `peakproms!` and `peakwidths!` """ -function findpeaks(x::AbstractVector) - data = x - inds, _ = findmaxima(x) - return (data=x, inds=inds) -end +findpeaks(x::AbstractVector) = (data=x, inds=findmaxima(x)[1]) """ _filter_fields!(pks, new_inds) From 4a0d9dcfb1e9fe081386988ae880101b1230b125 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Wed, 27 Sep 2023 19:45:53 +0200 Subject: [PATCH 10/63] Don't discard initially calculated heights --- src/api_rework.jl | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/api_rework.jl b/src/api_rework.jl index 260a73c..a016be1 100644 --- a/src/api_rework.jl +++ b/src/api_rework.jl @@ -10,7 +10,10 @@ in the field `inds`. This function serves as the entry-point for other functions such as `peakproms!` and `peakwidths!` """ -findpeaks(x::AbstractVector) = (data=x, inds=findmaxima(x)[1]) +function findpeaks(x::AbstractVector) + indices, heights = findmaxima(x) + return (data=x, indices=indices, heights=heights) +end """ _filter_fields!(pks, new_inds) @@ -19,9 +22,8 @@ Internal function to mutate the vectors in all fields of `pks` to remove any element with a corresponding `pks.inds` entry not present in `new_inds`. """ -function _filter_fields!(pks, new_inds) +function _filter_fields!(pks, mask) # Add checks on `pks` to see if it has fields "data" and "inds"? - mask = pks.inds .∉ Ref(new_inds) for field in filter(!=(:data), propertynames(pks)) # Do not want to mutate data vector v_to_be_mutated = getfield(pks, field) if length(v_to_be_mutated) != length(mask) @@ -109,8 +111,30 @@ function peakheights!(pks::NamedTuple; min=0, max=Inf) heights = pks.data[pks.inds] pks = merge(pks, (; heights)) end - new_inds, _ = _peakheights(pks.inds, pks.heights; minheight=min, maxheight=max) - _filter_fields!(pks, new_inds) + if !isnothing(min) || !isnothing(max) + lo = something(min, zero(eltype(x))) + up = something(max, typemax(Base.nonmissingtype(eltype(x)))) + matched = findall(x -> !ismissing(x) && !(lo ≤ x ≤ up), pks.proms) + _filter_fields!(pks, matched) + end return pks end -peakheights!(; kwargs...) = pks -> peakheights!(pks; kwargs...) \ No newline at end of file +peakheights!(; kwargs...) = pks -> peakheights!(pks; kwargs...) + + + +##==## +function _filter_fields!(pks, mask) + # Add checks on `pks` to see if it has fields "data" and "inds"? + for field in (:inds, :proms, :heights, :widths, :edges) # Only risk mutating fields added by this package + hasfield(pks, field) || continue # Do nothing if field is not present + v_to_be_mutated = getfield(pks, field) + if length(v_to_be_mutated) != length(mask) # Some performance overhead, but good test to have. Remove once tests are in place? + error("Internal error: The length of the vector in field `$field` is $(length(v_to_be_mutated)), but was expected to be $(length(mask))") + end + deleteat!(v_to_be_mutated, mask) + end + return nothing +end + + \ No newline at end of file From 0e20f16229af494c404595221e4793c74f468a23 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Wed, 27 Sep 2023 20:35:30 +0200 Subject: [PATCH 11/63] Export filterpeaks, improve internals in new API --- src/api_rework.jl | 108 +++++++++++++++++++++++----------------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/src/api_rework.jl b/src/api_rework.jl index a016be1..640ce35 100644 --- a/src/api_rework.jl +++ b/src/api_rework.jl @@ -1,38 +1,53 @@ # I am putting everything in here for now. The contents of this file should be moved around in the future. """ findpeaks(x) -> NamedTuple + findpeaks(x, w=1; strict=true) -> NamedTuple -Find the peaks in a vector `x`. +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 `inds`. +in the field `indices`. This function serves as the entry-point for other functions such as `peakproms!` and `peakwidths!` """ -function findpeaks(x::AbstractVector) - indices, heights = findmaxima(x) +function findpeaks(x::AbstractVector, w::Int=1; strict=true) + indices, heights = findmaxima(x, w; strict) return (data=x, indices=indices, heights=heights) end """ - _filter_fields!(pks, new_inds) + filterpeaks!(pks, mask) -> Nothing -Internal function to mutate the vectors in all fields of `pks` -to remove any element with a corresponding `pks.inds` entry -not present in `new_inds`. +Given a NamedTuple `pks` (as returned by `findpeaks`), filter +the peaks according to the given `mask` (A `BitVector` or `Vector{Bool}`. The functions `peakheights`, `peakproms` and `peakwidths` allow +filtering for a max/min limit of the relevant peak feature. +This function can be used to perform more complicated filtering, such +as keeping a peak if it has a certain height _or_ a certain width. + +# Examples: +ToDo: Make example """ -function _filter_fields!(pks, mask) - # Add checks on `pks` to see if it has fields "data" and "inds"? - for field in filter(!=(:data), propertynames(pks)) # Do not want to mutate data vector - v_to_be_mutated = getfield(pks, field) - if length(v_to_be_mutated) != length(mask) - error("Internal error: The length of the vector in field `$field` is $(length(v_to_be_mutated)), but was expected to be $(length(mask))") +function filterpeaks!(pks, mask::BitVector) + features_to_filter = (:indices, :proms, :heights, :widths, :edges) + + # Check lengths first to avoid a dimension mismatch + # after having filtered some features. + for field in features_to_filter + if length(mask) != length(getfield(pks, field)) + throw(DimensionMismatch("Length of `mask` is ($(length(mask))), but the length of `pks.$field` is $(length(getfield(pks, field))). This means that the given mask can not be used to filter the field `$field`.")) end - deleteat!(getfield(pks, field), mask) + end + + for field in features_to_filter # Only risk mutating fields added by this package + hasfield(pks, field) || continue # Do nothing if field is not present + v_to_be_mutated = getfield(pks, field) + deleteat!(v_to_be_mutated, mask) end return nothing end +export filterpeaks! """ peakproms!(pks::NamedTuple; min=0, max=Inf, strict=true) @@ -45,15 +60,19 @@ The prominences are returned in the field `:proms` of the returned named tuple. If the positional argument `pks` is omitted, an anonymus function is returned that performs the action (adding field `:proms` and filtering) to its input. """ -function peakproms!(pks::NamedTuple; min=0, max=Inf, strict=true) +function peakproms!(pks::NamedTuple; minprom=nothing, maxprom=nothing, min=minprom, max=maxprom, strict=true) 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.inds, pks.data) + _, proms = _peakproms(pks.indices, pks.data) pks = merge(pks, (; proms)) end - new_inds, _ = _peakproms(pks.inds, pks.data; minprom=min, maxprom=max, strict) - _filter_fields!(pks, new_inds) + if !isnothing(min) || !isnothing(max) + lo = something(min, zero(eltype(x))) + up = something(max, typemax(Base.nonmissingtype(eltype(x)))) + mask = findall(x -> !ismissing(x) && !(lo ≤ x ≤ up), pks.proms) + filterpeaks!(pks, mask) + end return pks end peakproms!(; kwargs...) = pks -> peakproms!(pks; kwargs...) @@ -74,7 +93,7 @@ that performs the action (adding fields `:widths` and `:edges` and filtering) to Note: If `pks` does not have a field `proms`, it is added. This is because it is needed to calculate the peak width. """ -function peakwidths!(pks::NamedTuple; min=0, max=Inf, relheight=0.5, strict=true) +function peakwidths!(pks::NamedTuple; minwidth=nothing, maxwidth=nothing, min=minwidth, max=maxwidth, relheight=0.5, strict=true) if !hasproperty(pks, :proms) # Add proms if needed pks = peakproms!(pks) end @@ -84,11 +103,15 @@ function peakwidths!(pks::NamedTuple; min=0, max=Inf, relheight=0.5, strict=true 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.inds, pks.data, pks.proms) + _, widths, leftedges, rightedges = _peakwidths(pks.indices, pks.data, pks.proms; relheight, strict) pks = merge(pks, (; widths, edges=collect(zip(leftedges, rightedges)))) end - new_inds, _ = _peakwidths(pks.inds, pks.data, pks.proms; minwidth=min, maxwidth=max, relheight, strict) - _filter_fields!(pks, new_inds) + if !isnothing(min) || !isnothing(max) + lo = something(min, zero(eltype(x))) + up = something(max, typemax(Base.nonmissingtype(eltype(x)))) + mask = findall(x -> !ismissing(x) && !(lo ≤ x ≤ up), pks.widths) + filterpeaks!(pks, mask) + end return pks end peakwidths!(; kwargs...) = pks -> peakwidths!(pks; kwargs...) @@ -99,42 +122,19 @@ peakwidths!(; kwargs...) = pks -> peakwidths!(pks; kwargs...) Find the heights of the peaks in `pks`, and filter out any peak with a heights smaller than `min` or greater than `max`. -The heights are returned in the field `:heights` of the returned named tuple. +Note that because the peaks returned by `findpeaks` already have +the feature `heights` calculated, this function is mainly useful to +filter peaks by a minimum and/or maximum height. -If the positional argument `pks` is omitted, an anonymus function is returned -that performs the action (adding field `heights` and filtering) to its input. +If the positional argument `pks` is omitted, an anonymus function is returned that performs the action (adding field `heights` and filtering) to its input. """ -function peakheights!(pks::NamedTuple; min=0, max=Inf) - if !hasproperty(pks, :heights) - # Avoid filtering by min/max here, so that it always happens outside if-statement. - # Pro: one less edge case. Con: More internal allocations - heights = pks.data[pks.inds] - pks = merge(pks, (; heights)) - end +function peakheights!(pks::NamedTuple; minheight=nothing, maxheight=nothing, min=minheight, max=maxheight) if !isnothing(min) || !isnothing(max) lo = something(min, zero(eltype(x))) up = something(max, typemax(Base.nonmissingtype(eltype(x)))) - matched = findall(x -> !ismissing(x) && !(lo ≤ x ≤ up), pks.proms) - _filter_fields!(pks, matched) + mask = findall(x -> !ismissing(x) && !(lo ≤ x ≤ up), pks.heights) + filterpeaks!(pks, mask) end return pks end -peakheights!(; kwargs...) = pks -> peakheights!(pks; kwargs...) - - - -##==## -function _filter_fields!(pks, mask) - # Add checks on `pks` to see if it has fields "data" and "inds"? - for field in (:inds, :proms, :heights, :widths, :edges) # Only risk mutating fields added by this package - hasfield(pks, field) || continue # Do nothing if field is not present - v_to_be_mutated = getfield(pks, field) - if length(v_to_be_mutated) != length(mask) # Some performance overhead, but good test to have. Remove once tests are in place? - error("Internal error: The length of the vector in field `$field` is $(length(v_to_be_mutated)), but was expected to be $(length(mask))") - end - deleteat!(v_to_be_mutated, mask) - end - return nothing -end - - \ No newline at end of file +peakheights!(; kwargs...) = pks -> peakheights!(pks; kwargs...) \ No newline at end of file From 369c0726dba764ba819f1d5b91f06fcd784ba80f Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Wed, 27 Sep 2023 20:38:18 +0200 Subject: [PATCH 12/63] Change linebreaks in "filterpeaks!" --- src/api_rework.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/api_rework.jl b/src/api_rework.jl index 640ce35..69c880f 100644 --- a/src/api_rework.jl +++ b/src/api_rework.jl @@ -36,7 +36,11 @@ function filterpeaks!(pks, mask::BitVector) # after having filtered some features. for field in features_to_filter if length(mask) != length(getfield(pks, field)) - throw(DimensionMismatch("Length of `mask` is ($(length(mask))), but the length of `pks.$field` is $(length(getfield(pks, field))). This means that the given mask can not be used to filter the field `$field`.")) + throw(DimensionMismatch( + "Length of `mask` is ($(length(mask))), but the length + of `pks.$field` is $(length(getfield(pks, field))). + This means that the given mask can not be used to filter + the field `$field`.")) end end From 080e9706f4db5705f8991cf92984d36f85e71053 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Wed, 27 Sep 2023 20:40:04 +0200 Subject: [PATCH 13/63] Make use of "strict" in all functions --- src/api_rework.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api_rework.jl b/src/api_rework.jl index 69c880f..2d37cbf 100644 --- a/src/api_rework.jl +++ b/src/api_rework.jl @@ -68,7 +68,7 @@ function peakproms!(pks::NamedTuple; minprom=nothing, maxprom=nothing, min=minpr 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) + _, proms = _peakproms(pks.indices, pks.data; strict) pks = merge(pks, (; proms)) end if !isnothing(min) || !isnothing(max) @@ -99,7 +99,7 @@ because it is needed to calculate the peak width. """ function peakwidths!(pks::NamedTuple; minwidth=nothing, maxwidth=nothing, min=minwidth, max=maxwidth, relheight=0.5, strict=true) if !hasproperty(pks, :proms) # Add proms if needed - pks = peakproms!(pks) + pks = peakproms!(pks; strict) end if xor(hasproperty(pks, :widths), hasproperty(pks, :edges)) throw(ArgumentError("The named tuple `pks` (first argument to `peakwidths!` is expected have both the fields `:widths` and `:edges`, or to have neither of them. The provided `pks` only has one of them.")) From 37839c8a75bdb4a61d009f40b653b2d0de73dcee Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Wed, 27 Sep 2023 21:51:58 +0200 Subject: [PATCH 14/63] Fix some bugs --- src/api_rework.jl | 23 ++++++++++++----------- src/peakwidth.jl | 5 ++--- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/api_rework.jl b/src/api_rework.jl index 2d37cbf..28b09bb 100644 --- a/src/api_rework.jl +++ b/src/api_rework.jl @@ -29,12 +29,13 @@ as keeping a peak if it has a certain height _or_ a certain width. # Examples: ToDo: Make example """ -function filterpeaks!(pks, mask::BitVector) +function filterpeaks!(pks, mask::Union{BitVector, Vector{Bool}}) features_to_filter = (:indices, :proms, :heights, :widths, :edges) # Check lengths first to avoid a dimension mismatch # after having filtered some features. for field in features_to_filter + hasproperty(pks, field) || continue # Do nothing if field is not present if length(mask) != length(getfield(pks, field)) throw(DimensionMismatch( "Length of `mask` is ($(length(mask))), but the length @@ -45,7 +46,7 @@ function filterpeaks!(pks, mask::BitVector) end for field in features_to_filter # Only risk mutating fields added by this package - hasfield(pks, field) || continue # Do nothing if field is not present + hasproperty(pks, field) || continue # Do nothing if field is not present v_to_be_mutated = getfield(pks, field) deleteat!(v_to_be_mutated, mask) end @@ -72,9 +73,9 @@ function peakproms!(pks::NamedTuple; minprom=nothing, maxprom=nothing, min=minpr pks = merge(pks, (; proms)) end if !isnothing(min) || !isnothing(max) - lo = something(min, zero(eltype(x))) - up = something(max, typemax(Base.nonmissingtype(eltype(x)))) - mask = findall(x -> !ismissing(x) && !(lo ≤ x ≤ up), pks.proms) + lo = something(min, zero(eltype(pks.data))) + up = something(max, typemax(Base.nonmissingtype(eltype(pks.data)))) + mask = map(x -> !ismissing(x) && !(lo ≤ x ≤ up), pks.proms) filterpeaks!(pks, mask) end return pks @@ -111,9 +112,9 @@ function peakwidths!(pks::NamedTuple; minwidth=nothing, maxwidth=nothing, min=mi pks = merge(pks, (; widths, edges=collect(zip(leftedges, rightedges)))) end if !isnothing(min) || !isnothing(max) - lo = something(min, zero(eltype(x))) - up = something(max, typemax(Base.nonmissingtype(eltype(x)))) - mask = findall(x -> !ismissing(x) && !(lo ≤ x ≤ up), pks.widths) + lo = something(min, zero(eltype(pks.data))) + up = something(max, typemax(Base.nonmissingtype(eltype(pks.data)))) + mask = map(x -> !ismissing(x) && !(lo ≤ x ≤ up), pks.widths) filterpeaks!(pks, mask) end return pks @@ -134,9 +135,9 @@ If the positional argument `pks` is omitted, an anonymus function is returned th """ function peakheights!(pks::NamedTuple; minheight=nothing, maxheight=nothing, min=minheight, max=maxheight) if !isnothing(min) || !isnothing(max) - lo = something(min, zero(eltype(x))) - up = something(max, typemax(Base.nonmissingtype(eltype(x)))) - mask = findall(x -> !ismissing(x) && !(lo ≤ x ≤ up), pks.heights) + lo = something(min, zero(eltype(pks.data))) + up = something(max, typemax(Base.nonmissingtype(eltype(pks.data)))) + mask = map(x -> !ismissing(x) && !(lo ≤ x ≤ up), pks.heights) filterpeaks!(pks, mask) end return pks diff --git a/src/peakwidth.jl b/src/peakwidth.jl index f133c6d..bd73a12 100644 --- a/src/peakwidth.jl +++ b/src/peakwidth.jl @@ -93,8 +93,8 @@ function _peakwidths!( _bad = Missing <: V1 ? missing : float(Int)(NaN) V = promote_type(V1, typeof(_bad)) - ledge = similar(proms, V) - redge = similar(proms, V) + ledge = similar(proms, typeof(one(V)/one(V))) # typeof(one(V)/one(V)) because the + redge = similar(proms, typeof(one(V)/one(V))) # vector eltype need to survive division if strict lst, fst = _bad, _bad @@ -126,7 +126,6 @@ function _peakwidths!( !isnothing(lo) && (lo += (ht - x[lo]) / (x[lo+1] - x[lo])) !isnothing(up) && (up -= (ht - x[up]) / (x[up-1] - x[up])) end - redge[i] = something(up, lst) ledge[i] = something(lo, fst) end From 6edefcf7805c7da7235a87b2ea38cdcf57ff6a6d Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Tue, 31 Oct 2023 13:11:49 +0100 Subject: [PATCH 15/63] export old functions --- src/api_rework.jl | 81 +++++++++----------------------- src/peakheight.jl | 65 +++++++++++++------------- src/peakprom.jl | 114 ++++++++++++++++++++++---------------------- src/peakwidth.jl | 117 +++++++++++++++++++++++----------------------- src/utils.jl | 64 +++++++++++++++++++++++++ 5 files changed, 235 insertions(+), 206 deletions(-) create mode 100644 src/utils.jl diff --git a/src/api_rework.jl b/src/api_rework.jl index 28b09bb..b008d5a 100644 --- a/src/api_rework.jl +++ b/src/api_rework.jl @@ -1,62 +1,14 @@ # I am putting everything in here for now. The contents of this file should be moved around in the future. -""" - 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 """ - filterpeaks!(pks, mask) -> Nothing + peakproms!(pks) --> NamedTuple + peakproms!() --> Function -Given a NamedTuple `pks` (as returned by `findpeaks`), filter -the peaks according to the given `mask` (A `BitVector` or `Vector{Bool}`. The functions `peakheights`, `peakproms` and `peakwidths` allow -filtering for a max/min limit of the relevant peak feature. -This function can be used to perform more complicated filtering, such -as keeping a peak if it has a certain height _or_ a certain width. - -# Examples: -ToDo: Make example -""" -function filterpeaks!(pks, mask::Union{BitVector, Vector{Bool}}) - features_to_filter = (:indices, :proms, :heights, :widths, :edges) - - # Check lengths first to avoid a dimension mismatch - # after having filtered some features. - for field in features_to_filter - hasproperty(pks, field) || continue # Do nothing if field is not present - if length(mask) != length(getfield(pks, field)) - throw(DimensionMismatch( - "Length of `mask` is ($(length(mask))), but the length - of `pks.$field` is $(length(getfield(pks, field))). - This means that the given mask can not be used to filter - the field `$field`.")) - end - end - - for field in features_to_filter # Only risk mutating fields added by this package - hasproperty(pks, field) || continue # Do nothing if field is not present - v_to_be_mutated = getfield(pks, field) - deleteat!(v_to_be_mutated, mask) - end - return nothing -end -export filterpeaks! - -""" - peakproms!(pks::NamedTuple; min=0, max=Inf, strict=true) - peakproms!(; min=0, max=Inf, strict=true) +# Optional keyword arguments +- `min`: Filter out any peak with a height smaller than `min` +- `max`: Filter out any peak with a height greater than `min` +- `relheight`: BLABLA. Defaults to `0.5` +- `strict`: BLABLA. Default to `true` Find the prominences of the peaks in `pks`, and filter out any peak with a prominence smaller than `min` or greater than `max`. @@ -82,10 +34,15 @@ function peakproms!(pks::NamedTuple; minprom=nothing, maxprom=nothing, min=minpr end peakproms!(; kwargs...) = pks -> peakproms!(pks; kwargs...) - """ - peakwidths!(pks::NamedTuple; min=0, max=Inf, relheight=0.5, strict=true) - peakwidths!(; min=0, max=Inf, relheight=0.5, strict=true) + peakwidths!(pks) --> NamedTuple + peakwidths!() --> Function + +# Optional keyword arguments +- `min`: Filter out any peak with a height smaller than `min` +- `max`: Filter out any peak with a height greater than `min` +- `relheight`: BLABLA. Defaults to `0.5` +- `strict`: BLABLA. Default to `true` Find the widths of the peaks in `pks`, and filter out any peak with a width smaller than `min` or greater than `max`. @@ -122,8 +79,12 @@ end peakwidths!(; kwargs...) = pks -> peakwidths!(pks; kwargs...) """ - peakheights!(pks::NamedTuple; min=0, max=Inf) - peakheights!(; min=0, max=Inf) + peakheights!(pks) --> NamedTuple + peakheights!() --> Function + +# Optional keyword arguments +- `min`: Filter out any peak with a height smaller than `min` +- `max`: Filter out any peak with a height greater than `min` Find the heights of the peaks in `pks`, and filter out any peak with a heights smaller than `min` or greater than `max`. diff --git a/src/peakheight.jl b/src/peakheight.jl index 8fb605b..816f141 100644 --- a/src/peakheight.jl +++ b/src/peakheight.jl @@ -1,11 +1,11 @@ """ - peakheights(peaks, heights; + peakheights!(peaks, heights; minheight=nothing, maxheight=nothing ) -> (peaks, heights) -Return a copy of `peaks` and `heights` where peak heights are removed if less than -`minheight` and/or greater than `maxheight`. +Modify and return `peaks` and `heights` by removing peaks that are less than `minheight` or greater +than `maxheight`. See also: [`peakprom`](@ref), [`peakwidths`](@ref), [`findmaxima`](@ref) @@ -16,28 +16,38 @@ julia> x = [0,5,2,3,3,1,4,0]; julia> xpks, vals = findmaxima(x) ([2, 4, 7], [5, 3, 4]) -julia> peakheights(xpks, vals; maxheight=4) -([4, 7], [3, 4]) +julia> peakheights!(xpks, vals; maxheight=4); -julia> peakheights(xpks, vals; minheight=4.5) -([2], [5]) +julia> xpks, vals +([4, 7], [3, 4]) ``` """ -function _peakheights( - peaks::AbstractVector{Int}, heights::AbstractVector; +function peakheights!( + peaks::Vector{Int}, heights::AbstractVector{T}; minheight=nothing, maxheight=nothing -) - _peakheights!(copy(peaks), copy(heights); minheight=minheight, maxheight=maxheight) +) where {T} + 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) + deleteat!(peaks, matched) + deleteat!(heights, matched) + end + + return peaks, heights end +export peakheights! + """ - peakheights!(peaks, heights; + peakheights(peaks, heights; minheight=nothing, maxheight=nothing ) -> (peaks, heights) -Modify and return `peaks` and `heights` by removing peaks that are less than `minheight` or greater -than `maxheight`. +Return a copy of `peaks` and `heights` where peak heights are removed if less than +`minheight` and/or greater than `maxheight`. See also: [`peakprom`](@ref), [`peakwidths`](@ref), [`findmaxima`](@ref) @@ -48,26 +58,17 @@ julia> x = [0,5,2,3,3,1,4,0]; julia> xpks, vals = findmaxima(x) ([2, 4, 7], [5, 3, 4]) -julia> peakheights!(xpks, vals; maxheight=4); - -julia> xpks, vals +julia> peakheights(xpks, vals; maxheight=4) ([4, 7], [3, 4]) + +julia> peakheights(xpks, vals; minheight=4.5) +([2], [5]) ``` """ -function _peakheights!( - peaks::Vector{Int}, heights::AbstractVector{T}; +function peakheights( + peaks::AbstractVector{Int}, heights::AbstractVector; minheight=nothing, maxheight=nothing -) where {T} - 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) - deleteat!(peaks, matched) - deleteat!(heights, matched) - end - - return peaks, heights +) + peakheights!(copy(peaks), copy(heights); minheight=minheight, maxheight=maxheight) end - - +export peakheights \ No newline at end of file diff --git a/src/peakprom.jl b/src/peakprom.jl index 3a5c30c..9ee4db5 100644 --- a/src/peakprom.jl +++ b/src/peakprom.jl @@ -1,58 +1,3 @@ -""" - peakproms(peaks, x; - strict=true, - minprom=nothing, - maxprom=nothing - ) -> (peaks, proms) - -Calculate the prominences of `peaks` in `x`, and removing peaks with prominences less than -`minprom` and/or greater than `maxprom`. - -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. - -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`. - -See also: [`findminima`](@ref), [`findmaxima`](@ref), [`peakproms!`](@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> x = [missing,5,2,3,3,1,4,0]; - -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]) -``` -""" -function _peakproms(peaks::AbstractVector{Int}, x::AbstractVector{T}; - strict=true, minprom=nothing, maxprom=nothing -) where {T} - if !isnothing(minprom) || !isnothing(maxprom) - _peaks = copy(peaks) - else - # peaks will not be modified - _peaks = peaks - end - return _peakproms!(_peaks, x; strict=strict, minprom=minprom, maxprom=maxprom) -end - """ peakproms!(peaks, x; strict=true, @@ -66,7 +11,7 @@ prominences. See also: [`peakproms`](@ref), [`findminima`](@ref), [`findmaxima`](@ref) """ -function _peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; +function peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; strict=true, minprom=nothing, maxprom=nothing ) where {T} if !isnothing(minprom) && !isnothing(maxprom) @@ -179,4 +124,61 @@ function _peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; return peaks, proms end +export peakproms! + +""" + peakproms(peaks, x; + strict=true, + minprom=nothing, + maxprom=nothing + ) -> (peaks, proms) + +Calculate the prominences of `peaks` in `x`, and removing peaks with prominences less than +`minprom` and/or greater than `maxprom`. + +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. + +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`. + +See also: [`findminima`](@ref), [`findmaxima`](@ref), [`peakproms!`](@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> x = [missing,5,2,3,3,1,4,0]; + +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]) +``` +""" +function peakproms(peaks::AbstractVector{Int}, x::AbstractVector{T}; + strict=true, minprom=nothing, maxprom=nothing +) where {T} + if !isnothing(minprom) || !isnothing(maxprom) + _peaks = copy(peaks) + else + # peaks will not be modified + _peaks = peaks + end + return peakproms!(_peaks, x; strict=strict, minprom=minprom, maxprom=maxprom) +end +export peakproms diff --git a/src/peakwidth.jl b/src/peakwidth.jl index bd73a12..38a1e19 100644 --- a/src/peakwidth.jl +++ b/src/peakwidth.jl @@ -1,60 +1,3 @@ -""" - 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`, 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. - -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. - -See also: [`peakprom`](@ref), [`findminima`](@ref), [`findmaxima`](@ref) - -# Examples -```jldoctest -julia> x = [0,1,0,-1.]; - -julia> xpks = argmaxima(x) -1-element Vector{Int64}: - 2 - -julia> peakwidths(xpks, x, [1]) -([2], [1.0], [1.5], [2.5]) - -julia> x[3] = NaN; - -julia> peakwidths(xpks, x, [1]) -([2], [NaN], [1.5], [NaN]) - -julia> peakwidths(xpks, x, [1]; strict=false) -([2], [1.0], [1.5], [2.5]) -``` -""" -function _peakwidths( - peaks::AbstractVector{Int}, x::AbstractVector, proms::AbstractVector; - strict=true, relheight=0.5, minwidth=nothing, maxwidth=nothing -) - if !isnothing(minwidth) || !isnothing(maxwidth) - _peaks = copy(peaks) - else - # peaks will not be modified - _peaks = peaks - end - _peakwidths!(_peaks, x, proms; strict=strict, relheight=relheight, - minwidth=minwidth, maxwidth=maxwidth) -end - """ peakwidths!(peaks, x, proms; strict=true, @@ -69,7 +12,7 @@ Returns the modified peaks, widths, and the left and right edges at the referenc See also: [`peakwidths`](@ref), [`peakproms`](@ref), [`findminima`](@ref), [`findmaxima`](@ref) """ -function _peakwidths!( +function peakwidths!( peaks::AbstractVector{Int}, x::AbstractVector{T}, proms::AbstractVector{U}; strict=true, relheight=0.5, minwidth=nothing, maxwidth=nothing ) where {T,U} @@ -145,4 +88,62 @@ function _peakwidths!( return peaks, widths, ledge, redge end +export peakwidths! +""" + 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`, 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. + +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. + +See also: [`peakprom`](@ref), [`findminima`](@ref), [`findmaxima`](@ref) + +# Examples +```jldoctest +julia> x = [0,1,0,-1.]; + +julia> xpks = argmaxima(x) +1-element Vector{Int64}: + 2 + +julia> peakwidths(xpks, x, [1]) +([2], [1.0], [1.5], [2.5]) + +julia> x[3] = NaN; + +julia> peakwidths(xpks, x, [1]) +([2], [NaN], [1.5], [NaN]) + +julia> peakwidths(xpks, x, [1]; strict=false) +([2], [1.0], [1.5], [2.5]) +``` +""" +function peakwidths( + peaks::AbstractVector{Int}, x::AbstractVector, proms::AbstractVector; + strict=true, relheight=0.5, minwidth=nothing, maxwidth=nothing +) + if !isnothing(minwidth) || !isnothing(maxwidth) + _peaks = copy(peaks) + else + # peaks will not be modified + _peaks = peaks + end + peakwidths!(_peaks, x, proms; strict=strict, relheight=relheight, + minwidth=minwidth, maxwidth=maxwidth) +end +export peakwidths \ No newline at end of file diff --git a/src/utils.jl b/src/utils.jl new file mode 100644 index 0000000..b254340 --- /dev/null +++ b/src/utils.jl @@ -0,0 +1,64 @@ +""" + filterpeaks!(pks, mask) -> pks + +Given a NamedTuple `pks` (as returned by `findpeaks`), filter +the peaks according to the given `mask` (A `BitVector` or `Vector{Bool}`. +This means if element `i` of `mask` is false, then the peak +at index `i` will be removed from `pks`. + +The functions `peakheights`, `peakproms` and `peakwidths` already +allow filtering by maximal and minimal values for different peak features. +This function can be used to perform more complicated filtering, such +as keeping a peak if it has a certain height _or_ a certain width. + +# Examples +ToDo: Make examples +""" +function filterpeaks!(pks::NamedTuple, mask::Union{BitVector, Vector{Bool}}) + features_to_filter = (:indices, :proms, :heights, :widths, :edges) + + # Check lengths first to avoid a dimension mismatch + # after having filtered some features. + for field in features_to_filter + hasproperty(pks, field) || continue # Do nothing if field is not present + if length(mask) != length(getfield(pks, field)) + throw(DimensionMismatch( + "Length of `mask` is ($(length(mask))), but the length + of `pks.$field` is $(length(getfield(pks, field))). + This means that the given mask can not be used to filter + the field `$field`.")) + end + end + + for field in features_to_filter # Only risk mutating fields added by this package + hasproperty(pks, field) || continue # Do nothing if field is not present + v_to_be_mutated = getfield(pks, field) + deleteat!(v_to_be_mutated, mask) + end + return nothing +end +export filterpeaks! + + + +#==================================================================== +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 +=# \ No newline at end of file From 169e5dd5b4f677b2e03df4d77345d173e5691f40 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Tue, 31 Oct 2023 13:40:48 +0100 Subject: [PATCH 16/63] improvements, polish docstrings --- src/Peaks.jl | 1 + src/api_rework.jl | 98 +++++++++++++++++++++++++++++--------------- src/minmax.jl | 7 ++-- src/utils.jl | 11 ++++- test/manual_tests.jl | 7 ++++ 5 files changed, 88 insertions(+), 36 deletions(-) create mode 100644 test/manual_tests.jl diff --git a/src/Peaks.jl b/src/Peaks.jl index a4bb684..e694b38 100644 --- a/src/Peaks.jl +++ b/src/Peaks.jl @@ -8,6 +8,7 @@ using Compat export findpeaks, peakproms!, peakwidths!, peakheights! include("minmax.jl") +include("utils.jl") include("peakprom.jl") include("peakwidth.jl") include("peakheight.jl") diff --git a/src/api_rework.jl b/src/api_rework.jl index b008d5a..c94ccfa 100644 --- a/src/api_rework.jl +++ b/src/api_rework.jl @@ -5,10 +5,9 @@ peakproms!() --> Function # Optional keyword arguments -- `min`: Filter out any peak with a height smaller than `min` -- `max`: Filter out any peak with a height greater than `min` -- `relheight`: BLABLA. Defaults to `0.5` -- `strict`: BLABLA. Default to `true` +- `min`: Filter out any peak with a height smaller than `min`. +- `max`: Filter out any peak with a height greater than `min`. +- `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. Find the prominences of the peaks in `pks`, and filter out any peak with a prominence smaller than `min` or greater than `max`. @@ -16,33 +15,44 @@ The prominences are returned in the field `:proms` of the returned named tuple. If the positional argument `pks` is omitted, an anonymus function is returned that performs the action (adding field `:proms` and filtering) to its input. +This means that `peakproms!(pks)` is equivalent to `pks|>peakproms!`. """ function peakproms!(pks::NamedTuple; minprom=nothing, maxprom=nothing, min=minprom, max=maxprom, strict=true) 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) + _, proms = peakproms(pks.indices, pks.data; strict) pks = merge(pks, (; proms)) end - if !isnothing(min) || !isnothing(max) - lo = something(min, zero(eltype(pks.data))) - up = something(max, typemax(Base.nonmissingtype(eltype(pks.data)))) - mask = map(x -> !ismissing(x) && !(lo ≤ x ≤ up), pks.proms) - filterpeaks!(pks, mask) - end + filterpeaks!(pks, min, max, :proms) return pks end peakproms!(; kwargs...) = pks -> peakproms!(pks; kwargs...) +""" + peakproms(pks) --> NamedTuple + peakproms() --> Function + +# Optional keyword arguments +- `min`: Filter out any peak with a height smaller than `min`. +- `max`: Filter out any peak with a height greater than `min`. +- `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. + +Non-mutation version of `peakproms!`. Note that this copies all vectors +in `pks`, meaning that it is less performant. +See the docstring for `peakproms!` for more information. +""" +peakproms(pks::NamedTuple; kwargs...) = peakproms!(deepcopy(pks); kwargs...) + """ peakwidths!(pks) --> NamedTuple peakwidths!() --> Function # Optional keyword arguments -- `min`: Filter out any peak with a height smaller than `min` -- `max`: Filter out any peak with a height greater than `min` -- `relheight`: BLABLA. Defaults to `0.5` -- `strict`: BLABLA. Default to `true` +- `min`: Filter out any peak with a height smaller than `min`. +- `max`: Filter out any peak with a height greater than `min`. +- `relheight`: How far up to measure width, as fraction of the peak prominence. Defaults to `0.5`. +- `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. Find the widths of the peaks in `pks`, and filter out any peak with a width smaller than `min` or greater than `max`. @@ -51,6 +61,7 @@ The edges of the peaks are also added in the field `:edges`. If the positional argument `pks` is omitted, an anonymus function is returned that performs the action (adding fields `:widths` and `:edges` and filtering) to its input. +This means that `peakwidths!(pks)` is equivalent to `pks|>peakwidths!`. Note: If `pks` does not have a field `proms`, it is added. This is because it is needed to calculate the peak width. @@ -65,26 +76,38 @@ function peakwidths!(pks::NamedTuple; minwidth=nothing, maxwidth=nothing, min=mi 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) + _, widths, leftedges, rightedges = peakwidths(pks.indices, pks.data, pks.proms; relheight, strict) pks = merge(pks, (; widths, edges=collect(zip(leftedges, rightedges)))) end - if !isnothing(min) || !isnothing(max) - lo = something(min, zero(eltype(pks.data))) - up = something(max, typemax(Base.nonmissingtype(eltype(pks.data)))) - mask = map(x -> !ismissing(x) && !(lo ≤ x ≤ up), pks.widths) - filterpeaks!(pks, mask) - end + filterpeaks!(pks, min, max, :widths) return pks end peakwidths!(; kwargs...) = pks -> peakwidths!(pks; kwargs...) +""" + peakwidths(pks) --> NamedTuple + peakwidths() --> Function + +# Optional keyword arguments +- `min`: Filter out any peak with a height smaller than `min`. +- `max`: Filter out any peak with a height greater than `min`. +- `relheight`: How far up to measure width, as fraction of the peak prominence. Defaults to `0.5`. +- `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. + +Non-mutation version of `peakwidths!`. Note that this copies all vectors +in `pks`, meaning that it is less performant. +See the docstring for `peakwidths!` for more information. +""" +peakwidths(pks::NamedTuple; kwargs...) = peakwidths!(deepcopy(pks); kwargs...) + + """ peakheights!(pks) --> NamedTuple peakheights!() --> Function # Optional keyword arguments -- `min`: Filter out any peak with a height smaller than `min` -- `max`: Filter out any peak with a height greater than `min` +- `min`: Filter out any peak with a height smaller than `min`. +- `max`: Filter out any peak with a height greater than `min`. Find the heights of the peaks in `pks`, and filter out any peak with a heights smaller than `min` or greater than `max`. @@ -92,15 +115,26 @@ Note that because the peaks returned by `findpeaks` already have the feature `heights` calculated, this function is mainly useful to filter peaks by a minimum and/or maximum height. -If the positional argument `pks` is omitted, an anonymus function is returned that performs the action (adding field `heights` and filtering) to its input. +If the positional argument `pks` is omitted, an anonymus function is returned +that performs the action (adding field `heights` and filtering) to its input. +This means that `peakheights!(pks)` is equivalent to `pks|>peakheights!`. """ function peakheights!(pks::NamedTuple; minheight=nothing, maxheight=nothing, min=minheight, max=maxheight) - if !isnothing(min) || !isnothing(max) - lo = something(min, zero(eltype(pks.data))) - up = something(max, typemax(Base.nonmissingtype(eltype(pks.data)))) - mask = map(x -> !ismissing(x) && !(lo ≤ x ≤ up), pks.heights) - filterpeaks!(pks, mask) - end + filterpeaks!(pks, min, max, :heights) return pks end -peakheights!(; kwargs...) = pks -> peakheights!(pks; kwargs...) \ No newline at end of file +peakheights!(; kwargs...) = pks -> peakheights!(pks; kwargs...) + +""" + peakheights(pks) --> NamedTuple + peakheights() --> Function + +# Optional keyword arguments +- `min`: Filter out any peak with a height smaller than `min`. +- `max`: Filter out any peak with a height greater than `min`. + +Non-mutation version of `peakheights!`. Note that this copies all vectors +in `pks`, meaning that it is less performant. +See the docstring for `peakheights!` for more information. +""" +peakheights(pks::NamedTuple; kwargs...) = peakheights!(deepcopy(pks); kwargs...) diff --git a/src/minmax.jl b/src/minmax.jl index 295bd6f..5286952 100644 --- a/src/minmax.jl +++ b/src/minmax.jl @@ -198,8 +198,9 @@ See also: [`argmaxima`](@ref), [`findnextmaxima`](@ref) """ 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 +export findmaxima """ findnextminima(x, i[, w=1, strict=true]) -> Int @@ -325,6 +326,6 @@ See also: [`argminima`](@ref), [`findnextminima`](@ref) """ 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 - +export findminima diff --git a/src/utils.jl b/src/utils.jl index b254340..ec3aa82 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -39,7 +39,16 @@ function filterpeaks!(pks::NamedTuple, mask::Union{BitVector, Vector{Bool}}) end export filterpeaks! - +# This method gets no docstring, as it is intended for internal use. +function filterpeaks!(pks::NamedTuple, min, max, feature::Symbol) + if !isnothing(min) || !isnothing(max) + lo = something(min, zero(eltype(pks.data))) + up = something(max, typemax(Base.nonmissingtype(eltype(pks.data)))) + mask = map(x -> !ismissing(x) && !(lo ≤ x ≤ up), getproperty(pks, feature)) + filterpeaks!(pks, mask) + end + return nothing +end #==================================================================== We store a version of findpeak here, as it might be implemented soon, diff --git a/test/manual_tests.jl b/test/manual_tests.jl new file mode 100644 index 0000000..94a427c --- /dev/null +++ b/test/manual_tests.jl @@ -0,0 +1,7 @@ +# 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) \ No newline at end of file From 67cea1b95d3da15bbd36592cdcfffec278be28e0 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Tue, 31 Oct 2023 13:41:35 +0100 Subject: [PATCH 17/63] tag breaking version --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index ade018a..1d733fa 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Peaks" uuid = "18e31ff7-3703-566c-8e60-38913d67486b" authors = ["Allen Hill "] -version = "0.4.4" +version = "0.5.0" [deps] Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" From 310fbfe625edbc6f7d64c6e206d668c8d9738d7f Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Tue, 31 Oct 2023 14:07:21 +0100 Subject: [PATCH 18/63] misc changes --- src/api_rework.jl | 27 ++++++++++++++++++--------- src/minmax.jl | 9 ++++++++- test/manual_tests.jl | 19 ++++++++++++++++++- 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/api_rework.jl b/src/api_rework.jl index c94ccfa..1ec1e82 100644 --- a/src/api_rework.jl +++ b/src/api_rework.jl @@ -13,9 +13,14 @@ Find the prominences of the peaks in `pks`, and filter out any peak with a prominence smaller than `min` or greater than `max`. The prominences are returned in the field `:proms` of the returned named tuple. -If the positional argument `pks` is omitted, an anonymus function is returned -that performs the action (adding field `:proms` and filtering) to its input. -This means that `peakproms!(pks)` is equivalent to `pks|>peakproms!`. +If the positional argument `pks` is omitted, a function is returned such +that `peakproms!(pks)` is equivalent to `pks|>peakproms!`. + +Note: This function mutates the vectors stored in the NamedTuple `pks`, +and not the named tuple itself. + +# Examples +Write examples """ function peakproms!(pks::NamedTuple; minprom=nothing, maxprom=nothing, min=minprom, max=maxprom, strict=true) if !hasproperty(pks, :proms) @@ -59,12 +64,17 @@ with a width smaller than `min` or greater than `max`. The widths are returned in the field `:widths` of the returned named tuple. The edges of the peaks are also added in the field `:edges`. -If the positional argument `pks` is omitted, an anonymus function is returned -that performs the action (adding fields `:widths` and `:edges` and filtering) to its input. -This means that `peakwidths!(pks)` is equivalent to `pks|>peakwidths!`. +If the positional argument `pks` is omitted, a function is returned such +that `peakwidths!(pks)` is equivalent to `pks|>peakwidths!`. Note: If `pks` does not have a field `proms`, it is added. This is because it is needed to calculate the peak width. + +Note: This function mutates the vectors stored in the NamedTuple `pks`, +and not the named tuple itself. + +# Examples +Write examples """ function peakwidths!(pks::NamedTuple; minwidth=nothing, maxwidth=nothing, min=minwidth, max=maxwidth, relheight=0.5, strict=true) if !hasproperty(pks, :proms) # Add proms if needed @@ -115,9 +125,8 @@ Note that because the peaks returned by `findpeaks` already have the feature `heights` calculated, this function is mainly useful to filter peaks by a minimum and/or maximum height. -If the positional argument `pks` is omitted, an anonymus function is returned -that performs the action (adding field `heights` and filtering) to its input. -This means that `peakheights!(pks)` is equivalent to `pks|>peakheights!`. +If the positional argument `pks` is omitted, a function is returned such +that `peakheights!(pks)` is equivalent to `pks|>peakheights!`. """ function peakheights!(pks::NamedTuple; minheight=nothing, maxheight=nothing, min=minheight, max=maxheight) filterpeaks!(pks, min, max, :heights) diff --git a/src/minmax.jl b/src/minmax.jl index 5286952..b569fab 100644 --- a/src/minmax.jl +++ b/src/minmax.jl @@ -104,6 +104,7 @@ julia> findnextmaxima([0,2,0,1,1,0], 3) ``` """ findnextmaxima(x, i, w=1; strict=true) = findnextextrema(<, x, i, w, strict) +export findnextmaxima """ ismaxima(i, x[, w=1; strict=true]) -> Bool @@ -117,6 +118,7 @@ are bounded by lesser values immediately before and after the consecutive maxima See also: [`findnextmaxima`](@ref) """ ismaxima(i, x, w=1; strict=true)::Bool = findnextextrema(<, x, i, w, strict) === i +export ismaxima """ argmaxima(x[, w=1; strict=true]) -> Vector{Int} @@ -166,6 +168,7 @@ function argmaxima( return pks end +export argmaxima """ maxima(x[, w=1; strict=true]) -> Vector{eltype(x)} @@ -184,6 +187,7 @@ function maxima( idxs = argmaxima(x, w; strict=strict) return x[idxs] end +export maxima """ findmaxima(x[, w=1; strict=true]) -> (idxs, vals) @@ -231,7 +235,7 @@ julia> findnextminima([3,2,3,1,1,3], 3) ``` """ findnextminima(x, i, w=1; strict=true) = findnextextrema(>, x, i, w, strict) - +export findnextminima """ isminima(i, x[, w=1; strict=true]) -> Bool @@ -244,6 +248,7 @@ are bounded by greater values immediately before and after the consecutive minim See also: [`findnextminima`](@ref) """ isminima(i, x, w=1; strict=true)::Bool = findnextextrema(>, x, i, w, strict) === i +export isminima """ argminima(x[, w=1; strict=false]) -> Vector{Int} @@ -294,6 +299,7 @@ function argminima( return pks end +export argminima """ minima(x[, w=1; strict=true]) -> Vector{eltype(x)} @@ -312,6 +318,7 @@ function minima( idxs = argminima(x, w; strict=strict) return x[idxs] end +export minima """ findminima(x[, w=1; strict=true]) -> (idxs, vals) diff --git a/test/manual_tests.jl b/test/manual_tests.jl index 94a427c..c98e98a 100644 --- a/test/manual_tests.jl +++ b/test/manual_tests.jl @@ -4,4 +4,21 @@ data = [1, 2, 3, 4, 5, 4, 3, 2, 1, 6, 1] pks = findmaxima(data) pks = peakproms!(pks) -pks = peakwidths!(pks) \ No newline at end of file +pks = peakwidths!(pks) + +## Below is code intended to generate docstring examples +data = [1, 5, 1, 3, 2] +pks = findmaxima(data) +pks = peakproms!(pks) +pks = peakwidths!(pks) + +## +run_tests = true +if run_tests + using Pkg + pkg_path = joinpath(homedir(), ".julia", "dev", "Peaks") + Pkg.activate("PeaksTestEnv"; shared=true) + Pkg.develop(path=pkg_path) + Pkg.add(["OffsetArrays", "Plots"]) + include(joinpath(pkg_path, "test", "runtests.jl")) +end \ No newline at end of file From 82c5b8e4cfbd7183ef571c16f280704ee22562d3 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Tue, 31 Oct 2023 14:21:35 +0100 Subject: [PATCH 19/63] comment out bad test --- test/manual_tests.jl | 49 +++++++++++++++++++++++++++++++++++++++++++- test/peakwidth.jl | 7 +++++-- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/test/manual_tests.jl b/test/manual_tests.jl index c98e98a..f265f90 100644 --- a/test/manual_tests.jl +++ b/test/manual_tests.jl @@ -21,4 +21,51 @@ if run_tests Pkg.develop(path=pkg_path) Pkg.add(["OffsetArrays", "Plots"]) include(joinpath(pkg_path, "test", "runtests.jl")) -end \ No newline at end of file +end + +## +begin + fs = 100 + T = 1/fs + t = 0:T:6pi+T + sint = sin.(t) + + begin + sinpks = argmaxima(sint) + _, widths, _, _ = peakwidths(sinpks, sint, sint[sinpks]; strict=false, relheight=1) + @assert isapprox.(widths, fill(pi*100, length(sinpks)), atol=.01)|>all + + sinpks = argminima(sint) + _, widths, _, _ = peakwidths(sinpks, sint, abs.(sint[sinpks]); strict=false, relheight=1) + @assert isapprox.(widths, fill(pi*100, length(sinpks)), atol=.01)|>all + end + + _, widths, _, _ = peakwidths([2], [0.,1.,0.], [1.]) + @assert widths == [1.] + _, widths, _, _ = peakwidths([2], [0.,1.,0.], [NaN]) + @assert widths[1] === NaN + _, widths, _, _ = peakwidths([2], [0.,1.,0.], [missing]) + @assert widths[1] === missing + _, widths, _, _ = peakwidths([2], [0.,1.,NaN], [1.]; strict=true) + @assert widths[1] === NaN + _, widths, _, _ = peakwidths([2], [0.,1.,0.,-1.], [1.]; strict=false) + _, widthsnan, _, _ = peakwidths([2], [0.,1.,NaN,-1.], [1.]; strict=false) + @assert widths == widthsnan + + begin + sinpks = argmaxima(sint) + _, widths, _, _ = peakwidths(sinpks, sint, ones(length(sinpks)); strict=true, relheight=1) + @assert first(widths) === NaN + _, widths, _, _ = peakwidths(sinpks, sint, ones(length(sinpks)); strict=false, relheight=1) + @assert first(widths) !== NaN + end + + begin + sinpks = argmaxima(sint) + _, proms = peakproms(sinpks, sint) + + @assert length(first(peakwidths(sinpks, sint, proms; minwidth=pi*75))) == 2 + @assert length(first(peakwidths(sinpks, sint, proms; maxwidth=pi*75))) == 1 + end + +end diff --git a/test/peakwidth.jl b/test/peakwidth.jl index 3a2fa93..d98e20e 100644 --- a/test/peakwidth.jl +++ b/test/peakwidth.jl @@ -18,8 +18,11 @@ @test widths == [1.] _, widths, _, _ = peakwidths([2], [0.,1.,0.], [NaN]) @test widths[1] === NaN - _, widths, _, _ = peakwidths([2], [0.,1.,0.], [missing]) - @test widths[1] === missing + # the line below errors with the error: + # "ERROR: MethodError: Cannot `convert` an object of type Missing to an object of type Float64" + #_, widths, _, _ = peakwidths([2], [0.,1.,0.], [missing]) + #@test widths[1] === missing + @test false # to flag commented-out test _, widths, _, _ = peakwidths([2], [0.,1.,NaN], [1.]; strict=true) @test widths[1] === NaN _, widths, _, _ = peakwidths([2], [0.,1.,0.,-1.], [1.]; strict=false) From 639f212ada5c09121791ec598f07c6cb1b79c498 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Tue, 31 Oct 2023 14:52:06 +0100 Subject: [PATCH 20/63] minor changes --- src/api_rework.jl | 73 +++++++++++++++++++++++++++++++++++--------- src/minmax.jl | 32 +++++++++++++++---- src/peakwidth.jl | 7 +++-- test/manual_tests.jl | 70 +++++++++++++----------------------------- 4 files changed, 110 insertions(+), 72 deletions(-) diff --git a/src/api_rework.jl b/src/api_rework.jl index 1ec1e82..ea396cf 100644 --- a/src/api_rework.jl +++ b/src/api_rework.jl @@ -14,13 +14,25 @@ with a prominence smaller than `min` or greater than `max`. The prominences are returned in the field `:proms` of the returned named tuple. If the positional argument `pks` is omitted, a function is returned such -that `peakproms!(pks)` is equivalent to `pks|>peakproms!`. +that `peakproms!(pks)` is equivalent to `pks |> peakproms!`. Note: This function mutates the vectors stored in the NamedTuple `pks`, and not the named tuple itself. +See also: [`peakwidths!`](@ref), [`peakheights!`](@ref) + # Examples -Write examples +```jldoctest +julia> data = [1, 5, 1, 3, 2]; + +julia> pks = findmaxima(data); + +julia> pks = peakproms!(pks) +(indices = [2, 4], heights = [5, 3], data = [1, 5, 1, 3, 2], proms = Union{Missing, Int64}[4, 1]) + +julia> data |> findmaxima |> peakproms! +(indices = [2, 4], heights = [5, 3], data = [1, 5, 1, 3, 2], proms = Union{Missing, Int64}[4, 1]) +``` """ function peakproms!(pks::NamedTuple; minprom=nothing, maxprom=nothing, min=minprom, max=maxprom, strict=true) if !hasproperty(pks, :proms) @@ -43,9 +55,10 @@ peakproms!(; kwargs...) = pks -> peakproms!(pks; kwargs...) - `max`: Filter out any peak with a height greater than `min`. - `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. -Non-mutation version of `peakproms!`. Note that this copies all vectors -in `pks`, meaning that it is less performant. -See the docstring for `peakproms!` for more information. +Non-mutation version of `peakproms!`. Note that +this copies all vectors in `pks`, meaning that +it is less performant. See the docstring for +`peakproms!` for more information. """ peakproms(pks::NamedTuple; kwargs...) = peakproms!(deepcopy(pks); kwargs...) @@ -65,7 +78,7 @@ The widths are returned in the field `:widths` of the returned named tuple. The edges of the peaks are also added in the field `:edges`. If the positional argument `pks` is omitted, a function is returned such -that `peakwidths!(pks)` is equivalent to `pks|>peakwidths!`. +that `peakwidths!(pks)` is equivalent to `pks |> peakwidths!`. Note: If `pks` does not have a field `proms`, it is added. This is because it is needed to calculate the peak width. @@ -73,8 +86,20 @@ because it is needed to calculate the peak width. Note: This function mutates the vectors stored in the NamedTuple `pks`, and not the named tuple itself. +See also: [`peakproms!`](@ref), [`peakheights!`](@ref) + # Examples -Write examples +```jldoctest +julia> data = [1, 5, 1, 3, 2]; + +julia> pks = findmaxima(data); + +julia> pks = peakwidths!(pks) +(indices = [2, 4], heights = [5, 3], data = [1, 5, 1, 3, 2], proms = Union{Missing, Int64}[4, 1], widths = [1.0, 0.75], edges = [(1.5, 2.5), (3.75, 4.5)]) + +julia> data |> findmaxima |> peakwidths! +(indices = [2, 4], heights = [5, 3], data = [1, 5, 1, 3, 2], proms = Union{Missing, Int64}[4, 1], widths = [1.0, 0.75], edges = [(1.5, 2.5), (3.75, 4.5)]) +``` """ function peakwidths!(pks::NamedTuple; minwidth=nothing, maxwidth=nothing, min=minwidth, max=maxwidth, relheight=0.5, strict=true) if !hasproperty(pks, :proms) # Add proms if needed @@ -104,9 +129,10 @@ peakwidths!(; kwargs...) = pks -> peakwidths!(pks; kwargs...) - `relheight`: How far up to measure width, as fraction of the peak prominence. Defaults to `0.5`. - `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. -Non-mutation version of `peakwidths!`. Note that this copies all vectors -in `pks`, meaning that it is less performant. -See the docstring for `peakwidths!` for more information. +Non-mutation version of `peakwidths!`. Note that +this copies all vectors in `pks`, meaning that +it is less performant. See the docstring for +`peakwidths!` for more information. """ peakwidths(pks::NamedTuple; kwargs...) = peakwidths!(deepcopy(pks); kwargs...) @@ -126,7 +152,25 @@ the feature `heights` calculated, this function is mainly useful to filter peaks by a minimum and/or maximum height. If the positional argument `pks` is omitted, a function is returned such -that `peakheights!(pks)` is equivalent to `pks|>peakheights!`. +that `peakheights!(pks)` is equivalent to `pks |> peakheights!`. + +Note: This function mutates the vectors stored in the NamedTuple `pks`, +and not the named tuple itself. + +See also: [`peakproms!`](@ref), [`peakwidths!`](@ref) + +# Examples +```jldoctest +julia> data = [1, 5, 1, 3, 2]; + +julia> pks = findmaxima(data); + +julia> pks = peakheights!(pks, min=4) +(indices = [2], heights = [5], data = [1, 5, 1, 3, 2]) + +julia> data |> findmaxima |> peakheights!(min=4) +(indices = [2], heights = [5], data = [1, 5, 1, 3, 2]) +``` """ function peakheights!(pks::NamedTuple; minheight=nothing, maxheight=nothing, min=minheight, max=maxheight) filterpeaks!(pks, min, max, :heights) @@ -142,8 +186,9 @@ peakheights!(; kwargs...) = pks -> peakheights!(pks; kwargs...) - `min`: Filter out any peak with a height smaller than `min`. - `max`: Filter out any peak with a height greater than `min`. -Non-mutation version of `peakheights!`. Note that this copies all vectors -in `pks`, meaning that it is less performant. -See the docstring for `peakheights!` for more information. +Non-mutation version of `peakheights!`. Note that +this copies all vectors in `pks`, meaning that +it is less performant. See the docstring for +`peakheights!` for more information. """ peakheights(pks::NamedTuple; kwargs...) = peakheights!(deepcopy(pks); kwargs...) diff --git a/src/minmax.jl b/src/minmax.jl index b569fab..848f85b 100644 --- a/src/minmax.jl +++ b/src/minmax.jl @@ -190,15 +190,25 @@ end export maxima """ - findmaxima(x[, w=1; strict=true]) -> (idxs, vals) + findmaxima(x[, w=1; strict=true]) -> NamedTuple -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. The +returned named tuple contains the fields `indices, heights, data`, +and contains what the fieldnames suggest. 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) @@ -321,15 +331,25 @@ end export minima """ - findminima(x[, w=1; strict=true]) -> (idxs, vals) + findminima(x[, w=1; strict=true]) -> NamedTuple -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. The +returned named tuple contains the fields `indices, heights, data`, +and contains what the fieldnames suggest. 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) diff --git a/src/peakwidth.jl b/src/peakwidth.jl index 38a1e19..3ed8293 100644 --- a/src/peakwidth.jl +++ b/src/peakwidth.jl @@ -35,9 +35,10 @@ function peakwidths!( V1 = promote_type(T, U) _bad = Missing <: V1 ? missing : float(Int)(NaN) - V = promote_type(V1, typeof(_bad)) - ledge = similar(proms, typeof(one(V)/one(V))) # typeof(one(V)/one(V)) because the - redge = similar(proms, typeof(one(V)/one(V))) # vector eltype need to survive division + V_ = promote_type(V1, typeof(_bad)) # temp variable used in next line + V = typeof(one(V_)/one(V_)) # We will insert the results of divisions. + ledge = similar(proms, V) + redge = similar(proms, V) if strict lst, fst = _bad, _bad diff --git a/test/manual_tests.jl b/test/manual_tests.jl index f265f90..21135b1 100644 --- a/test/manual_tests.jl +++ b/test/manual_tests.jl @@ -6,13 +6,29 @@ pks = findmaxima(data) pks = peakproms!(pks) pks = peakwidths!(pks) -## Below is code intended to generate docstring examples -data = [1, 5, 1, 3, 2] +##! 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 @@ -23,49 +39,5 @@ if run_tests include(joinpath(pkg_path, "test", "runtests.jl")) end -## -begin - fs = 100 - T = 1/fs - t = 0:T:6pi+T - sint = sin.(t) - - begin - sinpks = argmaxima(sint) - _, widths, _, _ = peakwidths(sinpks, sint, sint[sinpks]; strict=false, relheight=1) - @assert isapprox.(widths, fill(pi*100, length(sinpks)), atol=.01)|>all - - sinpks = argminima(sint) - _, widths, _, _ = peakwidths(sinpks, sint, abs.(sint[sinpks]); strict=false, relheight=1) - @assert isapprox.(widths, fill(pi*100, length(sinpks)), atol=.01)|>all - end - - _, widths, _, _ = peakwidths([2], [0.,1.,0.], [1.]) - @assert widths == [1.] - _, widths, _, _ = peakwidths([2], [0.,1.,0.], [NaN]) - @assert widths[1] === NaN - _, widths, _, _ = peakwidths([2], [0.,1.,0.], [missing]) - @assert widths[1] === missing - _, widths, _, _ = peakwidths([2], [0.,1.,NaN], [1.]; strict=true) - @assert widths[1] === NaN - _, widths, _, _ = peakwidths([2], [0.,1.,0.,-1.], [1.]; strict=false) - _, widthsnan, _, _ = peakwidths([2], [0.,1.,NaN,-1.], [1.]; strict=false) - @assert widths == widthsnan - - begin - sinpks = argmaxima(sint) - _, widths, _, _ = peakwidths(sinpks, sint, ones(length(sinpks)); strict=true, relheight=1) - @assert first(widths) === NaN - _, widths, _, _ = peakwidths(sinpks, sint, ones(length(sinpks)); strict=false, relheight=1) - @assert first(widths) !== NaN - end - - begin - sinpks = argmaxima(sint) - _, proms = peakproms(sinpks, sint) - - @assert length(first(peakwidths(sinpks, sint, proms; minwidth=pi*75))) == 2 - @assert length(first(peakwidths(sinpks, sint, proms; maxwidth=pi*75))) == 1 - end - -end +##! The line that caused an error in the tests: +_, widths, _, _ = peakwidths([2], [0.,1.,0.], [missing]) \ No newline at end of file From 8690fe207ebe5cc8588e364cbfdd15a8819ad140 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Tue, 31 Oct 2023 15:09:35 +0100 Subject: [PATCH 21/63] fix docstrung for peakheights --- src/peakheight.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/peakheight.jl b/src/peakheight.jl index 816f141..ecb35e1 100644 --- a/src/peakheight.jl +++ b/src/peakheight.jl @@ -14,7 +14,7 @@ See also: [`peakprom`](@ref), [`peakwidths`](@ref), [`findmaxima`](@ref) julia> x = [0,5,2,3,3,1,4,0]; julia> xpks, vals = findmaxima(x) -([2, 4, 7], [5, 3, 4]) +(indices = [2, 4, 7], heights = [5, 3, 4], data = [0, 5, 2, 3, 3, 1, 4, 0]) julia> peakheights!(xpks, vals; maxheight=4); @@ -56,7 +56,7 @@ See also: [`peakprom`](@ref), [`peakwidths`](@ref), [`findmaxima`](@ref) julia> x = [0,5,2,3,3,1,4,0]; julia> xpks, vals = findmaxima(x) -([2, 4, 7], [5, 3, 4]) +(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]) From 40ad1a7d8c5ee79b8e43e8a366f4ba04f7f2c80c Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Sun, 26 Nov 2023 14:28:19 +0100 Subject: [PATCH 22/63] un-commented failing test --- test/peakwidth.jl | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/peakwidth.jl b/test/peakwidth.jl index d98e20e..3a2fa93 100644 --- a/test/peakwidth.jl +++ b/test/peakwidth.jl @@ -18,11 +18,8 @@ @test widths == [1.] _, widths, _, _ = peakwidths([2], [0.,1.,0.], [NaN]) @test widths[1] === NaN - # the line below errors with the error: - # "ERROR: MethodError: Cannot `convert` an object of type Missing to an object of type Float64" - #_, widths, _, _ = peakwidths([2], [0.,1.,0.], [missing]) - #@test widths[1] === missing - @test false # to flag commented-out test + _, widths, _, _ = peakwidths([2], [0.,1.,0.], [missing]) + @test widths[1] === missing _, widths, _, _ = peakwidths([2], [0.,1.,NaN], [1.]; strict=true) @test widths[1] === NaN _, widths, _, _ = peakwidths([2], [0.,1.,0.,-1.], [1.]; strict=false) From cf0d8c31cd034394e00119792741a9c330a814ba Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Sun, 26 Nov 2023 14:32:41 +0100 Subject: [PATCH 23/63] revert exports --- src/Peaks.jl | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Peaks.jl b/src/Peaks.jl index e694b38..faeb4a5 100644 --- a/src/Peaks.jl +++ b/src/Peaks.jl @@ -2,10 +2,7 @@ module Peaks using Compat -# Old exports: -# export argmaxima, argminima, maxima, minima, findmaxima, findminima, findnextmaxima, findnextminima, peakproms, peakproms!, peakwidths, peakwidths!, peakheights, peakheights!, ismaxima, isminima, findpeaks -# Proposed exports: -export findpeaks, peakproms!, peakwidths!, peakheights! +export argmaxima, argminima, maxima, minima, findmaxima, findminima, findnextmaxima, findnextminima, peakproms, peakproms!, peakwidths, peakwidths!, peakheights, peakheights!, ismaxima, isminima, findpeaks include("minmax.jl") include("utils.jl") From 3c17d0c372efd570fe935c6c9b64fa976082ae41 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Sun, 26 Nov 2023 14:34:02 +0100 Subject: [PATCH 24/63] move export of filterpeaks! --- src/Peaks.jl | 2 +- src/utils.jl | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Peaks.jl b/src/Peaks.jl index faeb4a5..c7430e9 100644 --- a/src/Peaks.jl +++ b/src/Peaks.jl @@ -2,7 +2,7 @@ module Peaks using Compat -export argmaxima, argminima, maxima, minima, findmaxima, findminima, findnextmaxima, findnextminima, peakproms, peakproms!, peakwidths, peakwidths!, peakheights, peakheights!, ismaxima, isminima, findpeaks +export argmaxima, argminima, maxima, minima, findmaxima, findminima, findnextmaxima, findnextminima, peakproms, peakproms!, peakwidths, peakwidths!, peakheights, peakheights!, ismaxima, isminima, findpeaks, filterpeaks! include("minmax.jl") include("utils.jl") diff --git a/src/utils.jl b/src/utils.jl index ec3aa82..b3a66d6 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -37,7 +37,6 @@ function filterpeaks!(pks::NamedTuple, mask::Union{BitVector, Vector{Bool}}) end return nothing end -export filterpeaks! # This method gets no docstring, as it is intended for internal use. function filterpeaks!(pks::NamedTuple, min, max, feature::Symbol) From 21312b972b5cc902f53e0187b3cd6dbda9ac6a23 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Sun, 26 Nov 2023 14:35:32 +0100 Subject: [PATCH 25/63] remove extra exports --- src/minmax.jl | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/minmax.jl b/src/minmax.jl index 848f85b..bf74f59 100644 --- a/src/minmax.jl +++ b/src/minmax.jl @@ -104,7 +104,6 @@ julia> findnextmaxima([0,2,0,1,1,0], 3) ``` """ findnextmaxima(x, i, w=1; strict=true) = findnextextrema(<, x, i, w, strict) -export findnextmaxima """ ismaxima(i, x[, w=1; strict=true]) -> Bool @@ -118,7 +117,6 @@ are bounded by lesser values immediately before and after the consecutive maxima See also: [`findnextmaxima`](@ref) """ ismaxima(i, x, w=1; strict=true)::Bool = findnextextrema(<, x, i, w, strict) === i -export ismaxima """ argmaxima(x[, w=1; strict=true]) -> Vector{Int} @@ -168,7 +166,6 @@ function argmaxima( return pks end -export argmaxima """ maxima(x[, w=1; strict=true]) -> Vector{eltype(x)} @@ -187,7 +184,6 @@ function maxima( idxs = argmaxima(x, w; strict=strict) return x[idxs] end -export maxima """ findmaxima(x[, w=1; strict=true]) -> NamedTuple @@ -214,7 +210,6 @@ function findmaxima(x, w::Int=1; strict::Bool=true) idxs = argmaxima(x, w; strict=strict) return (indices=idxs, heights=x[idxs], data=x) end -export findmaxima """ findnextminima(x, i[, w=1, strict=true]) -> Int @@ -245,7 +240,6 @@ julia> findnextminima([3,2,3,1,1,3], 3) ``` """ findnextminima(x, i, w=1; strict=true) = findnextextrema(>, x, i, w, strict) -export findnextminima """ isminima(i, x[, w=1; strict=true]) -> Bool @@ -258,7 +252,6 @@ are bounded by greater values immediately before and after the consecutive minim See also: [`findnextminima`](@ref) """ isminima(i, x, w=1; strict=true)::Bool = findnextextrema(>, x, i, w, strict) === i -export isminima """ argminima(x[, w=1; strict=false]) -> Vector{Int} @@ -309,7 +302,6 @@ function argminima( return pks end -export argminima """ minima(x[, w=1; strict=true]) -> Vector{eltype(x)} @@ -328,7 +320,6 @@ function minima( idxs = argminima(x, w; strict=strict) return x[idxs] end -export minima """ findminima(x[, w=1; strict=true]) -> NamedTuple @@ -355,4 +346,3 @@ function findminima(x, w::Int=1; strict::Bool=true) idxs = argminima(x, w; strict=strict) return (indices=idxs, heights=x[idxs], data=x) end -export findminima From 7f005f6a52a135e8f741e8751faed5fad1822de3 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Sun, 26 Nov 2023 15:05:16 +0100 Subject: [PATCH 26/63] explicit about copying data in docstring --- src/api_rework.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/api_rework.jl b/src/api_rework.jl index ea396cf..1e6e5b2 100644 --- a/src/api_rework.jl +++ b/src/api_rework.jl @@ -56,8 +56,8 @@ peakproms!(; kwargs...) = pks -> peakproms!(pks; kwargs...) - `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. Non-mutation version of `peakproms!`. Note that -this copies all vectors in `pks`, meaning that -it is less performant. See the docstring for +this copies all vectors in `pks`, including the data. +This means that it is less performant. See the docstring for `peakproms!` for more information. """ peakproms(pks::NamedTuple; kwargs...) = peakproms!(deepcopy(pks); kwargs...) @@ -130,8 +130,8 @@ peakwidths!(; kwargs...) = pks -> peakwidths!(pks; kwargs...) - `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. Non-mutation version of `peakwidths!`. Note that -this copies all vectors in `pks`, meaning that -it is less performant. See the docstring for +this copies all vectors in `pks`, including the data. +This means that it is less performant. See the docstring for `peakwidths!` for more information. """ peakwidths(pks::NamedTuple; kwargs...) = peakwidths!(deepcopy(pks); kwargs...) @@ -187,8 +187,8 @@ peakheights!(; kwargs...) = pks -> peakheights!(pks; kwargs...) - `max`: Filter out any peak with a height greater than `min`. Non-mutation version of `peakheights!`. Note that -this copies all vectors in `pks`, meaning that -it is less performant. See the docstring for +this copies all vectors in `pks`, including the data. +This means that it is less performant. See the docstring for `peakheights!` for more information. """ peakheights(pks::NamedTuple; kwargs...) = peakheights!(deepcopy(pks); kwargs...) From 11eb80da7a7970061a50dc759a7faee073c24be7 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Sun, 26 Nov 2023 15:18:32 +0100 Subject: [PATCH 27/63] Turn --> info -> --- src/api_rework.jl | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/api_rework.jl b/src/api_rework.jl index 1e6e5b2..208895a 100644 --- a/src/api_rework.jl +++ b/src/api_rework.jl @@ -1,8 +1,8 @@ # I am putting everything in here for now. The contents of this file should be moved around in the future. """ - peakproms!(pks) --> NamedTuple - peakproms!() --> Function + peakproms!(pks) -> NamedTuple + peakproms!() -> Function # Optional keyword arguments - `min`: Filter out any peak with a height smaller than `min`. @@ -47,8 +47,8 @@ end peakproms!(; kwargs...) = pks -> peakproms!(pks; kwargs...) """ - peakproms(pks) --> NamedTuple - peakproms() --> Function + peakproms(pks) -> NamedTuple + peakproms() -> Function # Optional keyword arguments - `min`: Filter out any peak with a height smaller than `min`. @@ -63,8 +63,8 @@ This means that it is less performant. See the docstring for peakproms(pks::NamedTuple; kwargs...) = peakproms!(deepcopy(pks); kwargs...) """ - peakwidths!(pks) --> NamedTuple - peakwidths!() --> Function + peakwidths!(pks) -> NamedTuple + peakwidths!() -> Function # Optional keyword arguments - `min`: Filter out any peak with a height smaller than `min`. @@ -120,8 +120,8 @@ end peakwidths!(; kwargs...) = pks -> peakwidths!(pks; kwargs...) """ - peakwidths(pks) --> NamedTuple - peakwidths() --> Function + peakwidths(pks) -> NamedTuple + peakwidths() -> Function # Optional keyword arguments - `min`: Filter out any peak with a height smaller than `min`. @@ -138,8 +138,8 @@ peakwidths(pks::NamedTuple; kwargs...) = peakwidths!(deepcopy(pks); kwargs...) """ - peakheights!(pks) --> NamedTuple - peakheights!() --> Function + peakheights!(pks) -> NamedTuple + peakheights!() -> Function # Optional keyword arguments - `min`: Filter out any peak with a height smaller than `min`. @@ -179,8 +179,8 @@ end peakheights!(; kwargs...) = pks -> peakheights!(pks; kwargs...) """ - peakheights(pks) --> NamedTuple - peakheights() --> Function + peakheights(pks) -> NamedTuple + peakheights() -> Function # Optional keyword arguments - `min`: Filter out any peak with a height smaller than `min`. From df8537b337142d134c352dbf92bf8d6462647ba3 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Sun, 26 Nov 2023 15:21:16 +0100 Subject: [PATCH 28/63] add sentence about not copying data --- src/minmax.jl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/minmax.jl b/src/minmax.jl index bf74f59..356d4ba 100644 --- a/src/minmax.jl +++ b/src/minmax.jl @@ -190,8 +190,9 @@ end 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. The -returned named tuple contains the fields `indices, heights, data`, -and contains what the fieldnames suggest. +returned named tuple contains the fields `indices, heights, data`, and +contains what the fieldnames suggest. Note that the vector stored +in the field `:data` is the original data vector, and not a copy. 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. @@ -326,8 +327,9 @@ end 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. The -returned named tuple contains the fields `indices, heights, data`, -and contains what the fieldnames suggest. +returned named tuple contains the fields `indices, heights, data`, and +contains what the fieldnames suggest. Note that the vector stored +in the field `:data` is the original data vector, and not a copy. 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. From 5c82230ab0254697bce558cd0724e5e1446685f6 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Sun, 26 Nov 2023 15:38:02 +0100 Subject: [PATCH 29/63] minheight -> min, same for proms,widths, and max --- Project.toml | 2 +- src/api_rework.jl | 18 ++++++++++++++++++ src/peakheight.jl | 26 ++++++++++++++++++++------ src/peakprom.jl | 34 ++++++++++++++++++++++++---------- src/peakwidth.jl | 32 +++++++++++++++++++++++--------- 5 files changed, 86 insertions(+), 26 deletions(-) diff --git a/Project.toml b/Project.toml index 1d733fa..ade018a 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Peaks" uuid = "18e31ff7-3703-566c-8e60-38913d67486b" authors = ["Allen Hill "] -version = "0.5.0" +version = "0.4.4" [deps] Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" diff --git a/src/api_rework.jl b/src/api_rework.jl index 208895a..db75589 100644 --- a/src/api_rework.jl +++ b/src/api_rework.jl @@ -35,6 +35,12 @@ julia> data |> findmaxima |> peakproms! ``` """ function peakproms!(pks::NamedTuple; minprom=nothing, maxprom=nothing, min=minprom, max=maxprom, strict=true) + 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 !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 @@ -102,6 +108,12 @@ julia> data |> findmaxima |> peakwidths! ``` """ function peakwidths!(pks::NamedTuple; minwidth=nothing, maxwidth=nothing, min=minwidth, max=maxwidth, relheight=0.5, strict=true) + if !isnothing(minprom) + Base.depwarn("Keyword `minprom` has been renamed to `min`", :peakwidths!) + end + if !isnothing(maxprom) + Base.depwarn("Keyword `maxprom` has been renamed to `max`", :peakwidths!) + end if !hasproperty(pks, :proms) # Add proms if needed pks = peakproms!(pks; strict) end @@ -173,6 +185,12 @@ julia> data |> findmaxima |> peakheights!(min=4) ``` """ function peakheights!(pks::NamedTuple; minheight=nothing, maxheight=nothing, min=minheight, max=maxheight) + if !isnothing(minprom) + Base.depwarn("Keyword `minprom` has been renamed to `min`", :peakheights!) + end + if !isnothing(maxprom) + Base.depwarn("Keyword `maxprom` has been renamed to `max`", :peakheights!) + end filterpeaks!(pks, min, max, :heights) return pks end diff --git a/src/peakheight.jl b/src/peakheight.jl index ecb35e1..9255b1e 100644 --- a/src/peakheight.jl +++ b/src/peakheight.jl @@ -24,12 +24,19 @@ julia> xpks, vals """ function peakheights!( peaks::Vector{Int}, heights::AbstractVector{T}; - minheight=nothing, maxheight=nothing + minheight=nothing, maxheight=nothing, + min=minheight, max=maxheight ) where {T} + if !isnothing(minprom) + Base.depwarn("Keyword `minheight` has been renamed to `min`", :peakheights!) + end + if !isnothing(maxprom) + 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))) + if !isnothing(min) || !isnothing(max) + lo = something(min, typemin(Base.nonmissingtype(T))) + up = something(max, typemax(Base.nonmissingtype(T))) matched = findall(x -> !(lo ≤ x ≤ up), heights) deleteat!(peaks, matched) deleteat!(heights, matched) @@ -67,8 +74,15 @@ julia> peakheights(xpks, vals; minheight=4.5) """ function peakheights( peaks::AbstractVector{Int}, heights::AbstractVector; - minheight=nothing, maxheight=nothing + minheight=nothing, maxheight=nothing, + min=minheight, max=maxheight ) - peakheights!(copy(peaks), copy(heights); minheight=minheight, maxheight=maxheight) + if !isnothing(minprom) + Base.depwarn("Keyword `minheight` has been renamed to `min`", :peakheights!) + end + if !isnothing(maxprom) + Base.depwarn("Keyword `maxheight` has been renamed to `max`", :peakheights!) + end + peakheights!(copy(peaks), copy(heights); min=min, max=max) end export peakheights \ No newline at end of file diff --git a/src/peakprom.jl b/src/peakprom.jl index 9ee4db5..68e1691 100644 --- a/src/peakprom.jl +++ b/src/peakprom.jl @@ -12,10 +12,17 @@ prominences. See also: [`peakproms`](@ref), [`findminima`](@ref), [`findmaxima`](@ref) """ function peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; - strict=true, minprom=nothing, maxprom=nothing + strict=true, minprom=nothing, maxprom=nothing, + min=minprom, max=maxprom ) where {T} - if !isnothing(minprom) && !isnothing(maxprom) - minprom < maxprom || throw(ArgumentError("minprom must be less than maxprom")) + 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("minimal prominence must be less than maximal prominence")) end all(∈(eachindex(x)), peaks) || throw(ArgumentError("peaks contains invalid indices to x")) @@ -36,7 +43,7 @@ function peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; Float64 <: T ? NaN : Float32 <: T ? NaN32 : Float16 <: T ? NaN16 : - missing + missing proms = similar(peaks, promote_type(T, typeof(_ref))) @@ -114,9 +121,9 @@ 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)))) + if !isnothing(min) || !isnothing(max) + lo = something(min, zero(eltype(x))) + up = something(max, typemax(Base.nonmissingtype(eltype(x)))) matched = findall(x -> !ismissing(x) && !(lo ≤ x ≤ up), proms) deleteat!(peaks, matched) deleteat!(proms, matched) @@ -170,15 +177,22 @@ julia> peakproms(xpks, x; strict=false) ``` """ function peakproms(peaks::AbstractVector{Int}, x::AbstractVector{T}; - strict=true, minprom=nothing, maxprom=nothing + strict=true, minprom=nothing, maxprom=nothing, + min=minprom, max=maxprom ) where {T} - if !isnothing(minprom) || !isnothing(maxprom) + 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 export peakproms diff --git a/src/peakwidth.jl b/src/peakwidth.jl index 3ed8293..35ca59a 100644 --- a/src/peakwidth.jl +++ b/src/peakwidth.jl @@ -14,10 +14,17 @@ See also: [`peakwidths`](@ref), [`peakproms`](@ref), [`findminima`](@ref), [`fin """ function peakwidths!( peaks::AbstractVector{Int}, x::AbstractVector{T}, proms::AbstractVector{U}; - strict=true, relheight=0.5, minwidth=nothing, maxwidth=nothing + strict=true, relheight=0.5, minwidth=nothing, maxwidth=nothing, + min=minwidth, max=maxwidth ) where {T,U} - if !isnothing(minwidth) && !isnothing(maxwidth) - minwidth < maxwidth || throw(ArgumentError("maxwidth must be greater than minwidth")) + 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("max width must be greater than min width")) end all(∈(eachindex(x)), peaks) || throw(ArgumentError("peaks contains invalid indices to x")) @@ -77,9 +84,9 @@ function peakwidths!( widths::Vector{V} = redge - ledge - if !isnothing(minwidth) || !isnothing(maxwidth) - lo = something(minwidth, zero(eltype(widths))) - up = something(maxwidth, typemax(Base.nonmissingtype(eltype(widths)))) + if !isnothing(min) || !isnothing(max) + lo = something(min, zero(eltype(widths))) + up = something(max, typemax(Base.nonmissingtype(eltype(widths)))) matched = findall(x -> !ismissing(x) && !(lo ≤ x ≤ up), widths) deleteat!(peaks, matched) deleteat!(ledge, matched) @@ -136,15 +143,22 @@ julia> peakwidths(xpks, x, [1]; strict=false) """ function peakwidths( peaks::AbstractVector{Int}, x::AbstractVector, proms::AbstractVector; - strict=true, relheight=0.5, minwidth=nothing, maxwidth=nothing + 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 export peakwidths \ No newline at end of file From 779081e6e98a3cf64e90a40a7932bc20d65d1235 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Sun, 26 Nov 2023 17:15:18 +0100 Subject: [PATCH 30/63] some changes + better length check in filterpeaks! --- src/api_rework.jl | 19 +++++---- src/peakprom.jl | 2 +- src/utils.jl | 103 +++++++++++++++++++++++++++++++++++++--------- 3 files changed, 95 insertions(+), 29 deletions(-) diff --git a/src/api_rework.jl b/src/api_rework.jl index db75589..69f24f5 100644 --- a/src/api_rework.jl +++ b/src/api_rework.jl @@ -108,11 +108,11 @@ julia> data |> findmaxima |> peakwidths! ``` """ function peakwidths!(pks::NamedTuple; minwidth=nothing, maxwidth=nothing, min=minwidth, max=maxwidth, relheight=0.5, strict=true) - if !isnothing(minprom) - Base.depwarn("Keyword `minprom` has been renamed to `min`", :peakwidths!) + if !isnothing(minwidth) + Base.depwarn("Keyword `minwidth` has been renamed to `min`", :peakwidths!) end - if !isnothing(maxprom) - Base.depwarn("Keyword `maxprom` has been renamed to `max`", :peakwidths!) + if !isnothing(maxwidth) + Base.depwarn("Keyword `maxwidth` has been renamed to `max`", :peakwidths!) end if !hasproperty(pks, :proms) # Add proms if needed pks = peakproms!(pks; strict) @@ -175,7 +175,8 @@ See also: [`peakproms!`](@ref), [`peakwidths!`](@ref) ```jldoctest julia> data = [1, 5, 1, 3, 2]; -julia> pks = findmaxima(data); +julia> pks = findmaxima(data) +(indices = [2, 4], heights = [5, 3], data = [1, 5, 1, 3, 2]) julia> pks = peakheights!(pks, min=4) (indices = [2], heights = [5], data = [1, 5, 1, 3, 2]) @@ -185,11 +186,11 @@ julia> data |> findmaxima |> peakheights!(min=4) ``` """ function peakheights!(pks::NamedTuple; minheight=nothing, maxheight=nothing, min=minheight, max=maxheight) - if !isnothing(minprom) - Base.depwarn("Keyword `minprom` has been renamed to `min`", :peakheights!) + if !isnothing(minheight) + Base.depwarn("Keyword `minheight` has been renamed to `min`", :peakheights!) end - if !isnothing(maxprom) - Base.depwarn("Keyword `maxprom` has been renamed to `max`", :peakheights!) + if !isnothing(maxheight) + Base.depwarn("Keyword `maxheight` has been renamed to `max`", :peakheights!) end filterpeaks!(pks, min, max, :heights) return pks diff --git a/src/peakprom.jl b/src/peakprom.jl index 68e1691..02634bd 100644 --- a/src/peakprom.jl +++ b/src/peakprom.jl @@ -37,7 +37,7 @@ function peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; end cmp = pktype === :maxima ? (≥) : (≤) exm = pktype === :maxima ? minimum : maximum - exa = pktype === :maxima ? max : min + exa = pktype === :maxima ? Base.max : Base.min _ref = Missing <: T ? missing : Float64 <: T ? NaN : diff --git a/src/utils.jl b/src/utils.jl index b3a66d6..3ab4ec7 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,54 +1,119 @@ """ filterpeaks!(pks, mask) -> pks -Given a NamedTuple `pks` (as returned by `findpeaks`), filter -the peaks according to the given `mask` (A `BitVector` or `Vector{Bool}`. -This means if element `i` of `mask` is false, then the peak -at index `i` will be removed from `pks`. +Filter the known fields of `pks` using `mask`, by removing elements of vectors +in `pks` fields for which `mask[i]` is `false`. Known fields +of `pks` are `:indices`, `:proms`, `:heights`, `:widths`, `:edges`. The functions `peakheights`, `peakproms` and `peakwidths` already allow filtering by maximal and minimal values for different peak features. This function can be used to perform more complicated filtering, such -as keeping a peak if it has a certain height _or_ a certain width. +as keeping a peak if it has a certain height _or_ a certain width. # Examples -ToDo: Make examples +julia> data = [1, 2, 3, 2, 3, 4, 0]; + +julia> pks = findmaxima(data) +(indices = [3, 6], heights = [3, 4], data = [1, 2, 3, 2, 3, 4, 0]) + +julia> pks = peakwidths!(pks) +(indices = [3, 6], heights = [3, 4], data = [1, 2, 3, 2, 3, 4, 0], proms = Union{Missing, Int64}[1, 3], widths = [1.0, 1.875], edges = [(2.5, 3.5), (4.5, 6.375)]) + +julia> # We can demand that the peak height is greater than 3.5 AND that the width is smaller than 1.5 +julia> # with the following mask. Note that with this data, that leaves no peaks. + +julia> my_mask = [pks.heights[i] > 3.5 && pks.widths[i] < 1.5 for i in eachindex(pks.indices)] +2-element Vector{Bool}: + 0 + 0 + +julia> filterpeaks!(pks, my_mask) +(indices = Int64[], heights = Int64[], data = [1, 2, 3, 2, 3, 4, 0], proms = Union{Missing, Int64}[], widths = Float64[], edges = Tuple{Float64, Float64}[]) + """ function filterpeaks!(pks::NamedTuple, mask::Union{BitVector, Vector{Bool}}) features_to_filter = (:indices, :proms, :heights, :widths, :edges) # Check lengths first to avoid a dimension mismatch # after having filtered some features. - for field in features_to_filter - hasproperty(pks, field) || continue # Do nothing if field is not present - if length(mask) != length(getfield(pks, field)) - throw(DimensionMismatch( - "Length of `mask` is ($(length(mask))), but the length - of `pks.$field` is $(length(getfield(pks, field))). - This means that the given mask can not be used to filter - the field `$field`.")) - end + # feature_mask = hasproperty.(pks, features_to_filter) + feature_lengths = [length(pks[feature]) for feature in features_to_filter if hasproperty(pks, feature)] + 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:\n$length_pairs + This should not happen, and indicates that the argument `pks` been created or modified by something outside Peaks.jl")) + end + # At this point we know that all feature_length are equal, and do not need to check it again + if first(feature_lengths) != length(mask) + throw(DimensionMismatch( + "Length of `mask` is $(length(mask)), but the length of each of the known fields of `pks` is $(first(feature_lengths)). + This means that the given mask can not be used to filter the given named tuple`pks`." + )) end for field in features_to_filter # Only risk mutating fields added by this package hasproperty(pks, field) || continue # Do nothing if field is not present - v_to_be_mutated = getfield(pks, field) - deleteat!(v_to_be_mutated, mask) + v_to_be_mutated = pks[field] + deleteat!(v_to_be_mutated, .!mask) end - return nothing + return pks end # This method gets no docstring, as it is intended for internal use. +""" + filterpeaks!(pks, min, max, feature) + +Calculate `mask` as `[min < x < max for x in pks.feature]`, +and call `filterpeaks!(pks, mask)`. + +This method is intended for internal use. If you want to filter +by a minimal or maximal value of some peak feature, concider using +the keyword arguments `min` and `max` in the function that calculated +that feature, such as `peakproms!(pks, min=2)`. +""" function filterpeaks!(pks::NamedTuple, min, max, feature::Symbol) if !isnothing(min) || !isnothing(max) lo = something(min, zero(eltype(pks.data))) up = something(max, typemax(Base.nonmissingtype(eltype(pks.data)))) - mask = map(x -> !ismissing(x) && !(lo ≤ x ≤ up), getproperty(pks, feature)) + mask = map(x -> !ismissing(x) && (lo ≤ x ≤ up), pks[feature]) filterpeaks!(pks, mask) end return nothing end + +#= +! This is an idea for making it easier to create masks. +""" + peakmask(conds...) + +Experimental function to make it easier to create masks intended +for use in `filterpeaks!(pks, mask)`. `Conds` is a variable number of +conditions that define the mask. + +The each condition in `conds` is expected to be a tuple that contains +`(s::Symbol`, f::`Function`, n::`Number`). It is used to find the +peaks in `pks` for which `f([:symbol][i], n)`. + +# Examples + +""" +function peakmask(conds::Tuple{Symbol, Function, <:Real}...) + function f(pks) + mask = trues(length(pks[:indices])) + + for cond in conds + for i in eachindex(pks[:indices]) + mask[i] = mask[i] && cond[2](pks[cond[1]][i], cond[3]) + end + end + return mask + end + return f +end +export peakmask +=# + #==================================================================== We store a version of findpeak here, as it might be implemented soon, and I did not want to throw away this implementation From c19d483babb19b178c6c0f6364a20130b1792cda Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Sun, 26 Nov 2023 17:54:32 +0100 Subject: [PATCH 31/63] add new method for filterpeaks! --- src/utils.jl | 118 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 80 insertions(+), 38 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 3ab4ec7..447edaf 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,3 +1,29 @@ +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:\n$length_pairs + This should not happen, and indicates that the argument `pks` been created or modified by something outside Peaks.jl")) + end + return nothing +end + +function check_has_known_field(pks::NamedTuple) + if !any(hasproperty(pks, prop) for prop in known_fields()) + throw(ArgumentError( + "Attempting to filter a named tuple `pks` that contains none of the known fields $(known_fields()). Because + this is thought to be an error, this error is thrown to help catch the original error." + )) + end + return nothing +end + """ filterpeaks!(pks, mask) -> pks @@ -8,7 +34,10 @@ of `pks` are `:indices`, `:proms`, `:heights`, `:widths`, `:edges`. The functions `peakheights`, `peakproms` and `peakwidths` already allow filtering by maximal and minimal values for different peak features. This function can be used to perform more complicated filtering, such -as keeping a peak if it has a certain height _or_ a certain width. +as keeping a peak if it has a certain height _or_ a certain width. + +If you find it inconvenient to define the the mask, see also the +version of `filterpeaks!` that takes a function as its first argument. # Examples julia> data = [1, 2, 3, 2, 3, 4, 0]; @@ -32,29 +61,26 @@ julia> filterpeaks!(pks, my_mask) """ function filterpeaks!(pks::NamedTuple, mask::Union{BitVector, Vector{Bool}}) - features_to_filter = (:indices, :proms, :heights, :widths, :edges) + # Check lengths first to avoid a dimension mismatch # after having filtered some features. # feature_mask = hasproperty.(pks, features_to_filter) - feature_lengths = [length(pks[feature]) for feature in features_to_filter if hasproperty(pks, feature)] - 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:\n$length_pairs - This should not happen, and indicates that the argument `pks` been created or modified by something outside Peaks.jl")) - end + check_known_fields_equal_length(pks) + check_has_known_field(pks) + # At this point we know that all feature_length are equal, and do not need to check it again - if first(feature_lengths) != length(mask) + # pks[1] returns the indices. + if length(pks[1]) != length(mask) throw(DimensionMismatch( - "Length of `mask` is $(length(mask)), but the length of each of the known fields of `pks` is $(first(feature_lengths)). - This means that the given mask can not be used to filter the given named tuple`pks`." + "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 features_to_filter # Only risk mutating fields added by this package + for field in known_fields() # Only risk mutating fields added by this package hasproperty(pks, field) || continue # Do nothing if field is not present - v_to_be_mutated = pks[field] - deleteat!(v_to_be_mutated, .!mask) + deleteat!(pks[field], .!mask) end return pks end @@ -81,38 +107,54 @@ function filterpeaks!(pks::NamedTuple, min, max, feature::Symbol) return nothing end - -#= -! This is an idea for making it easier to create masks. """ - peakmask(conds...) + filterpeaks!(pred, pks) -> NamedTuple -Experimental function to make it easier to create masks intended -for use in `filterpeaks!(pks, mask)`. `Conds` is a variable number of -conditions that define the mask. - -The each condition in `conds` is expected to be a tuple that contains -`(s::Symbol`, f::`Function`, n::`Number`). It is used to find the -peaks in `pks` for which `f([:symbol][i], n)`. +Apply a predicate function `pred` to named tuple slices to get a filter-mask. +By "named tuple slice" we refer to +If `pred` returns `false` for peak number `i`, element `i` is removed from +the known fields of `pks`. # Examples +julia> data = [1, 2, 3, 2, 3, 4, 0]; + +julia> pks = findmaxima(data) +(indices = [3, 6], heights = [3, 4], data = [1, 2, 3, 2, 3, 4, 0]) + +julia> pks = peakwidths!(pks); + +julia> data = [1, 2, 3, 2, 3, 4, 0]; + +julia> pks = findmaxima(data) +(indices = [3, 6], heights = [3, 4], data = [1, 2, 3, 2, 3, 4, 0]) + +julia> pks = peakwidths!(pks) +(indices = [3, 6], heights = [3, 4], data = [1, 2, 3, 2, 3, 4, 0], proms = Union{Missing, Int64}[1, 3], widths = [1.0, 1.875], edges = [(2.5, 3.5), (4.5, 6.375)]) + +julia> filterpeaks!(pks) do nt_slice + nt_slice.heights > 3.5 && nt_slice.widths > 1.8 + end +(indices = [6], heights = [4], data = [1, 2, 3, 2, 3, 4, 0], proms = Union{Missing, Int64}[3], widths = [1.875], edges = [(4.5, 6.375)]) """ -function peakmask(conds::Tuple{Symbol, Function, <:Real}...) - function f(pks) - mask = trues(length(pks[:indices])) - - for cond in conds - for i in eachindex(pks[:indices]) - mask[i] = mask[i] && cond[2](pks[cond[1]][i], cond[3]) - end - end - return mask +function filterpeaks!(pred::Function, pks::NamedTuple) + check_known_fields_equal_length(pks) + check_has_known_field(pks) + + mask = map(eachindex(pks[1])) do i + # :data is included in the nt_slice, but that should not be a problem + itr = zip(keys(pks), getindex.(values(pks), i)) + nt_slice = NamedTuple(itr) + return pred(nt_slice) end - return f + + 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 -export peakmask -=# + #==================================================================== We store a version of findpeak here, as it might be implemented soon, From 26cdb9de8e6eea298c71db1701a898feefe9c073 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Sun, 26 Nov 2023 18:02:05 +0100 Subject: [PATCH 32/63] Distribute contents of rework, structure exports --- src/Peaks.jl | 13 ++- src/api_rework.jl | 213 ---------------------------------------------- src/peakheight.jl | 69 ++++++++++++++- src/peakprom.jl | 76 ++++++++++++++++- src/peakwidth.jl | 86 ++++++++++++++++++- 5 files changed, 237 insertions(+), 220 deletions(-) delete mode 100644 src/api_rework.jl diff --git a/src/Peaks.jl b/src/Peaks.jl index c7430e9..54feeaa 100644 --- a/src/Peaks.jl +++ b/src/Peaks.jl @@ -2,7 +2,18 @@ module Peaks using Compat -export argmaxima, argminima, maxima, minima, findmaxima, findminima, findnextmaxima, findnextminima, peakproms, peakproms!, peakwidths, peakwidths!, peakheights, peakheights!, ismaxima, isminima, findpeaks, filterpeaks! +# Function related to locating peaks +export argmaxima, argminima +export maxima, minima +export findmaxima, findminima +export findnextmaxima, findnextminima +export ismaxima, isminima + +# Functions related to working with a set of peaks +export peakproms, peakproms! +export peakwidths, peakwidths! +export peakheights, peakheights! +export filterpeaks! include("minmax.jl") include("utils.jl") diff --git a/src/api_rework.jl b/src/api_rework.jl deleted file mode 100644 index 69f24f5..0000000 --- a/src/api_rework.jl +++ /dev/null @@ -1,213 +0,0 @@ -# I am putting everything in here for now. The contents of this file should be moved around in the future. - -""" - peakproms!(pks) -> NamedTuple - peakproms!() -> Function - -# Optional keyword arguments -- `min`: Filter out any peak with a height smaller than `min`. -- `max`: Filter out any peak with a height greater than `min`. -- `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. - -Find the prominences of the peaks in `pks`, and filter out any peak -with a prominence smaller than `min` or greater than `max`. -The prominences are returned in the field `:proms` of the returned named tuple. - -If the positional argument `pks` is omitted, a function is returned such -that `peakproms!(pks)` is equivalent to `pks |> peakproms!`. - -Note: This function mutates the vectors stored in the NamedTuple `pks`, -and not the named tuple itself. - -See also: [`peakwidths!`](@ref), [`peakheights!`](@ref) - -# Examples -```jldoctest -julia> data = [1, 5, 1, 3, 2]; - -julia> pks = findmaxima(data); - -julia> pks = peakproms!(pks) -(indices = [2, 4], heights = [5, 3], data = [1, 5, 1, 3, 2], proms = Union{Missing, Int64}[4, 1]) - -julia> data |> findmaxima |> peakproms! -(indices = [2, 4], heights = [5, 3], data = [1, 5, 1, 3, 2], proms = Union{Missing, Int64}[4, 1]) -``` -""" -function peakproms!(pks::NamedTuple; minprom=nothing, maxprom=nothing, min=minprom, max=maxprom, strict=true) - 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 !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, min, max, :proms) - return pks -end -peakproms!(; kwargs...) = pks -> peakproms!(pks; kwargs...) - -""" - peakproms(pks) -> NamedTuple - peakproms() -> Function - -# Optional keyword arguments -- `min`: Filter out any peak with a height smaller than `min`. -- `max`: Filter out any peak with a height greater than `min`. -- `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. - -Non-mutation version of `peakproms!`. Note that -this copies all vectors in `pks`, including the data. -This means that it is less performant. See the docstring for -`peakproms!` for more information. -""" -peakproms(pks::NamedTuple; kwargs...) = peakproms!(deepcopy(pks); kwargs...) - -""" - peakwidths!(pks) -> NamedTuple - peakwidths!() -> Function - -# Optional keyword arguments -- `min`: Filter out any peak with a height smaller than `min`. -- `max`: Filter out any peak with a height greater than `min`. -- `relheight`: How far up to measure width, as fraction of the peak prominence. Defaults to `0.5`. -- `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. - -Find the widths of the peaks in `pks`, and filter out any peak -with a width smaller than `min` or greater than `max`. -The widths are returned in the field `:widths` of the returned named tuple. -The edges of the peaks are also added in the field `:edges`. - -If the positional argument `pks` is omitted, a function is returned such -that `peakwidths!(pks)` is equivalent to `pks |> peakwidths!`. - -Note: If `pks` does not have a field `proms`, it is added. This is -because it is needed to calculate the peak width. - -Note: This function mutates the vectors stored in the NamedTuple `pks`, -and not the named tuple itself. - -See also: [`peakproms!`](@ref), [`peakheights!`](@ref) - -# Examples -```jldoctest -julia> data = [1, 5, 1, 3, 2]; - -julia> pks = findmaxima(data); - -julia> pks = peakwidths!(pks) -(indices = [2, 4], heights = [5, 3], data = [1, 5, 1, 3, 2], proms = Union{Missing, Int64}[4, 1], widths = [1.0, 0.75], edges = [(1.5, 2.5), (3.75, 4.5)]) - -julia> data |> findmaxima |> peakwidths! -(indices = [2, 4], heights = [5, 3], data = [1, 5, 1, 3, 2], proms = Union{Missing, Int64}[4, 1], widths = [1.0, 0.75], edges = [(1.5, 2.5), (3.75, 4.5)]) -``` -""" -function peakwidths!(pks::NamedTuple; minwidth=nothing, maxwidth=nothing, min=minwidth, max=maxwidth, relheight=0.5, strict=true) - 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 !hasproperty(pks, :proms) # Add proms if needed - pks = peakproms!(pks; strict) - end - if xor(hasproperty(pks, :widths), hasproperty(pks, :edges)) - throw(ArgumentError("The named tuple `pks` (first argument to `peakwidths!` is expected have both the fields `:widths` and `:edges`, or to have neither of them. The provided `pks` only has one of them.")) - 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, min, max, :widths) - return pks -end -peakwidths!(; kwargs...) = pks -> peakwidths!(pks; kwargs...) - -""" - peakwidths(pks) -> NamedTuple - peakwidths() -> Function - -# Optional keyword arguments -- `min`: Filter out any peak with a height smaller than `min`. -- `max`: Filter out any peak with a height greater than `min`. -- `relheight`: How far up to measure width, as fraction of the peak prominence. Defaults to `0.5`. -- `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. - -Non-mutation version of `peakwidths!`. Note that -this copies all vectors in `pks`, including the data. -This means that it is less performant. See the docstring for -`peakwidths!` for more information. -""" -peakwidths(pks::NamedTuple; kwargs...) = peakwidths!(deepcopy(pks); kwargs...) - - -""" - peakheights!(pks) -> NamedTuple - peakheights!() -> Function - -# Optional keyword arguments -- `min`: Filter out any peak with a height smaller than `min`. -- `max`: Filter out any peak with a height greater than `min`. - -Find the heights of the peaks in `pks`, and filter out any peak -with a heights smaller than `min` or greater than `max`. -Note that because the peaks returned by `findpeaks` already have -the feature `heights` calculated, this function is mainly useful to -filter peaks by a minimum and/or maximum height. - -If the positional argument `pks` is omitted, a function is returned such -that `peakheights!(pks)` is equivalent to `pks |> peakheights!`. - -Note: This function mutates the vectors stored in the NamedTuple `pks`, -and not the named tuple itself. - -See also: [`peakproms!`](@ref), [`peakwidths!`](@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]) - -julia> pks = peakheights!(pks, min=4) -(indices = [2], heights = [5], data = [1, 5, 1, 3, 2]) - -julia> data |> findmaxima |> peakheights!(min=4) -(indices = [2], heights = [5], data = [1, 5, 1, 3, 2]) -``` -""" -function peakheights!(pks::NamedTuple; minheight=nothing, maxheight=nothing, min=minheight, max=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 - filterpeaks!(pks, min, max, :heights) - return pks -end -peakheights!(; kwargs...) = pks -> peakheights!(pks; kwargs...) - -""" - peakheights(pks) -> NamedTuple - peakheights() -> Function - -# Optional keyword arguments -- `min`: Filter out any peak with a height smaller than `min`. -- `max`: Filter out any peak with a height greater than `min`. - -Non-mutation version of `peakheights!`. Note that -this copies all vectors in `pks`, including the data. -This means that it is less performant. See the docstring for -`peakheights!` for more information. -""" -peakheights(pks::NamedTuple; kwargs...) = peakheights!(deepcopy(pks); kwargs...) diff --git a/src/peakheight.jl b/src/peakheight.jl index 9255b1e..2fda367 100644 --- a/src/peakheight.jl +++ b/src/peakheight.jl @@ -44,7 +44,6 @@ function peakheights!( return peaks, heights end -export peakheights! """ @@ -85,4 +84,70 @@ function peakheights( end peakheights!(copy(peaks), copy(heights); min=min, max=max) end -export peakheights \ No newline at end of file + +##!===============================================================================================!## +##!========================================== New API ==========================================!## +##!===============================================================================================!## + +""" + peakheights!(pks) -> NamedTuple + peakheights!() -> Function + +# Optional keyword arguments +- `min`: Filter out any peak with a height smaller than `min`. +- `max`: Filter out any peak with a height greater than `min`. + +Find the heights of the peaks in `pks`, and filter out any peak +with a heights smaller than `min` or greater than `max`. +Note that because the peaks returned by `findpeaks` already have +the feature `heights` calculated, this function is mainly useful to +filter peaks by a minimum and/or maximum height. + +If the positional argument `pks` is omitted, a function is returned such +that `peakheights!(pks)` is equivalent to `pks |> peakheights!`. + +Note: This function mutates the vectors stored in the NamedTuple `pks`, +and not the named tuple itself. + +See also: [`peakproms!`](@ref), [`peakwidths!`](@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]) + +julia> pks = peakheights!(pks, min=4) +(indices = [2], heights = [5], data = [1, 5, 1, 3, 2]) + +julia> data |> findmaxima |> peakheights!(min=4) +(indices = [2], heights = [5], data = [1, 5, 1, 3, 2]) +``` +""" +function peakheights!(pks::NamedTuple; minheight=nothing, maxheight=nothing, min=minheight, max=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 + filterpeaks!(pks, min, max, :heights) + return pks +end +peakheights!(; kwargs...) = pks -> peakheights!(pks; kwargs...) + +""" + peakheights(pks) -> NamedTuple + peakheights() -> Function + +# Optional keyword arguments +- `min`: Filter out any peak with a height smaller than `min`. +- `max`: Filter out any peak with a height greater than `min`. + +Non-mutation version of `peakheights!`. Note that +this copies all vectors in `pks`, including the data. +This means that it is less performant. See the docstring for +`peakheights!` for more information. +""" +peakheights(pks::NamedTuple; kwargs...) = peakheights!(deepcopy(pks); kwargs...) \ No newline at end of file diff --git a/src/peakprom.jl b/src/peakprom.jl index 02634bd..1f7ec3a 100644 --- a/src/peakprom.jl +++ b/src/peakprom.jl @@ -131,7 +131,6 @@ function peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; return peaks, proms end -export peakproms! """ peakproms(peaks, x; @@ -194,5 +193,78 @@ function peakproms(peaks::AbstractVector{Int}, x::AbstractVector{T}; end return peakproms!(_peaks, x; strict=strict, min=min, max=max) end -export peakproms + +##!===============================================================================================!## +##!========================================== New API ==========================================!## +##!===============================================================================================!## + + + +""" + peakproms!(pks) -> NamedTuple + peakproms!() -> Function + +# Optional keyword arguments +- `min`: Filter out any peak with a height smaller than `min`. +- `max`: Filter out any peak with a height greater than `min`. +- `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. + +Find the prominences of the peaks in `pks`, and filter out any peak +with a prominence smaller than `min` or greater than `max`. +The prominences are returned in the field `:proms` of the returned named tuple. + +If the positional argument `pks` is omitted, a function is returned such +that `peakproms!(pks)` is equivalent to `pks |> peakproms!`. + +Note: This function mutates the vectors stored in the NamedTuple `pks`, +and not the named tuple itself. + +See also: [`peakwidths!`](@ref), [`peakheights!`](@ref) + +# Examples +```jldoctest +julia> data = [1, 5, 1, 3, 2]; + +julia> pks = findmaxima(data); + +julia> pks = peakproms!(pks) +(indices = [2, 4], heights = [5, 3], data = [1, 5, 1, 3, 2], proms = Union{Missing, Int64}[4, 1]) + +julia> data |> findmaxima |> peakproms! +(indices = [2, 4], heights = [5, 3], data = [1, 5, 1, 3, 2], proms = Union{Missing, Int64}[4, 1]) +``` +""" +function peakproms!(pks::NamedTuple; minprom=nothing, maxprom=nothing, min=minprom, max=maxprom, strict=true) + 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 !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, min, max, :proms) + return pks +end +peakproms!(; kwargs...) = pks -> peakproms!(pks; kwargs...) + +""" + peakproms(pks) -> NamedTuple + peakproms() -> Function + +# Optional keyword arguments +- `min`: Filter out any peak with a height smaller than `min`. +- `max`: Filter out any peak with a height greater than `min`. +- `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. + +Non-mutation version of `peakproms!`. Note that +this copies all vectors in `pks`, including the data. +This means that it is less performant. See the docstring for +`peakproms!` for more information. +""" +peakproms(pks::NamedTuple; kwargs...) = peakproms!(deepcopy(pks); kwargs...) \ No newline at end of file diff --git a/src/peakwidth.jl b/src/peakwidth.jl index 35ca59a..eda7cf8 100644 --- a/src/peakwidth.jl +++ b/src/peakwidth.jl @@ -96,7 +96,6 @@ function peakwidths!( return peaks, widths, ledge, redge end -export peakwidths! """ peakwidths(peaks, x, proms; @@ -161,4 +160,87 @@ function peakwidths( peakwidths!(_peaks, x, proms; strict=strict, relheight=relheight, min=min, max=max) end -export peakwidths \ No newline at end of file + +##!===============================================================================================!## +##!========================================== New API ==========================================!## +##!===============================================================================================!## + +""" + peakwidths!(pks) -> NamedTuple + peakwidths!() -> Function + +# Optional keyword arguments +- `min`: Filter out any peak with a height smaller than `min`. +- `max`: Filter out any peak with a height greater than `min`. +- `relheight`: How far up to measure width, as fraction of the peak prominence. Defaults to `0.5`. +- `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. + +Find the widths of the peaks in `pks`, and filter out any peak +with a width smaller than `min` or greater than `max`. +The widths are returned in the field `:widths` of the returned named tuple. +The edges of the peaks are also added in the field `:edges`. + +If the positional argument `pks` is omitted, a function is returned such +that `peakwidths!(pks)` is equivalent to `pks |> peakwidths!`. + +Note: If `pks` does not have a field `proms`, it is added. This is +because it is needed to calculate the peak width. + +Note: This function mutates the vectors stored in the NamedTuple `pks`, +and not the named tuple itself. + +See also: [`peakproms!`](@ref), [`peakheights!`](@ref) + +# Examples +```jldoctest +julia> data = [1, 5, 1, 3, 2]; + +julia> pks = findmaxima(data); + +julia> pks = peakwidths!(pks) +(indices = [2, 4], heights = [5, 3], data = [1, 5, 1, 3, 2], proms = Union{Missing, Int64}[4, 1], widths = [1.0, 0.75], edges = [(1.5, 2.5), (3.75, 4.5)]) + +julia> data |> findmaxima |> peakwidths! +(indices = [2, 4], heights = [5, 3], data = [1, 5, 1, 3, 2], proms = Union{Missing, Int64}[4, 1], widths = [1.0, 0.75], edges = [(1.5, 2.5), (3.75, 4.5)]) +``` +""" +function peakwidths!(pks::NamedTuple; minwidth=nothing, maxwidth=nothing, min=minwidth, max=maxwidth, relheight=0.5, strict=true) + 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 !hasproperty(pks, :proms) # Add proms if needed + pks = peakproms!(pks; strict) + end + if xor(hasproperty(pks, :widths), hasproperty(pks, :edges)) + throw(ArgumentError("The named tuple `pks` (first argument to `peakwidths!` is expected have both the fields `:widths` and `:edges`, or to have neither of them. The provided `pks` only has one of them.")) + 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, min, max, :widths) + return pks +end +peakwidths!(; kwargs...) = pks -> peakwidths!(pks; kwargs...) + +""" + peakwidths(pks) -> NamedTuple + peakwidths() -> Function + +# Optional keyword arguments +- `min`: Filter out any peak with a height smaller than `min`. +- `max`: Filter out any peak with a height greater than `min`. +- `relheight`: How far up to measure width, as fraction of the peak prominence. Defaults to `0.5`. +- `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. + +Non-mutation version of `peakwidths!`. Note that +this copies all vectors in `pks`, including the data. +This means that it is less performant. See the docstring for +`peakwidths!` for more information. +""" +peakwidths(pks::NamedTuple; kwargs...) = peakwidths!(deepcopy(pks); kwargs...) \ No newline at end of file From c1d553d2f395256e6642629dd5b738f50e664925 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Sun, 26 Nov 2023 18:02:21 +0100 Subject: [PATCH 33/63] Do not include api_work, which does not exist --- src/Peaks.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Peaks.jl b/src/Peaks.jl index 54feeaa..a2d1152 100644 --- a/src/Peaks.jl +++ b/src/Peaks.jl @@ -20,7 +20,6 @@ include("utils.jl") include("peakprom.jl") include("peakwidth.jl") include("peakheight.jl") -include("api_rework.jl") include("plot.jl") end # module Peaks From 676a0b5db26775e7c03f62f1fa57922334e0e969 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Sun, 26 Nov 2023 18:13:13 +0100 Subject: [PATCH 34/63] fixed a couple copy-paste errors --- src/peakheight.jl | 8 ++++---- test/manual_tests.jl | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/peakheight.jl b/src/peakheight.jl index 2fda367..136930a 100644 --- a/src/peakheight.jl +++ b/src/peakheight.jl @@ -27,10 +27,10 @@ function peakheights!( minheight=nothing, maxheight=nothing, min=minheight, max=maxheight ) where {T} - if !isnothing(minprom) + if !isnothing(minheight) Base.depwarn("Keyword `minheight` has been renamed to `min`", :peakheights!) end - if !isnothing(maxprom) + 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))")) @@ -76,10 +76,10 @@ function peakheights( minheight=nothing, maxheight=nothing, min=minheight, max=maxheight ) - if !isnothing(minprom) + if !isnothing(minheight) Base.depwarn("Keyword `minheight` has been renamed to `min`", :peakheights!) end - if !isnothing(maxprom) + if !isnothing(maxheight) Base.depwarn("Keyword `maxheight` has been renamed to `max`", :peakheights!) end peakheights!(copy(peaks), copy(heights); min=min, max=max) diff --git a/test/manual_tests.jl b/test/manual_tests.jl index 21135b1..c6241f1 100644 --- a/test/manual_tests.jl +++ b/test/manual_tests.jl @@ -32,7 +32,7 @@ data|>findmaxima|>peakheights! run_tests = true if run_tests using Pkg - pkg_path = joinpath(homedir(), ".julia", "dev", "Peaks") + pkg_path = joinpath(homedir(), ".julia", "dev", "Peaks.jl") Pkg.activate("PeaksTestEnv"; shared=true) Pkg.develop(path=pkg_path) Pkg.add(["OffsetArrays", "Plots"]) From b70804bbc264aceb93cd8cc413b34309b4dbeec6 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Mon, 22 Jan 2024 14:24:44 -0800 Subject: [PATCH 35/63] Revert function reordering (clarify changes in docstrings) --- src/peakheight.jl | 85 +++++++++++++-------------- src/peakprom.jl | 145 ++++++++++++++++++++++----------------------- src/peakwidth.jl | 146 ++++++++++++++++++++++------------------------ 3 files changed, 180 insertions(+), 196 deletions(-) diff --git a/src/peakheight.jl b/src/peakheight.jl index 136930a..0359b25 100644 --- a/src/peakheight.jl +++ b/src/peakheight.jl @@ -1,11 +1,11 @@ """ - peakheights!(peaks, heights; + peakheights(peaks, heights; minheight=nothing, maxheight=nothing ) -> (peaks, heights) -Modify and return `peaks` and `heights` by removing peaks that are less than `minheight` or greater -than `maxheight`. +Return a copy of `peaks` and `heights` where peak heights are removed if less than +`minheight` and/or greater than `maxheight`. See also: [`peakprom`](@ref), [`peakwidths`](@ref), [`findmaxima`](@ref) @@ -16,44 +16,35 @@ julia> x = [0,5,2,3,3,1,4,0]; julia> xpks, vals = 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> xpks, vals +julia> peakheights(xpks, vals; maxheight=4) ([4, 7], [3, 4]) + +julia> peakheights(xpks, vals; minheight=4.5) +([2], [5]) ``` """ -function peakheights!( - peaks::Vector{Int}, heights::AbstractVector{T}; - minheight=nothing, maxheight=nothing, +function peakheights( + peaks::AbstractVector{Int}, heights::AbstractVector; + 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(min) || !isnothing(max) - lo = something(min, typemin(Base.nonmissingtype(T))) - up = something(max, typemax(Base.nonmissingtype(T))) - matched = findall(x -> !(lo ≤ x ≤ up), heights) - deleteat!(peaks, matched) - deleteat!(heights, matched) - end - - return peaks, heights + peakheights!(copy(peaks), copy(heights); min=min, max=max) end - """ - peakheights(peaks, heights; + peakheights!(peaks, heights; minheight=nothing, maxheight=nothing ) -> (peaks, heights) -Return a copy of `peaks` and `heights` where peak heights are removed if less than -`minheight` and/or greater than `maxheight`. +Modify and return `peaks` and `heights` by removing peaks that are less than `minheight` or greater +than `maxheight`. See also: [`peakprom`](@ref), [`peakwidths`](@ref), [`findmaxima`](@ref) @@ -64,30 +55,34 @@ julia> x = [0,5,2,3,3,1,4,0]; julia> xpks, vals = 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!(xpks, vals; maxheight=4); -julia> peakheights(xpks, vals; minheight=4.5) -([2], [5]) +julia> xpks, vals +([4, 7], [3, 4]) ``` """ -function peakheights( - peaks::AbstractVector{Int}, heights::AbstractVector; - minheight=nothing, maxheight=nothing, +function peakheights!( + peaks::Vector{Int}, heights::AbstractVector{T}; + 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 - peakheights!(copy(peaks), copy(heights); min=min, max=max) -end + length(peaks) == length(heights) || throw(DimensionMismatch("length of `peaks`, $(length(peaks)), does not match the length of `heights`, $(length(heights))")) + if !isnothing(min) || !isnothing(max) + lo = something(min, typemin(Base.nonmissingtype(T))) + up = something(max, typemax(Base.nonmissingtype(T))) + matched = findall(x -> !(lo ≤ x ≤ up), heights) + deleteat!(peaks, matched) + deleteat!(heights, matched) + end -##!===============================================================================================!## -##!========================================== New API ==========================================!## -##!===============================================================================================!## + return peaks, heights +end """ peakheights!(pks) -> NamedTuple @@ -97,16 +92,16 @@ end - `min`: Filter out any peak with a height smaller than `min`. - `max`: Filter out any peak with a height greater than `min`. -Find the heights of the peaks in `pks`, and filter out any peak +Find the heights of the peaks in `pks`, and filter out any peak with a heights smaller than `min` or greater than `max`. -Note that because the peaks returned by `findpeaks` already have -the feature `heights` calculated, this function is mainly useful to +Note that because the peaks returned by `findpeaks` already have +the feature `heights` calculated, this function is mainly useful to filter peaks by a minimum and/or maximum height. If the positional argument `pks` is omitted, a function is returned such that `peakheights!(pks)` is equivalent to `pks |> peakheights!`. -Note: This function mutates the vectors stored in the NamedTuple `pks`, +Note: This function mutates the vectors stored in the NamedTuple `pks`, and not the named tuple itself. See also: [`peakproms!`](@ref), [`peakwidths!`](@ref) @@ -145,9 +140,9 @@ peakheights!(; kwargs...) = pks -> peakheights!(pks; kwargs...) - `min`: Filter out any peak with a height smaller than `min`. - `max`: Filter out any peak with a height greater than `min`. -Non-mutation version of `peakheights!`. Note that -this copies all vectors in `pks`, including the data. -This means that it is less performant. See the docstring for +Non-mutation version of `peakheights!`. Note that +this copies all vectors in `pks`, including the data. +This means that it is less performant. See the docstring for `peakheights!` for more information. """ -peakheights(pks::NamedTuple; kwargs...) = peakheights!(deepcopy(pks); kwargs...) \ No newline at end of file +peakheights(pks::NamedTuple; kwargs...) = peakheights!(deepcopy(pks); kwargs...) diff --git a/src/peakprom.jl b/src/peakprom.jl index 1f7ec3a..5df4a17 100644 --- a/src/peakprom.jl +++ b/src/peakprom.jl @@ -1,3 +1,65 @@ +""" + peakproms(peaks, x; + strict=true, + minprom=nothing, + maxprom=nothing + ) -> (peaks, proms) + +Calculate the prominences of `peaks` in `x`, and removing peaks with prominences less than +`minprom` and/or greater than `maxprom`. + +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. + +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`. + +See also: [`findminima`](@ref), [`findmaxima`](@ref), [`peakproms!`](@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> x = [missing,5,2,3,3,1,4,0]; + +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]) +``` +""" +function peakproms(peaks::AbstractVector{Int}, x::AbstractVector{T}; + 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, min=min, max=max) +end + """ peakproms!(peaks, x; strict=true, @@ -12,7 +74,7 @@ prominences. See also: [`peakproms`](@ref), [`findminima`](@ref), [`findmaxima`](@ref) """ function peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; - strict=true, minprom=nothing, maxprom=nothing, + strict=true, minprom=nothing, maxprom=nothing, min=minprom, max=maxprom ) where {T} if !isnothing(minprom) @@ -132,75 +194,6 @@ function peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; return peaks, proms end -""" - peakproms(peaks, x; - strict=true, - minprom=nothing, - maxprom=nothing - ) -> (peaks, proms) - -Calculate the prominences of `peaks` in `x`, and removing peaks with prominences less than -`minprom` and/or greater than `maxprom`. - -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. - -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`. - -See also: [`findminima`](@ref), [`findmaxima`](@ref), [`peakproms!`](@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> x = [missing,5,2,3,3,1,4,0]; - -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]) -``` -""" -function peakproms(peaks::AbstractVector{Int}, x::AbstractVector{T}; - 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, min=min, max=max) -end - - -##!===============================================================================================!## -##!========================================== New API ==========================================!## -##!===============================================================================================!## - - - """ peakproms!(pks) -> NamedTuple peakproms!() -> Function @@ -210,14 +203,14 @@ end - `max`: Filter out any peak with a height greater than `min`. - `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. -Find the prominences of the peaks in `pks`, and filter out any peak +Find the prominences of the peaks in `pks`, and filter out any peak with a prominence smaller than `min` or greater than `max`. The prominences are returned in the field `:proms` of the returned named tuple. If the positional argument `pks` is omitted, a function is returned such that `peakproms!(pks)` is equivalent to `pks |> peakproms!`. -Note: This function mutates the vectors stored in the NamedTuple `pks`, +Note: This function mutates the vectors stored in the NamedTuple `pks`, and not the named tuple itself. See also: [`peakwidths!`](@ref), [`peakheights!`](@ref) @@ -262,9 +255,9 @@ peakproms!(; kwargs...) = pks -> peakproms!(pks; kwargs...) - `max`: Filter out any peak with a height greater than `min`. - `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. -Non-mutation version of `peakproms!`. Note that -this copies all vectors in `pks`, including the data. -This means that it is less performant. See the docstring for +Non-mutation version of `peakproms!`. Note that +this copies all vectors in `pks`, including the data. +This means that it is less performant. See the docstring for `peakproms!` for more information. """ -peakproms(pks::NamedTuple; kwargs...) = peakproms!(deepcopy(pks); kwargs...) \ No newline at end of file +peakproms(pks::NamedTuple; kwargs...) = peakproms!(deepcopy(pks); kwargs...) diff --git a/src/peakwidth.jl b/src/peakwidth.jl index eda7cf8..3cf512a 100644 --- a/src/peakwidth.jl +++ b/src/peakwidth.jl @@ -1,3 +1,67 @@ +""" + 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`, 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. + +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. + +See also: [`peakprom`](@ref), [`findminima`](@ref), [`findmaxima`](@ref) + +# Examples +```jldoctest +julia> x = [0,1,0,-1.]; + +julia> xpks = argmaxima(x) +1-element Vector{Int64}: + 2 + +julia> peakwidths(xpks, x, [1]) +([2], [1.0], [1.5], [2.5]) + +julia> x[3] = NaN; + +julia> peakwidths(xpks, x, [1]) +([2], [NaN], [1.5], [NaN]) + +julia> peakwidths(xpks, x, [1]; strict=false) +([2], [1.0], [1.5], [2.5]) +``` +""" +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) + 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, + min=min, max=max) +end + """ peakwidths!(peaks, x, proms; strict=true, @@ -97,74 +161,6 @@ function peakwidths!( return peaks, widths, ledge, redge 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`, 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. - -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. - -See also: [`peakprom`](@ref), [`findminima`](@ref), [`findmaxima`](@ref) - -# Examples -```jldoctest -julia> x = [0,1,0,-1.]; - -julia> xpks = argmaxima(x) -1-element Vector{Int64}: - 2 - -julia> peakwidths(xpks, x, [1]) -([2], [1.0], [1.5], [2.5]) - -julia> x[3] = NaN; - -julia> peakwidths(xpks, x, [1]) -([2], [NaN], [1.5], [NaN]) - -julia> peakwidths(xpks, x, [1]; strict=false) -([2], [1.0], [1.5], [2.5]) -``` -""" -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) - 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, - min=min, max=max) -end - -##!===============================================================================================!## -##!========================================== New API ==========================================!## -##!===============================================================================================!## - """ peakwidths!(pks) -> NamedTuple peakwidths!() -> Function @@ -175,7 +171,7 @@ end - `relheight`: How far up to measure width, as fraction of the peak prominence. Defaults to `0.5`. - `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. -Find the widths of the peaks in `pks`, and filter out any peak +Find the widths of the peaks in `pks`, and filter out any peak with a width smaller than `min` or greater than `max`. The widths are returned in the field `:widths` of the returned named tuple. The edges of the peaks are also added in the field `:edges`. @@ -183,10 +179,10 @@ The edges of the peaks are also added in the field `:edges`. If the positional argument `pks` is omitted, a function is returned such that `peakwidths!(pks)` is equivalent to `pks |> peakwidths!`. -Note: If `pks` does not have a field `proms`, it is added. This is +Note: If `pks` does not have a field `proms`, it is added. This is because it is needed to calculate the peak width. -Note: This function mutates the vectors stored in the NamedTuple `pks`, +Note: This function mutates the vectors stored in the NamedTuple `pks`, and not the named tuple itself. See also: [`peakproms!`](@ref), [`peakheights!`](@ref) @@ -238,9 +234,9 @@ peakwidths!(; kwargs...) = pks -> peakwidths!(pks; kwargs...) - `relheight`: How far up to measure width, as fraction of the peak prominence. Defaults to `0.5`. - `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. -Non-mutation version of `peakwidths!`. Note that -this copies all vectors in `pks`, including the data. -This means that it is less performant. See the docstring for +Non-mutation version of `peakwidths!`. Note that +this copies all vectors in `pks`, including the data. +This means that it is less performant. See the docstring for `peakwidths!` for more information. """ -peakwidths(pks::NamedTuple; kwargs...) = peakwidths!(deepcopy(pks); kwargs...) \ No newline at end of file +peakwidths(pks::NamedTuple; kwargs...) = peakwidths!(deepcopy(pks); kwargs...) From f9ab0f2ae276d55f2e44bde4342886c1b4f3e678 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Mon, 22 Jan 2024 17:36:35 -0800 Subject: [PATCH 36/63] Example compromise docstrings and ordering --- src/peakheight.jl | 138 ++++++++++++++++++++++------------------------ 1 file changed, 67 insertions(+), 71 deletions(-) diff --git a/src/peakheight.jl b/src/peakheight.jl index 0359b25..22b18b8 100644 --- a/src/peakheight.jl +++ b/src/peakheight.jl @@ -1,30 +1,35 @@ """ - 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`. -See also: [`peakprom`](@ref), [`peakwidths`](@ref), [`findmaxima`](@ref) +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 the +fields in `pks`. `pks` must have `:indices` and `:heights` fields, at a minimum. +The following fields will also be copied and filtered if they exist: `:proms`, `:widths`, +and `:edges`. + +See also: [`peakprom`](@ref), [`peakwidths`](@ref), [`findmaxima`](@ref), +[`filterpeaks`](@ref) # Examples ```jldoctest julia> x = [0,5,2,3,3,1,4,0]; -julia> xpks, vals = findmaxima(x) +julia> indices, heights = 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(indices, heights; max=4) ([4, 7], [3, 4]) -julia> peakheights(xpks, vals; minheight=4.5) -([2], [5]) +julia> peakheights((;indices, heights, data=x); min=4) +(indices = [2], heights = [5], data = [0, 5, 2, 3, 3, 1, 4, 0]) ``` """ function peakheights( - peaks::AbstractVector{Int}, heights::AbstractVector; + indices::AbstractVector{Int}, heights::AbstractVector; minheight=nothing, maxheight=nothing, min=minheight, max=maxheight ) @@ -34,31 +39,56 @@ function peakheights( if !isnothing(maxheight) Base.depwarn("Keyword `maxheight` has been renamed to `max`", :peakheights!) end - peakheights!(copy(peaks), copy(heights); min=min, max=max) + peakheights!(copy(indices), copy(heights); min=min, max=max) end +peakheights(pks::NamedTuple; kwargs...) = peakheights!(deepcopy(pks); kwargs...) + """ - peakheights!(peaks, heights; - minheight=nothing, - maxheight=nothing - ) -> (peaks, heights) + peakheights(; min=nothing, max=nothing) -> Function + +Create a function that filters the fields of a copy of the singular NamedTuple argument +`pks`. -Modify and return `peaks` and `heights` by removing peaks that are less than `minheight` or greater -than `maxheight`. +This allows for piping several `Peaks.jl` functions together. -See also: [`peakprom`](@ref), [`peakwidths`](@ref), [`findmaxima`](@ref) +## Examples +```julia-repl +julia> pks |> peakheights(;min=4) +(indices = [2], heights = [5], data = [1, 5, 1, 3, 2]) +``` +""" +peakheights(; kwargs...) = function curried_peakheights(pks) + return peakheights(deepcopy(pks); kwargs...) +end + +""" + 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` +or greater than `max`. + +If a NamedTuple `pks` is given, a new NamedTuple is returned with the same fields as in +`pks`. `pks` must have `:indices` and `:heights` fields, at a minimum. The +following fields will also be filtered (mutated), if they exist: `:proms`, `:widths`, and +`:edges`. + +See also: [`peakprom`](@ref), [`peakwidths`](@ref), [`findmaxima`](@ref), +[`filterpeaks!`](@ref) # Examples ```jldoctest julia> x = [0,5,2,3,3,1,4,0]; -julia> xpks, vals = findmaxima(x) +julia> indices, heights = 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!(indices, heights; max=4) +(indices = [2], heights = [5], data = [0, 5, 2, 3, 3, 1, 4, 0]) -julia> xpks, vals -([4, 7], [3, 4]) +julia> peakheights!((;indices, heights, data=x); max=4) +(indices = [2], heights = [5], data = [0, 5, 2, 3, 3, 1, 4, 0]) ``` """ function peakheights!( @@ -84,42 +114,6 @@ function peakheights!( return peaks, heights end -""" - peakheights!(pks) -> NamedTuple - peakheights!() -> Function - -# Optional keyword arguments -- `min`: Filter out any peak with a height smaller than `min`. -- `max`: Filter out any peak with a height greater than `min`. - -Find the heights of the peaks in `pks`, and filter out any peak -with a heights smaller than `min` or greater than `max`. -Note that because the peaks returned by `findpeaks` already have -the feature `heights` calculated, this function is mainly useful to -filter peaks by a minimum and/or maximum height. - -If the positional argument `pks` is omitted, a function is returned such -that `peakheights!(pks)` is equivalent to `pks |> peakheights!`. - -Note: This function mutates the vectors stored in the NamedTuple `pks`, -and not the named tuple itself. - -See also: [`peakproms!`](@ref), [`peakwidths!`](@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]) - -julia> pks = peakheights!(pks, min=4) -(indices = [2], heights = [5], data = [1, 5, 1, 3, 2]) - -julia> data |> findmaxima |> peakheights!(min=4) -(indices = [2], heights = [5], data = [1, 5, 1, 3, 2]) -``` -""" function peakheights!(pks::NamedTuple; minheight=nothing, maxheight=nothing, min=minheight, max=maxheight) if !isnothing(minheight) Base.depwarn("Keyword `minheight` has been renamed to `min`", :peakheights!) @@ -130,19 +124,21 @@ function peakheights!(pks::NamedTuple; minheight=nothing, maxheight=nothing, min filterpeaks!(pks, min, max, :heights) return pks end -peakheights!(; kwargs...) = pks -> peakheights!(pks; kwargs...) """ - peakheights(pks) -> NamedTuple - peakheights() -> Function + peakheights!(; min=nothing, max=nothing) -> Function + +Create a function that filters (mutates) the fields of a singular NamedTuple argument `pks`. -# Optional keyword arguments -- `min`: Filter out any peak with a height smaller than `min`. -- `max`: Filter out any peak with a height greater than `min`. +This allows for piping several `Peaks.jl` functions together. -Non-mutation version of `peakheights!`. Note that -this copies all vectors in `pks`, including the data. -This means that it is less performant. See the docstring for -`peakheights!` for more information. +# Examples +```jldoctest +julia> pks |> peakheights!(; max=4) +(indices = [2], heights = [5], data = [0, 5, 2, 3, 3, 1, 4, 0]) +``` """ -peakheights(pks::NamedTuple; kwargs...) = peakheights!(deepcopy(pks); kwargs...) +peakheights!(; kwargs...) = function curried_peakheights!(pks) + peakheights!(pks; kwargs...) +end + From 75a588e0d0016de710f2eedcaa677cb30478be9a Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Mon, 22 Jan 2024 21:24:53 -0800 Subject: [PATCH 37/63] Revert random change (fixes tests) --- src/peakwidth.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/peakwidth.jl b/src/peakwidth.jl index 3cf512a..6db1678 100644 --- a/src/peakwidth.jl +++ b/src/peakwidth.jl @@ -106,8 +106,7 @@ function peakwidths!( V1 = promote_type(T, U) _bad = Missing <: V1 ? missing : float(Int)(NaN) - V_ = promote_type(V1, typeof(_bad)) # temp variable used in next line - V = typeof(one(V_)/one(V_)) # We will insert the results of divisions. + V = promote_type(V1, typeof(_bad)) ledge = similar(proms, V) redge = similar(proms, V) From 32b16e3e53bdfc8cb695e5532ef08073831df6ae Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Thu, 25 Jan 2024 10:23:49 +0100 Subject: [PATCH 38/63] Separate docstring examples for APIs --- src/peakheight.jl | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/peakheight.jl b/src/peakheight.jl index 22b18b8..623c6e6 100644 --- a/src/peakheight.jl +++ b/src/peakheight.jl @@ -15,6 +15,19 @@ See also: [`peakprom`](@ref), [`peakwidths`](@ref), [`findmaxima`](@ref), [`filterpeaks`](@ref) # Examples + +## NamedTuple API: +```jldoctest +julia> x = [0,5,2,3,3,1,4,0]; + +julia> nt = findmaxima(x) +(indices = [2, 4, 7], heights = [5, 3, 4], data = [0, 5, 2, 3, 3, 1, 4, 0]) + +julia> peakheights(nt; max=4) +(indices = [4, 7], heights = [3, 4], data = [0, 5, 2, 3, 3, 1, 4, 0]) +``` + +## Seperate vector API: ```jldoctest julia> x = [0,5,2,3,3,1,4,0]; @@ -23,9 +36,6 @@ julia> indices, heights = findmaxima(x) julia> peakheights(indices, heights; max=4) ([4, 7], [3, 4]) - -julia> peakheights((;indices, heights, data=x); min=4) -(indices = [2], heights = [5], data = [0, 5, 2, 3, 3, 1, 4, 0]) ``` """ function peakheights( From e98362ddd47fe5febe3763de174d1f472c9dffcb Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Thu, 25 Jan 2024 10:36:23 +0100 Subject: [PATCH 39/63] Improve curried docstring --- src/peakheight.jl | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/peakheight.jl b/src/peakheight.jl index 623c6e6..c5178a2 100644 --- a/src/peakheight.jl +++ b/src/peakheight.jl @@ -57,15 +57,16 @@ peakheights(pks::NamedTuple; kwargs...) = peakheights!(deepcopy(pks); kwargs...) """ peakheights(; min=nothing, max=nothing) -> Function -Create a function that filters the fields of a copy of the singular NamedTuple argument -`pks`. - -This allows for piping several `Peaks.jl` functions together. +Return a function that acts just like the NamedTuple method for +`peakheights`, but with a not-yet-specified first argument (named tuple). +This allows a convenient workflow for chaining operations. ## Examples -```julia-repl -julia> pks |> peakheights(;min=4) -(indices = [2], heights = [5], data = [1, 5, 1, 3, 2]) +```jldoctest +julia> x = [0,5,2,3,3,1,4,0]; + +julia> nt = findmaxima(x) |> peakheights(max=4) +(indices = [4, 7], heights = [3, 4], data = [0, 5, 2, 3, 3, 1, 4, 0]) ``` """ peakheights(; kwargs...) = function curried_peakheights(pks) From aaf8b51035259b648c7478201793cfb5ee2877d0 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Thu, 25 Jan 2024 10:53:29 +0100 Subject: [PATCH 40/63] make known_fields a const --- src/utils.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 447edaf..b4293b1 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,7 +1,7 @@ -known_fields() = (:indices, :proms, :heights, :widths, :edges) +const known_fields = (:indices, :proms, :heights, :widths, :edges) function check_known_fields_equal_length(pks::NamedTuple) - features_to_filter = known_fields() + features_to_filter = known_fields feature_lengths = [length(pks[feature]) for feature in features_to_filter if hasproperty(pks, feature)] @@ -15,9 +15,9 @@ function check_known_fields_equal_length(pks::NamedTuple) end function check_has_known_field(pks::NamedTuple) - if !any(hasproperty(pks, prop) for prop in known_fields()) + if !any(hasproperty(pks, prop) for prop in known_fields) throw(ArgumentError( - "Attempting to filter a named tuple `pks` that contains none of the known fields $(known_fields()). Because + "Attempting to filter a named tuple `pks` that contains none of the known fields $known_fields. Because this is thought to be an error, this error is thrown to help catch the original error." )) end @@ -78,7 +78,7 @@ function filterpeaks!(pks::NamedTuple, mask::Union{BitVector, Vector{Bool}}) )) end - for field in known_fields() # Only risk mutating fields added by this package + 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 @@ -148,7 +148,7 @@ function filterpeaks!(pred::Function, pks::NamedTuple) return pred(nt_slice) end - for field in known_fields() # Only risk mutating fields added by this package + 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 From 613d0f0693dde224c980d95f503c65d1901306f4 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Thu, 25 Jan 2024 10:59:03 +0100 Subject: [PATCH 41/63] explain what a named tuple slice is --- src/utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index b4293b1..54f3851 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -111,7 +111,7 @@ end filterpeaks!(pred, pks) -> NamedTuple Apply a predicate function `pred` to named tuple slices to get a filter-mask. -By "named tuple slice" we refer to +An example of a "named tuple slice" is `(indices=5, heights=3, proms=2)`. If `pred` returns `false` for peak number `i`, element `i` is removed from the known fields of `pks`. From 8ac658db9aeed883f905d3b185f8f907f329c2d8 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Thu, 25 Jan 2024 11:00:38 +0100 Subject: [PATCH 42/63] Make filterpeaks signature match --- src/utils.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils.jl b/src/utils.jl index 54f3851..3043c05 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -87,7 +87,7 @@ end # This method gets no docstring, as it is intended for internal use. """ - filterpeaks!(pks, min, max, feature) + filterpeaks!(pks, feature; min=nothing, max=nothing) Calculate `mask` as `[min < x < max for x in pks.feature]`, and call `filterpeaks!(pks, mask)`. @@ -97,7 +97,7 @@ by a minimal or maximal value of some peak feature, concider using the keyword arguments `min` and `max` in the function that calculated that feature, such as `peakproms!(pks, min=2)`. """ -function filterpeaks!(pks::NamedTuple, min, max, feature::Symbol) +function filterpeaks!(pks::NamedTuple, feature::Symbol; min=nothing, max=nothing) if !isnothing(min) || !isnothing(max) lo = something(min, zero(eltype(pks.data))) up = something(max, typemax(Base.nonmissingtype(eltype(pks.data)))) From 98482050616a4d39dd1bff667a859963850d3d90 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Thu, 25 Jan 2024 11:03:21 +0100 Subject: [PATCH 43/63] swap check order filterpeaks! --- src/utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index 3043c05..cc71c7b 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -138,8 +138,8 @@ julia> filterpeaks!(pks) do nt_slice """ function filterpeaks!(pred::Function, pks::NamedTuple) - check_known_fields_equal_length(pks) check_has_known_field(pks) + check_known_fields_equal_length(pks) mask = map(eachindex(pks[1])) do i # :data is included in the nt_slice, but that should not be a problem From 42aba3c662a94ea62eae63db99476e69abc31bc5 Mon Sep 17 00:00:00 2001 From: KronosTheLate Date: Thu, 25 Jan 2024 11:03:42 +0100 Subject: [PATCH 44/63] Swap check order filterpeaks! --- src/utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.jl b/src/utils.jl index cc71c7b..22f4651 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -66,8 +66,8 @@ 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_known_fields_equal_length(pks) check_has_known_field(pks) + check_known_fields_equal_length(pks) # At this point we know that all feature_length are equal, and do not need to check it again # pks[1] returns the indices. From 36b71c79459b885d351e95702a34012511466533 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 27 Feb 2024 16:16:49 -0800 Subject: [PATCH 45/63] [nfc] whitespace changes --- README.md | 1 + src/Peaks.jl | 15 +++------------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 74cfa7c..55d0f53 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Peaks.jl + [![version](https://juliahub.com/docs/Peaks/version.svg)](https://juliahub.com/ui/Packages/Peaks/3TWUM) [![pkgeval](https://juliahub.com/docs/Peaks/pkgeval.svg)](https://juliahub.com/ui/Packages/Peaks/3TWUM) [![stable-docs](https://img.shields.io/badge/docs-stable-blue.svg)](https://halleysfifthinc.github.io/Peaks.jl/stable) diff --git a/src/Peaks.jl b/src/Peaks.jl index a2d1152..60c633e 100644 --- a/src/Peaks.jl +++ b/src/Peaks.jl @@ -2,18 +2,9 @@ module Peaks using Compat -# Function related to locating peaks -export argmaxima, argminima -export maxima, minima -export findmaxima, findminima -export findnextmaxima, findnextminima -export ismaxima, isminima - -# Functions related to working with a set of peaks -export peakproms, peakproms! -export peakwidths, peakwidths! -export peakheights, peakheights! -export filterpeaks! +export argmaxima, argminima, maxima, minima, findmaxima, findminima, findnextmaxima, + findnextminima, peakproms, peakproms!, peakwidths, peakwidths!, peakheights, + peakheights!, ismaxima, isminima, filterpeaks! include("minmax.jl") include("utils.jl") From df11d3ae61b535112d970c575c213477fa4ec571 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 27 Feb 2024 16:22:24 -0800 Subject: [PATCH 46/63] Update/rewrite docstrings and doctests --- src/minmax.jl | 30 ++++---- src/peakheight.jl | 78 +++++++++----------- src/peakprom.jl | 175 ++++++++++++++++++++------------------------ src/peakwidth.jl | 183 +++++++++++++++++++++------------------------- 4 files changed, 212 insertions(+), 254 deletions(-) diff --git a/src/minmax.jl b/src/minmax.jl index 356d4ba..aba2378 100644 --- a/src/minmax.jl +++ b/src/minmax.jl @@ -186,13 +186,14 @@ function maxima( end """ - findmaxima(x[, w=1; strict=true]) -> NamedTuple + 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. The -returned named tuple contains the fields `indices, heights, data`, and -contains what the fieldnames suggest. Note that the vector stored -in the field `:data` is the original data vector, and not a copy. +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`](@ref) 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. @@ -209,7 +210,7 @@ julia> pks = findmaxima(data) """ function findmaxima(x, w::Int=1; strict::Bool=true) idxs = argmaxima(x, w; strict=strict) - return (indices=idxs, heights=x[idxs], data=x) + return (;indices=idxs, heights=x[idxs], data=x) end """ @@ -323,13 +324,14 @@ function minima( end """ - findminima(x[, w=1; strict=true]) -> NamedTuple + 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. The -returned named tuple contains the fields `indices, heights, data`, and -contains what the fieldnames suggest. Note that the vector stored -in the field `:data` is the original data vector, and not a copy. +Returns a [`NamedTuple`](@ref) 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. @@ -346,5 +348,5 @@ julia> valleys = findminima(data) """ function findminima(x, w::Int=1; strict::Bool=true) idxs = argminima(x, w; strict=strict) - return (indices=idxs, heights=x[idxs], data=x) + return (;indices=idxs, heights=x[idxs], data=x) end diff --git a/src/peakheight.jl b/src/peakheight.jl index df9001e..78ce176 100644 --- a/src/peakheight.jl +++ b/src/peakheight.jl @@ -6,35 +6,27 @@ 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 the -fields in `pks`. `pks` must have `:indices` and `:heights` fields, at a minimum. -The following fields will also be copied and filtered if they exist: `:proms`, `:widths`, -and `:edges`. +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) [`filterpeaks`](@ref) # Examples - -## NamedTuple API: ```jldoctest julia> x = [0,5,2,3,3,1,4,0]; -julia> nt = findmaxima(x) +julia> pks = findmaxima(x) (indices = [2, 4, 7], heights = [5, 3, 4], data = [0, 5, 2, 3, 3, 1, 4, 0]) -julia> peakheights(nt; max=4) +julia> peakheights(pks; max=4) (indices = [4, 7], heights = [3, 4], data = [0, 5, 2, 3, 3, 1, 4, 0]) -``` -## Seperate vector API: -```jldoctest -julia> x = [0,5,2,3,3,1,4,0]; +julia> inds, heights = pks; -julia> indices, heights = findmaxima(x) -(indices = [2, 4, 7], heights = [5, 3, 4], data = [0, 5, 2, 3, 3, 1, 4, 0]) - -julia> peakheights(indices, heights; max=4) +julia> inds, heights = peakheights(indices, heights; max=4) ([4, 7], [3, 4]) ``` """ @@ -55,21 +47,18 @@ end peakheights(pks::NamedTuple; kwargs...) = peakheights!(deepcopy(pks); kwargs...) """ - peakheights(; min=nothing, max=nothing) -> Function + peakheights(; [min, max]) -> Function -Return a function that acts just like the NamedTuple method for -`peakheights`, but with a not-yet-specified first argument (named tuple). -This allows a convenient workflow for chaining operations. +Create a function, `f(pks::NamedTuple)`, that copies and filters the peak heights of its +argument, `pks`, using any given keyword arguments. -## Examples +# Examples ```jldoctest -julia> x = [0,5,2,3,3,1,4,0]; - -julia> nt = findmaxima(x) |> peakheights(max=4) +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) +peakheights(; kwargs...) = function _curried_peakheights(pks) return peakheights(deepcopy(pks); kwargs...) end @@ -78,12 +67,12 @@ end peakheights!(pks::NamedTuple; [min, max]) -> NamedTuple Filter (mutate) and return `indices` and `heights` by removing peaks that are less than `min` -or greater than `max`. +and/or greater than `max`. -If a NamedTuple `pks` is given, a new NamedTuple is returned with the same fields as in -`pks`. `pks` must have `:indices` and `:heights` fields, at a minimum. The -following fields will also be filtered (mutated), if they exist: `:proms`, `:widths`, and -`:edges`. +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) @@ -92,14 +81,16 @@ See also: [`peakproms`](@ref), [`peakwidths`](@ref), [`findmaxima`](@ref) ```jldoctest julia> x = [0,5,2,3,3,1,4,0]; -julia> indices, heights = findmaxima(x) +julia> pks = findmaxima(x) (indices = [2, 4, 7], heights = [5, 3, 4], data = [0, 5, 2, 3, 3, 1, 4, 0]) -julia> peakheights!(indices, heights; max=4) -(indices = [2], heights = [5], data = [0, 5, 2, 3, 3, 1, 4, 0]) +julia> peakheights!(pks; max=4) +(indices = [4, 7], heights = [3, 4], data = [0, 5, 2, 3, 3, 1, 4, 0]) + +julia> inds, heights = pks; -julia> peakheights!((;indices, heights, data=x); max=4) -(indices = [2], heights = [5], data = [0, 5, 2, 3, 3, 1, 4, 0]) +julia> inds, heights = peakheights!(indices, heights; min=3.5) +([7], [4]) ``` """ function peakheights!( @@ -132,24 +123,23 @@ function peakheights!(pks::NamedTuple; minheight=nothing, maxheight=nothing, min if !isnothing(maxheight) Base.depwarn("Keyword `maxheight` has been renamed to `max`", :peakheights!) end - filterpeaks!(pks, min, max, :heights) + filterpeaks!(pks, :heights; min, max) return pks end """ - peakheights!(; min=nothing, max=nothing) -> Function - -Create a function that filters (mutates) the fields of a singular NamedTuple argument `pks`. + peakheights!(; [min, max]) -> Function -This allows for piping several `Peaks.jl` functions together. +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> pks |> peakheights!(; max=4) -(indices = [2], heights = [5], data = [0, 5, 2, 3, 3, 1, 4, 0]) +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) - peakheights!(pks; kwargs...) +peakheights!(; kwargs...) = function _curried_peakheights!(pks) + return peakheights!(pks; kwargs...) end diff --git a/src/peakprom.jl b/src/peakprom.jl index 5df4a17..a352b5e 100644 --- a/src/peakprom.jl +++ b/src/peakprom.jl @@ -1,44 +1,36 @@ """ - 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> x = [missing,5,2,3,3,1,4,0]; +julia> pks = findmaxima([0,5,2,3,3,1,4,0]); -julia> peakproms(xpks, x) -([2, 4, 7], Union{Missing, Int64}[missing, 1, 3]) +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; 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}; @@ -60,18 +52,49 @@ function peakproms(peaks::AbstractVector{Int}, x::AbstractVector{T}; return peakproms!(_peaks, x; strict=strict, min=min, max=max) end +peakproms(pks::NamedTuple; kwargs...) = peakproms!(deepcopy(pks); kwargs...) + """ - peakproms!(peaks, x; - strict=true, - minprom=nothing, - maxprom=nothing - ) -> (peaks, proms) + peakproms(; [strict, min, max]) -> Function -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. +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 -See also: [`peakproms`](@ref), [`findminima`](@ref), [`findmaxima`](@ref) +""" + 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. + +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]) + +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, @@ -194,70 +217,30 @@ function peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; return peaks, proms end -""" - peakproms!(pks) -> NamedTuple - peakproms!() -> Function - -# Optional keyword arguments -- `min`: Filter out any peak with a height smaller than `min`. -- `max`: Filter out any peak with a height greater than `min`. -- `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. - -Find the prominences of the peaks in `pks`, and filter out any peak -with a prominence smaller than `min` or greater than `max`. -The prominences are returned in the field `:proms` of the returned named tuple. - -If the positional argument `pks` is omitted, a function is returned such -that `peakproms!(pks)` is equivalent to `pks |> peakproms!`. - -Note: This function mutates the vectors stored in the NamedTuple `pks`, -and not the named tuple itself. - -See also: [`peakwidths!`](@ref), [`peakheights!`](@ref) - -# Examples -```jldoctest -julia> data = [1, 5, 1, 3, 2]; - -julia> pks = findmaxima(data); - -julia> pks = peakproms!(pks) -(indices = [2, 4], heights = [5, 3], data = [1, 5, 1, 3, 2], proms = Union{Missing, Int64}[4, 1]) - -julia> data |> findmaxima |> peakproms! -(indices = [2, 4], heights = [5, 3], data = [1, 5, 1, 3, 2], proms = Union{Missing, Int64}[4, 1]) -``` -""" -function peakproms!(pks::NamedTuple; minprom=nothing, maxprom=nothing, min=minprom, max=maxprom, strict=true) - 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 +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, min, max, :proms) + filterpeaks!(pks, :proms; min, max) return pks end -peakproms!(; kwargs...) = pks -> peakproms!(pks; kwargs...) """ - peakproms(pks) -> NamedTuple - peakproms() -> Function - -# Optional keyword arguments -- `min`: Filter out any peak with a height smaller than `min`. -- `max`: Filter out any peak with a height greater than `min`. -- `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. - -Non-mutation version of `peakproms!`. Note that -this copies all vectors in `pks`, including the data. -This means that it is less performant. See the docstring for -`peakproms!` for more information. + 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(pks::NamedTuple; kwargs...) = peakproms!(deepcopy(pks); kwargs...) +peakproms!(; kwargs...) = function _curried_peakproms!(pks) + return peakproms!(pks; kwargs...) +end + diff --git a/src/peakwidth.jl b/src/peakwidth.jl index 3a86bba..fb1a47f 100644 --- a/src/peakwidth.jl +++ b/src/peakwidth.jl @@ -1,44 +1,43 @@ """ - 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], data = [0, 5, 2, 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}}[(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], Union{Missing, Float64}[NaN], Union{Missing, Float64}[NaN], Union{Missing, Float64}[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], Union{Missing, Float64}[2.25], Union{Missing, Float64}[4.0], Union{Missing, Float64}[6.25]) ``` """ function peakwidths( @@ -62,19 +61,51 @@ function peakwidths( min=min, max=max) end +peakwidths(pks::NamedTuple; kwargs...) = peakwidths!(deepcopy(pks); kwargs...) + """ - 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(; [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=2) +(indices = [2, 7], heights = [5, 4], data = [0, 5, 2, 3, 3, 1, 4, 0], widths = Union{Missing, Int64}[5, 3]) +``` +""" +peakwidths(; kwargs...) = function _curried_peakwidths(pks) + return peakwidths(pks; kwargs...) +end + +""" + 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}; @@ -160,57 +191,11 @@ function peakwidths!( return peaks, widths, ledge, redge end -""" - peakwidths!(pks) -> NamedTuple - peakwidths!() -> Function - -# Optional keyword arguments -- `min`: Filter out any peak with a height smaller than `min`. -- `max`: Filter out any peak with a height greater than `min`. -- `relheight`: How far up to measure width, as fraction of the peak prominence. Defaults to `0.5`. -- `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. - -Find the widths of the peaks in `pks`, and filter out any peak -with a width smaller than `min` or greater than `max`. -The widths are returned in the field `:widths` of the returned named tuple. -The edges of the peaks are also added in the field `:edges`. - -If the positional argument `pks` is omitted, a function is returned such -that `peakwidths!(pks)` is equivalent to `pks |> peakwidths!`. - -Note: If `pks` does not have a field `proms`, it is added. This is -because it is needed to calculate the peak width. - -Note: This function mutates the vectors stored in the NamedTuple `pks`, -and not the named tuple itself. - -See also: [`peakproms!`](@ref), [`peakheights!`](@ref) - -# Examples -```jldoctest -julia> data = [1, 5, 1, 3, 2]; - -julia> pks = findmaxima(data); - -julia> pks = peakwidths!(pks) -(indices = [2, 4], heights = [5, 3], data = [1, 5, 1, 3, 2], proms = Union{Missing, Int64}[4, 1], widths = [1.0, 0.75], edges = [(1.5, 2.5), (3.75, 4.5)]) - -julia> data |> findmaxima |> peakwidths! -(indices = [2, 4], heights = [5, 3], data = [1, 5, 1, 3, 2], proms = Union{Missing, Int64}[4, 1], widths = [1.0, 0.75], edges = [(1.5, 2.5), (3.75, 4.5)]) -``` -""" -function peakwidths!(pks::NamedTuple; minwidth=nothing, maxwidth=nothing, min=minwidth, max=maxwidth, relheight=0.5, strict=true) - 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 !hasproperty(pks, :proms) # Add proms if needed - pks = peakproms!(pks; strict) - 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("The named tuple `pks` (first argument to `peakwidths!` is expected have both the fields `:widths` and `:edges`, or to have neither of them. The provided `pks` only has one of them.")) + 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. @@ -218,24 +203,22 @@ function peakwidths!(pks::NamedTuple; minwidth=nothing, maxwidth=nothing, min=mi _, widths, leftedges, rightedges = peakwidths(pks.indices, pks.data, pks.proms; relheight, strict) pks = merge(pks, (; widths, edges=collect(zip(leftedges, rightedges)))) end - filterpeaks!(pks, min, max, :widths) + filterpeaks!(pks, :widths; min, max) return pks end -peakwidths!(; kwargs...) = pks -> peakwidths!(pks; kwargs...) """ - peakwidths(pks) -> NamedTuple - peakwidths() -> Function - -# Optional keyword arguments -- `min`: Filter out any peak with a height smaller than `min`. -- `max`: Filter out any peak with a height greater than `min`. -- `relheight`: How far up to measure width, as fraction of the peak prominence. Defaults to `0.5`. -- `strict`: How to handle `NaN` and `missing` values. See documentation for more details. Default to `true`. - -Non-mutation version of `peakwidths!`. Note that -this copies all vectors in `pks`, including the data. -This means that it is less performant. See the docstring for -`peakwidths!` for more information. + 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=2) +``` """ -peakwidths(pks::NamedTuple; kwargs...) = peakwidths!(deepcopy(pks); kwargs...) +peakwidths!(; kwargs...) = function _curried_peakwidths!(pks) + return peakwidths!(pks; kwargs...) +end + From bd468ccbf76066a6a47b246457b9e5a3d46e6faf Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 27 Feb 2024 16:24:43 -0800 Subject: [PATCH 47/63] Don't need to depwarn old kwargs for new functions --- src/peakheight.jl | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/peakheight.jl b/src/peakheight.jl index 78ce176..df83418 100644 --- a/src/peakheight.jl +++ b/src/peakheight.jl @@ -116,13 +116,7 @@ function peakheights!( return peaks, heights end -function peakheights!(pks::NamedTuple; minheight=nothing, maxheight=nothing, min=minheight, max=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 +function peakheights!(pks::NamedTuple; min=nothing, max=nothing) filterpeaks!(pks, :heights; min, max) return pks end From 40ea9a3e15557bf3862fe5b480bc1fc4e9267987 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 27 Feb 2024 16:26:29 -0800 Subject: [PATCH 48/63] Rename `up` => `hi` --- src/peakheight.jl | 4 ++-- src/peakprom.jl | 4 ++-- src/peakwidth.jl | 16 ++++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/peakheight.jl b/src/peakheight.jl index df83418..c3b54ab 100644 --- a/src/peakheight.jl +++ b/src/peakheight.jl @@ -107,8 +107,8 @@ function peakheights!( length(peaks) == length(heights) || throw(DimensionMismatch("length of `peaks`, $(length(peaks)), does not match the length of `heights`, $(length(heights))")) if !isnothing(min) || !isnothing(max) lo = something(min, typemin(Base.nonmissingtype(T))) - up = something(max, typemax(Base.nonmissingtype(T))) - matched = findall(x -> !(lo ≤ x ≤ up), heights) + hi = something(max, typemax(Base.nonmissingtype(T))) + matched = findall(x -> !(lo ≤ x ≤ hi), heights) deleteat!(peaks, matched) deleteat!(heights, matched) end diff --git a/src/peakprom.jl b/src/peakprom.jl index a352b5e..58981e9 100644 --- a/src/peakprom.jl +++ b/src/peakprom.jl @@ -208,8 +208,8 @@ function peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; if !isnothing(min) || !isnothing(max) lo = something(min, zero(eltype(x))) - up = something(max, typemax(Base.nonmissingtype(eltype(x)))) - matched = findall(x -> !ismissing(x) && !(lo ≤ x ≤ up), proms) + hi = something(max, typemax(Base.nonmissingtype(eltype(x)))) + matched = findall(x -> !ismissing(x) && !(lo ≤ x ≤ hi), proms) deleteat!(peaks, matched) deleteat!(proms, matched) end diff --git a/src/peakwidth.jl b/src/peakwidth.jl index fb1a47f..24d9172 100644 --- a/src/peakwidth.jl +++ b/src/peakwidth.jl @@ -156,22 +156,22 @@ function peakwidths!( 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]) + 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) 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(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 @@ -180,8 +180,8 @@ function peakwidths!( if !isnothing(min) || !isnothing(max) lo = something(min, zero(eltype(widths))) - up = something(max, typemax(Base.nonmissingtype(eltype(widths)))) - matched = findall(x -> !ismissing(x) && !(lo ≤ x ≤ up), 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) From 9b7da882127fd819a03f081914a9f7c5d0f418b8 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 27 Feb 2024 16:32:06 -0800 Subject: [PATCH 49/63] Update `peakproms!`/`peakwidths!` to use `ismaxima`/`isminima` for argument validation --- src/peakprom.jl | 17 +++++++++-------- src/peakwidth.jl | 13 +++++++------ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/peakprom.jl b/src/peakprom.jl index 58981e9..9e95512 100644 --- a/src/peakprom.jl +++ b/src/peakprom.jl @@ -114,15 +114,16 @@ function peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; 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 ? Base.max : Base.min + cmp = maxima ? (≥) : (≤) + exm = maxima ? minimum : maximum + exa = maxima ? Base.max : Base.min _ref = Missing <: T ? missing : Float64 <: T ? NaN : @@ -163,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 diff --git a/src/peakwidth.jl b/src/peakwidth.jl index 24d9172..bed45d5 100644 --- a/src/peakwidth.jl +++ b/src/peakwidth.jl @@ -125,14 +125,15 @@ function peakwidths!( 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) _bad = Missing <: V1 ? missing : float(Int)(NaN) From 5493b5206955e63530fd20ed7940a9365d464a83 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 27 Feb 2024 16:32:50 -0800 Subject: [PATCH 50/63] Rewrite error for bad min/max order --- src/peakprom.jl | 2 +- src/peakwidth.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/peakprom.jl b/src/peakprom.jl index 9e95512..ae3be5e 100644 --- a/src/peakprom.jl +++ b/src/peakprom.jl @@ -107,7 +107,7 @@ function peakproms!(peaks::AbstractVector{Int}, x::AbstractVector{T}; Base.depwarn("Keyword `maxprom` has been renamed to `max`", :peakproms!) end if !isnothing(min) && !isnothing(max) - min < max || throw(ArgumentError("minimal prominence must be less than maximal prominence")) + min < max || throw(ArgumentError("Keyword `min` must be less than `max`")) end all(∈(eachindex(x)), peaks) || throw(ArgumentError("peaks contains invalid indices to x")) diff --git a/src/peakwidth.jl b/src/peakwidth.jl index bed45d5..efd7f15 100644 --- a/src/peakwidth.jl +++ b/src/peakwidth.jl @@ -119,7 +119,7 @@ function peakwidths!( Base.depwarn("Keyword `maxwidth` has been renamed to `max`", :peakwidths!) end if !isnothing(min) && !isnothing(max) - min < max || throw(ArgumentError("max width must be greater than min width")) + min < max || throw(ArgumentError("Keyword `min` must be less than `max`")) end all(∈(eachindex(x)), peaks) || throw(ArgumentError("peaks contains invalid indices to x")) From 598477c1fef2074d389a80a04d58cc2411189569 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 27 Feb 2024 16:58:07 -0800 Subject: [PATCH 51/63] Fix unnecessary Missing union when input array doesn't have missings --- src/peakwidth.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/peakwidth.jl b/src/peakwidth.jl index efd7f15..0c121f2 100644 --- a/src/peakwidth.jl +++ b/src/peakwidth.jl @@ -138,7 +138,7 @@ function peakwidths!( 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) From d5b4281461aaa924b147f70d364d1690c2fd5d5c Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 27 Feb 2024 16:58:59 -0800 Subject: [PATCH 52/63] Update doctest outputs --- src/peakheight.jl | 8 ++------ src/peakwidth.jl | 15 ++++++++------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/peakheight.jl b/src/peakheight.jl index c3b54ab..0c4a192 100644 --- a/src/peakheight.jl +++ b/src/peakheight.jl @@ -24,9 +24,7 @@ julia> pks = findmaxima(x) julia> peakheights(pks; max=4) (indices = [4, 7], heights = [3, 4], data = [0, 5, 2, 3, 3, 1, 4, 0]) -julia> inds, heights = pks; - -julia> inds, heights = peakheights(indices, heights; max=4) +julia> inds, heights = peakheights(pks.indices, pks.heights; max=4) ([4, 7], [3, 4]) ``` """ @@ -87,9 +85,7 @@ julia> pks = findmaxima(x) julia> peakheights!(pks; max=4) (indices = [4, 7], heights = [3, 4], data = [0, 5, 2, 3, 3, 1, 4, 0]) -julia> inds, heights = pks; - -julia> inds, heights = peakheights!(indices, heights; min=3.5) +julia> inds, heights = peakheights!(pks.indices, pks.heights; min=3.5) ([7], [4]) ``` """ diff --git a/src/peakwidth.jl b/src/peakwidth.jl index 0c121f2..f24dddb 100644 --- a/src/peakwidth.jl +++ b/src/peakwidth.jl @@ -29,15 +29,15 @@ julia> x = Float64[0,5,2,2,3,3,1,4,0]; julia> pks = findmaxima(x) |> peakproms!(;max=2); julia> peakwidths(pks) -(indices = [5], heights = [3], data = [0, 5, 2, 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}}[(4.5, 6.25)]) +(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[4] = NaN; julia> peakwidths(pks.indices, x, pks.proms) -([5], Union{Missing, Float64}[NaN], Union{Missing, Float64}[NaN], Union{Missing, Float64}[6.25]) +([5], [NaN], [NaN], [6.25]) julia> peakwidths(pks.indices, x, pks.proms; strict=false) -([5], Union{Missing, Float64}[2.25], Union{Missing, Float64}[4.0], Union{Missing, Float64}[6.25]) +([5], [2.25], [4.0], [6.25]) ``` """ function peakwidths( @@ -71,8 +71,8 @@ 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=2) -(indices = [2, 7], heights = [5, 4], data = [0, 5, 2, 3, 3, 1, 4, 0], widths = Union{Missing, Int64}[5, 3]) +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) @@ -95,7 +95,7 @@ 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***" +```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!(); @@ -216,7 +216,8 @@ widths and other fields of its argument, `pks`, using any given keyword argument # Examples ```jldoctest -julia> findmaxima([0,5,2,3,3,1,4,0]) |> peakproms!() |> peakwidths!(; min=2) +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) From d805b046dbc2b800f3a2c831af0e405bbe44f580 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 27 Feb 2024 16:59:29 -0800 Subject: [PATCH 53/63] Update deprecated kwargs in tests --- test/peakheight.jl | 6 +++--- test/peakprom.jl | 6 +++--- test/peakwidth.jl | 6 +++--- test/plotting.jl | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/test/peakheight.jl b/test/peakheight.jl index 74a7579..5a4770d 100644 --- a/test/peakheight.jl +++ b/test/peakheight.jl @@ -8,12 +8,12 @@ @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..ac0129b 100644 --- a/test/peakprom.jl +++ b/test/peakprom.jl @@ -70,10 +70,10 @@ x1 = a*sin.(2*pi*f1*T*t)+b*sin.(2*pi*f2*T*t)+c*sin.(2*pi*f3*T*t); @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; maxprom=0.1, minprom=1) + @test_throws ArgumentError peakproms([1,2,3], sint; max=0.1, min=1) end # issue #4 diff --git a/test/peakwidth.jl b/test/peakwidth.jl index 3a2fa93..754e910 100644 --- a/test/peakwidth.jl +++ b/test/peakwidth.jl @@ -38,10 +38,10 @@ 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) end end diff --git a/test/plotting.jl b/test/plotting.jl index ae39585..fcf6993 100644 --- a/test/plotting.jl +++ b/test/plotting.jl @@ -6,14 +6,14 @@ 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 From 5c98dab76f888851f633ee8cea3c58ac14481f3d Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 27 Feb 2024 16:59:45 -0800 Subject: [PATCH 54/63] Test doctests when running tests --- Project.toml | 7 ------- test/Project.toml | 6 ++++++ test/runtests.jl | 8 +++++++- 3 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 test/Project.toml 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/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/runtests.jl b/test/runtests.jl index fe4a297..cfbea7b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,8 +1,14 @@ 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") + +@testset "Doctests" begin + doctest(Peaks; manual=false) +end From e7912928dc4421da84452ad3f73f90ad8cdb9e29 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 27 Feb 2024 17:20:18 -0800 Subject: [PATCH 55/63] Test depwarns --- test/peakheight.jl | 6 ++++++ test/peakprom.jl | 6 ++++++ test/peakwidth.jl | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/test/peakheight.jl b/test/peakheight.jl index 5a4770d..280e8ab 100644 --- a/test/peakheight.jl +++ b/test/peakheight.jl @@ -4,6 +4,12 @@ @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 diff --git a/test/peakprom.jl b/test/peakprom.jl index ac0129b..959c52d 100644 --- a/test/peakprom.jl +++ b/test/peakprom.jl @@ -74,6 +74,12 @@ x1 = a*sin.(2*pi*f1*T*t)+b*sin.(2*pi*f2*T*t)+c*sin.(2*pi*f3*T*t); @test length(first(peakproms(maxs, sint; max=1.5))) == 1 @test_throws ArgumentError peakproms([1,2,3], sint; max=0.1, min=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 # issue #4 diff --git a/test/peakwidth.jl b/test/peakwidth.jl index 754e910..b34108b 100644 --- a/test/peakwidth.jl +++ b/test/peakwidth.jl @@ -42,6 +42,12 @@ @test length(first(peakwidths(sinpks, sint, proms; max=pi*75))) == 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 end From 1c7180d8efd4a2c151b822c46b51166a8d40287f Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 27 Feb 2024 17:24:27 -0800 Subject: [PATCH 56/63] Test error for when first peak is not an extrema --- test/peakprom.jl | 6 +++--- test/peakwidth.jl | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/test/peakprom.jl b/test/peakprom.jl index 959c52d..7ae56ad 100644 --- a/test/peakprom.jl +++ b/test/peakprom.jl @@ -63,8 +63,6 @@ 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 @@ -82,6 +80,8 @@ x1 = a*sin.(2*pi*f1*T*t)+b*sin.(2*pi*f2*T*t)+c*sin.(2*pi*f3*T*t); @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)) @@ -94,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 b34108b..f235891 100644 --- a/test/peakwidth.jl +++ b/test/peakwidth.jl @@ -50,4 +50,5 @@ @test_logs (:warn, r"renamed") peakwidths!(copy(sinpks), copy(sint), copy(proms); minwidth=1) end + @test_throws ArgumentError peakwidths([2], 1:10, [1]) end From 66df7ad6c48a74574fc39d70d70999a91062afa5 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 27 Feb 2024 17:26:53 -0800 Subject: [PATCH 57/63] Confirm error when namedtuple has widths OR edges --- test/peakwidth.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/peakwidth.jl b/test/peakwidth.jl index f235891..229ebae 100644 --- a/test/peakwidth.jl +++ b/test/peakwidth.jl @@ -51,4 +51,6 @@ 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 From 4939c3d2db975d0339ff6456c434de5c5529e744 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 27 Feb 2024 17:32:12 -0800 Subject: [PATCH 58/63] Confirm error in plotpeaks when first peak isnt an extrema --- src/plot.jl | 2 +- test/plotting.jl | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) 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/test/plotting.jl b/test/plotting.jl index fcf6993..a0b7c06 100644 --- a/test/plotting.jl +++ b/test/plotting.jl @@ -17,6 +17,9 @@ let 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) From 862ad61c56eef5c52059ef8626c4177895cc83b0 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 27 Feb 2024 18:21:46 -0800 Subject: [PATCH 59/63] Update docstrings/doctests for utils functions --- src/utils.jl | 151 ++++++++++++++++++----------------------------- test/runtests.jl | 1 + test/utils.jl | 7 +++ 3 files changed, 65 insertions(+), 94 deletions(-) create mode 100644 test/utils.jl diff --git a/src/utils.jl b/src/utils.jl index 22f4651..7254029 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -3,77 +3,64 @@ 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)] + 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:\n$length_pairs - This should not happen, and indicates that the argument `pks` been created or modified by something outside Peaks.jl")) + 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_known_field(pks::NamedTuple) - if !any(hasproperty(pks, prop) for prop in known_fields) - throw(ArgumentError( - "Attempting to filter a named tuple `pks` that contains none of the known fields $known_fields. Because - this is thought to be an error, this error is thrown to help catch the original error." - )) - end +function check_has_required_fields(pks::NamedTuple) + !haskey(pks, :indices) && throw(ArgumentError( + "`pks` is missing required field `:indices`")) return nothing end """ - filterpeaks!(pks, mask) -> pks - -Filter the known fields of `pks` using `mask`, by removing elements of vectors -in `pks` fields for which `mask[i]` is `false`. Known fields -of `pks` are `:indices`, `:proms`, `:heights`, `:widths`, `:edges`. + filterpeaks!(pks::NT, feature; [min, max]) where {NT<:NamedTuple} -> pks::NT + filterpeaks!(pks::NT, mask) -> pks::NT -The functions `peakheights`, `peakproms` and `peakwidths` already -allow filtering by maximal and minimal values for different peak features. -This function can be used to perform more complicated filtering, such -as keeping a peak if it has a certain height _or_ a certain width. +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`. -If you find it inconvenient to define the the mask, see also the -version of `filterpeaks!` that takes a function as its first argument. +Standard Peaks.jl fields of `pks` are `:indices`, `:proms`, `:heights`, `:widths`, `:edges`. # Examples -julia> data = [1, 2, 3, 2, 3, 4, 0]; +```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> pks = findmaxima(data) -(indices = [3, 6], heights = [3, 4], data = [1, 2, 3, 2, 3, 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 = peakwidths!(pks) -(indices = [3, 6], heights = [3, 4], data = [1, 2, 3, 2, 3, 4, 0], proms = Union{Missing, Int64}[1, 3], widths = [1.0, 1.875], edges = [(2.5, 3.5), (4.5, 6.375)]) +julia> pks = findmaxima([0,5,2,3,3,1,4,0]) |> peakproms!(); -julia> # We can demand that the peak height is greater than 3.5 AND that the width is smaller than 1.5 -julia> # with the following mask. Note that with this data, that leaves no peaks. - -julia> my_mask = [pks.heights[i] > 3.5 && pks.widths[i] < 1.5 for i in eachindex(pks.indices)] -2-element Vector{Bool}: +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, my_mask) -(indices = Int64[], heights = Int64[], data = [1, 2, 3, 2, 3, 4, 0], proms = Union{Missing, Int64}[], widths = Float64[], edges = Tuple{Float64, Float64}[]) - +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 + # Check lengths first to avoid a dimension mismatch # after having filtered some features. # feature_mask = hasproperty.(pks, features_to_filter) - check_has_known_field(pks) + check_has_required_fields(pks) check_known_fields_equal_length(pks) - # At this point we know that all feature_length are equal, and do not need to check it again - # pks[1] returns the indices. - if length(pks[1]) != length(mask) + 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])). + "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 @@ -85,95 +72,71 @@ function filterpeaks!(pks::NamedTuple, mask::Union{BitVector, Vector{Bool}}) return pks end -# This method gets no docstring, as it is intended for internal use. -""" - filterpeaks!(pks, feature; min=nothing, max=nothing) - -Calculate `mask` as `[min < x < max for x in pks.feature]`, -and call `filterpeaks!(pks, mask)`. - -This method is intended for internal use. If you want to filter -by a minimal or maximal value of some peak feature, concider using -the keyword arguments `min` and `max` in the function that calculated -that feature, such as `peakproms!(pks, min=2)`. -""" function filterpeaks!(pks::NamedTuple, feature::Symbol; min=nothing, max=nothing) if !isnothing(min) || !isnothing(max) lo = something(min, zero(eltype(pks.data))) - up = something(max, typemax(Base.nonmissingtype(eltype(pks.data)))) - mask = map(x -> !ismissing(x) && (lo ≤ x ≤ up), pks[feature]) + hi = something(max, typemax(Base.nonmissingtype(eltype(pks.data)))) + mask = map(x -> !ismissing(x) && (lo ≤ x ≤ hi), pks[feature]) filterpeaks!(pks, mask) end - return nothing + return pks end """ filterpeaks!(pred, pks) -> NamedTuple -Apply a predicate function `pred` to named tuple slices to get a filter-mask. -An example of a "named tuple slice" is `(indices=5, heights=3, proms=2)`. -If `pred` returns `false` for peak number `i`, element `i` is removed from -the known fields of `pks`. +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 -julia> data = [1, 2, 3, 2, 3, 4, 0]; - -julia> pks = findmaxima(data) -(indices = [3, 6], heights = [3, 4], data = [1, 2, 3, 2, 3, 4, 0]) - -julia> pks = peakwidths!(pks); +```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> data = [1, 2, 3, 2, 3, 4, 0]; - -julia> pks = findmaxima(data) -(indices = [3, 6], heights = [3, 4], data = [1, 2, 3, 2, 3, 4, 0]) - -julia> pks = peakwidths!(pks) -(indices = [3, 6], heights = [3, 4], data = [1, 2, 3, 2, 3, 4, 0], proms = Union{Missing, Int64}[1, 3], widths = [1.0, 1.875], edges = [(2.5, 3.5), (4.5, 6.375)]) - -julia> filterpeaks!(pks) do nt_slice - nt_slice.heights > 3.5 && nt_slice.widths > 1.8 +julia> filterpeaks!(pks) do nt + return nt.heights > 4.5 || nt.heights < 3.5 end -(indices = [6], heights = [4], data = [1, 2, 3, 2, 3, 4, 0], proms = Union{Missing, Int64}[3], widths = [1.875], edges = [(4.5, 6.375)]) - +(indices = [2, 4], heights = [5, 3], data = [0, 5, 2, 3, 3, 1, 4, 0]) +``` """ function filterpeaks!(pred::Function, pks::NamedTuple) - check_has_known_field(pks) + check_has_required_fields(pks) check_known_fields_equal_length(pks) - mask = map(eachindex(pks[1])) do i - # :data is included in the nt_slice, but that should not be a problem - itr = zip(keys(pks), getindex.(values(pks), i)) - nt_slice = NamedTuple(itr) - return pred(nt_slice) + # `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) + deleteat!(pks[field], mask) end return pks end #==================================================================== -We store a version of findpeak here, as it might be implemented soon, +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 +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 +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 +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 -=# \ No newline at end of file +=# diff --git a/test/runtests.jl b/test/runtests.jl index cfbea7b..08dbb0e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -8,6 +8,7 @@ include("peakprom.jl") include("peakwidth.jl") include("peakheight.jl") include("plotting.jl") +include("utils.jl") @testset "Doctests" begin doctest(Peaks; manual=false) 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 From adc14397bda87b7e7431872287ba8d4b5583f21b Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 27 Feb 2024 18:22:38 -0800 Subject: [PATCH 60/63] Add `filterpeaks!` to the docs --- docs/src/index.md | 1 + 1 file changed, 1 insertion(+) 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 From 925c9af6e5e98108da941376c744c2da1fbe5787 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 27 Feb 2024 18:23:02 -0800 Subject: [PATCH 61/63] Add julia cache to CI docs build --- .github/workflows/CI.yml | 1 + 1 file changed, 1 insertion(+) 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 From b2ec0a6b3f23755968e638322c9b915f753e80f8 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 27 Feb 2024 18:28:46 -0800 Subject: [PATCH 62/63] Dont try linking to Base docs (for the moment) --- src/minmax.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/minmax.jl b/src/minmax.jl index aba2378..5b16232 100644 --- a/src/minmax.jl +++ b/src/minmax.jl @@ -191,7 +191,7 @@ end 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`](@ref) contains the fields `indices`, `heights`, `data`, which are +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`. @@ -329,7 +329,7 @@ end 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`](@ref) contains the fields `indices`, `heights`, `data`, which are +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`. From f8b6f0ee95af316752707d64419a3172978c3012 Mon Sep 17 00:00:00 2001 From: Allen Hill Date: Tue, 27 Feb 2024 18:32:18 -0800 Subject: [PATCH 63/63] fix errant ref --- src/peakheight.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/peakheight.jl b/src/peakheight.jl index 0c4a192..1b11a99 100644 --- a/src/peakheight.jl +++ b/src/peakheight.jl @@ -12,7 +12,6 @@ from `pks`. `pks` must have `:indices` and `:heights` fields. The fields `:proms copied unmodified. See also: [`peakproms`](@ref), [`peakwidths`](@ref), [`findmaxima`](@ref) -[`filterpeaks`](@ref) # Examples ```jldoctest