From 52824d8c8f34c0700ad8a4d023bd9dc4bb1fab68 Mon Sep 17 00:00:00 2001 From: Datseris Date: Sat, 26 Apr 2025 17:42:25 +0100 Subject: [PATCH 01/22] update CI --- .github/workflows/CompatHelper.yml | 51 +++++++++-------------------- .github/workflows/TagBot.yml | 2 +- .github/workflows/ci.yml | 39 +++++++++------------- .github/workflows/doccleanup.yml | 26 +++++++++++++++ .github/workflows/documentation.yml | 26 +++++++++++++++ 5 files changed, 84 insertions(+), 60 deletions(-) create mode 100644 .github/workflows/doccleanup.yml create mode 100644 .github/workflows/documentation.yml diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml index 3dfba52..6c2390a 100644 --- a/.github/workflows/CompatHelper.yml +++ b/.github/workflows/CompatHelper.yml @@ -1,45 +1,24 @@ name: CompatHelper + on: schedule: - - cron: 0 0 * * * - workflow_dispatch: -permissions: - contents: write - pull-requests: write + - cron: '00 * * * *' + jobs: CompatHelper: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + matrix: + julia-version: [1] + julia-arch: [x86] + os: [ubuntu-latest] steps: - - name: Check if Julia is already available in the PATH - id: julia_in_path - run: which julia - continue-on-error: true - - name: Install Julia, but only if it is not already available in the PATH - uses: julia-actions/setup-julia@v1 + - uses: julia-actions/setup-julia@latest with: - version: '1' - arch: ${{ runner.arch }} - if: steps.julia_in_path.outcome != 'success' - - name: "Add the General registry via Git" - run: | - import Pkg - ENV["JULIA_PKG_SERVER"] = "" - Pkg.Registry.add("General") - shell: julia --color=yes {0} - - name: "Install CompatHelper" - run: | - import Pkg - name = "CompatHelper" - uuid = "aa819f21-2bde-4658-8897-bab36330d9b7" - version = "3" - Pkg.add(; name, uuid, version) - shell: julia --color=yes {0} - - name: "Run CompatHelper" - run: | - import CompatHelper - CompatHelper.main() - shell: julia --color=yes {0} + version: ${{ matrix.julia-version }} + - name: Pkg.add("CompatHelper") + run: julia -e 'using Pkg; Pkg.add("CompatHelper")' + - name: CompatHelper.main() env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} - # COMPATHELPER_PRIV: ${{ secrets.COMPATHELPER_PRIV }} \ No newline at end of file + run: julia -e 'using CompatHelper; CompatHelper.main()' diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml index f49313b..623860f 100644 --- a/.github/workflows/TagBot.yml +++ b/.github/workflows/TagBot.yml @@ -12,4 +12,4 @@ jobs: - uses: JuliaRegistries/TagBot@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - ssh: ${{ secrets.DOCUMENTER_KEY }} + ssh: ${{ secrets.DOCUMENTER_KEY }} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fda332b..56dd6cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,10 +2,11 @@ name: CI on: pull_request: branches: - - master + - main + - '**' # matches every branch push: branches: - - master + - main tags: '*' jobs: test: @@ -15,11 +16,19 @@ jobs: fail-fast: false matrix: version: - - '1' # Replace this with the minimum Julia version that your package supports. E.g. if your package requires Julia 1.5 or higher, change this to '1.5'. - os: [ubuntu-latest] + - '1' + os: [ubuntu-latest] # adjust according to need, e.g. os: [ubuntu-latest] if testing only on linux arch: - x64 steps: + # Cancel ongoing CI test runs if pushing to branch again before the previous tests + # have finished + - name: Cancel ongoing test runs for previous commits + uses: styfle/cancel-workflow-action@0.6.0 + with: + access_token: ${{ github.token }} + + # Do tests - uses: actions/checkout@v2 - uses: julia-actions/setup-julia@v1 with: @@ -37,23 +46,7 @@ jobs: ${{ runner.os }}- - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - docs: - name: Documentation - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: julia-actions/setup-julia@v1 + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v1 with: - version: '1' - - name: Instantiate and install dependencies - run: | - julia --project=docs -e ' - using Pkg - Pkg.develop(PackageSpec(path=pwd())) - Pkg.instantiate()' - - name: Generate documentation and deploy - env: # needed for pushing to gh-pages branch - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} - run: - julia --project=docs docs/make.jl + file: lcov.info diff --git a/.github/workflows/doccleanup.yml b/.github/workflows/doccleanup.yml new file mode 100644 index 0000000..22d9043 --- /dev/null +++ b/.github/workflows/doccleanup.yml @@ -0,0 +1,26 @@ +name: Doc Preview Cleanup + +on: + pull_request: + types: [closed] + +jobs: + doc-preview-cleanup: + runs-on: ubuntu-latest + steps: + - name: Checkout gh-pages branch + uses: actions/checkout@v2 + with: + ref: gh-pages + - name: Delete preview and history + push changes + run: | + if [ -d "previews/PR$PRNUM" ]; then + git config user.name "Documenter.jl" + git config user.email "documenter@juliadocs.github.io" + git rm -rf "previews/PR$PRNUM" + git commit -m "delete preview" + git branch gh-pages-new $(echo "delete history" | git commit-tree HEAD^{tree}) + git push --force origin gh-pages-new:gh-pages + fi + env: + PRNUM: ${{ github.event.number }} \ No newline at end of file diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 0000000..33e080c --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,26 @@ +name: Documentation + +on: + push: + branches: + - main + tags: '*' + pull_request: + +jobs: + build: + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: '1' + - name: Install dependencies + run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' + - name: Build and deploy + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # If authenticating with GitHub Actions token + DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # If authenticating with SSH deploy key + run: julia --project=docs/ docs/make.jl \ No newline at end of file From 1ae7c15a9310c2ad8b8eca9db4d4bd12b79ab3aa Mon Sep 17 00:00:00 2001 From: Datseris Date: Sat, 26 Apr 2025 17:42:50 +0100 Subject: [PATCH 02/22] WIP Porting Aryan's code --- CHANGELOG.md | 5 ++ README.md | 7 +- docs/src/index.md | 17 +++- src/SignalDecomposition.jl | 1 + src/detrending/aryan_original_cde.jl | 113 +++++++++++++++++++++++++++ src/detrending/simple.jl | 28 +++++++ src/detrending/smoothing.jl | 0 src/misc/anomaly.jl | 2 +- 8 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 src/detrending/aryan_original_cde.jl create mode 100644 src/detrending/simple.jl create mode 100644 src/detrending/smoothing.jl diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3f84367 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +*changelog kept with respect to version 1.1* + +# v1.2 + +Detrending! \ No newline at end of file diff --git a/README.md b/README.md index c1e4a44..eb993f8 100644 --- a/README.md +++ b/README.md @@ -5,5 +5,10 @@ |[![](https://img.shields.io/badge/docs-online-blue.svg)](https://JuliaDynamics.github.io/SignalDecomposition.jl/dev)| [![CI](https://github.com/juliadynamics/SignalDecomposition.jl/workflows/CI/badge.svg)](https://github.com/JuliaDynamics/SignalDecomposition.jl/actions) | [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/JuliaDynamics/Lobby) -Decompose a signal/timeseries into structure and noise or seasonal and residual components. +Decompose a signal/timeseries into one of: + +- structure and noise (de-noising or smoothing) +- seasonal and residual (anomalies) +- trend and residual (de-trending) + Further info in the docs! diff --git a/docs/src/index.md b/docs/src/index.md index dae21bd..7049e83 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -3,6 +3,7 @@ --- ## API + `SignalDecomposition` is a simple Julia package that offers a single function: ```@docs decompose @@ -12,7 +13,8 @@ Feel free to add more methods to `decompose` by opening a PR, SignalDecomposition.jl is very open in the methods it offers. ## Linear methods -Here "linear" means linear in frequency space. For some methods you could of course get more control of the process by directly using [DSP.jl](https://github.com/JuliaDSP/DSP.jl/). + +Here "linear" means linear in frequency space. For some methods you could of course get more control of the process by directly using [DSP.jl](https://github.com/JuliaDSP/DSP.jl/). ```@docs Fourier FrequencySplit @@ -25,6 +27,19 @@ ExtremelySimpleNL ManifoldProjection ``` +## Detrending methods + +```@docs +``` + +## Smoothing (or detrending) methods + +These methods can be either smoothing or detrending depending on how large +the smoothing window is taken! + +```@docs +``` + ## Product methods ```@docs ProductInversion diff --git a/src/SignalDecomposition.jl b/src/SignalDecomposition.jl index f40bb9b..6a4a791 100644 --- a/src/SignalDecomposition.jl +++ b/src/SignalDecomposition.jl @@ -7,6 +7,7 @@ abstract type Decomposition end """ decompose([t, ] s, method::Decomposition) → x, r + Decompose an 1D input signal or timeseries `s(t)` into components, `x, r`, using the given `method`. `t` defaults to `1:length(s)`. diff --git a/src/detrending/aryan_original_cde.jl b/src/detrending/aryan_original_cde.jl new file mode 100644 index 0000000..0d9eeb5 --- /dev/null +++ b/src/detrending/aryan_original_cde.jl @@ -0,0 +1,113 @@ +using Polynomials +using Statistics +using LinearAlgebra +using EmpiricalModeDecomposition +using HPFilter +using Loess +using MarketData +using ComplexityMeasures + +function closing_stock_timeseries(name::String, start::DateTime, finish::DateTime, freq::String) + opt = YahooOpt(period1 = start, period2 = finish, interval = freq) + stockdata = yahoo(name, opt) + stock_close = stockdata["Close"] + actual_numbers = values(stock_close) + return float.(actual_numbers) +end + +function Complexity_Measures(y; r = std(y)*0.2, a=6) + permutation = entropy_normalized(OrdinalPatterns{4}(), y) + spectral = entropy_normalized(PowerSpectrum(), y) + dispersion = entropy_normalized(Dispersion(c = a), y) + approximate = complexity(ApproximateEntropy(; r), y) + sample = complexity(SampleEntropy(; r), y) + return permutation, spectral, dispersion, approximate, sample +end + +function detrend_ols(y) + n = length(y) + X = hcat(ones(n), collect(1:n)) + β = X \ y # OLS estimation + trend = X * β # Fitted trend + y_detrended = y - trend # de-trend data + return y_detrended +end + +function polynomial_detrend(y, deg=2) + x = 1:length(y) + p = fit(x, y, deg) + trend = p.(x) + mse = sum((y - trend).^2) / length(y) + y_detrended = y .-trend + return y_detrended +end + +function moving_average(y, window = 30) + n = length(y) + trend = zeros(n) + + for i in 1:n + half = (window - 1) ÷ 2 + start_idx = max(1, i - half) + end_idx = min(n, i + half) + trend[i] = mean(y[start_idx:end_idx]) + end + mse = sum((y - trend).^2) / n + detrend = y .- trend + return detrend +end + +function detrend_emd(y) + n = length(y) + t = 1:n + imf = emd(y, t) + trend = imf[7] + imf[6] + residual = sum(imf[i] for i in 1:5) + detrend = y .- trend + return detrend +end + +function detrend_hp(y, lambda = 1600) + n = length(y) + trend = HP(y, lambda) + detrend = y .- trend + mse = sum((y .- trend).^2) / n + return detrend +end + +function detrend_loess(y, s = 0.2) + n = length(y) + x = 1:n + model = loess(x, y, span = s) + trend = predict(model, x) + detrend = y .- trend + mse = sum((y .- trend).^2) / n + return detrend +end + +function mutual_information(x, y, n=1000) + x = copy(x) + y = copy(y) + Hx = entropy(ValueHistogram(10), Dataset(x)) + Hy = entropy(ValueHistogram(10), Dataset(y)) + Hxy = entropy(ValueHistogram(10), Dataset(x, y)) + mi = Hx + Hy - Hxy + + null = zeros(n) + for i in 1:n + shuffle!(x), shuffle!(y) + Hxy = entropy(ValueHistogram(10), Dataset(x, y)) + null[i] = Hx + Hy - Hxy + end + + mu = mean(null) + sd = std(null) + return mi, mu, sd +end + + +function main_analysis(timeseries, detrending, complexity_measure) + detrended = detrending(timeseries)[1] + measure = complexity_measure(detrended) + return measure +end \ No newline at end of file diff --git a/src/detrending/simple.jl b/src/detrending/simple.jl new file mode 100644 index 0000000..af3e507 --- /dev/null +++ b/src/detrending/simple.jl @@ -0,0 +1,28 @@ +import Polynomials + +""" + PolyNomialDetrending(degree::Int = 2) + +Decompose timeseries `s` into a **sum** `x + r` where `x` is the trend +and `r` the residual, utilizing a polynomial guaranteed to have given `degree`. +For `degree = 1` this is linear detrending (ordinary least squares). +""" +@kwdef struct PonynomialDetrending <: Decomposition + degree::Int = 1 +end + +function decompose(t, s, method::PonynomialDetrending) + p = Polynomials.fit(t, s, method.degree) + trend = p.(t) + return trend, trend .- s +end + +@kwdef struct MovingAverage + window +end + +struct NoDecomposition <: Decomposition end + +function decompose(t, s, ::NoDecomposition) + return s, similar(s) +end diff --git a/src/detrending/smoothing.jl b/src/detrending/smoothing.jl new file mode 100644 index 0000000..e69de29 diff --git a/src/misc/anomaly.jl b/src/misc/anomaly.jl index 2735b07..b3ddb49 100644 --- a/src/misc/anomaly.jl +++ b/src/misc/anomaly.jl @@ -5,7 +5,7 @@ daymonth(t) = day(t), month(t) """ TimeAnomaly() -Decompose timeseries `s` into a **sum** `x + r` wher `x` is the same-time average and +Decompose timeseries `s` into a **sum** `x + r` where `x` is the same-time average and `r` is the anomalies. Each unique day+month combination in `t` is identified, and the values of `s` for each year that has this day+month combination are averaged. As a result, the time vector `t` must be `<:AbstractVector{<:TimeType}`. From 00004b5b02ea5d4741435586d7e9477c35e01b5d Mon Sep 17 00:00:00 2001 From: Datseris Date: Thu, 1 May 2025 22:27:08 +0100 Subject: [PATCH 03/22] add basic detranding and window smoothing --- CHANGELOG.md | 4 +++- Project.toml | 4 +++- src/SignalDecomposition.jl | 3 +++ src/detrending/simple.jl | 18 ++++++++++-------- src/detrending/smoothing.jl | 23 +++++++++++++++++++++++ src/linear/fourier.jl | 1 + 6 files changed, 43 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f84367..2f4bdd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,4 +2,6 @@ # v1.2 -Detrending! \ No newline at end of file +- Add new functionality for the package: detrending +- Add new functionality for the package: smoothing +- Add new `Decompositions`: `PolynomialDetrending, NoDecomposition, MovingAverageSmoothing`. \ No newline at end of file diff --git a/Project.toml b/Project.toml index 5ab7dec..47aef85 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "SignalDecomposition" uuid = "11a47235-7b84-4c7c-b885-fc3e2a9cf955" authors = ["Datseris "] -version = "1.1.0" +version = "1.2.0" [deps] BandedMatrices = "aae01518-5342-5314-be14-df237901396f" @@ -11,6 +11,7 @@ FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" LPVSpectral = "26dcc766-85df-5edc-b560-6076d5dbac63" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Neighborhood = "645ca80c-8b79-4109-87ea-e1f58159d116" +Polynomials = "f27b6e38-b328-58d1-80ce-0feddd5e7a45" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" @@ -21,6 +22,7 @@ DelayEmbeddings = "2" FFTW = "1.2" LPVSpectral = "0.3" Neighborhood = "0.2" +Polynomials = "4" StaticArrays = "1" Statistics = "1" julia = "1.9" diff --git a/src/SignalDecomposition.jl b/src/SignalDecomposition.jl index 6a4a791..c86744d 100644 --- a/src/SignalDecomposition.jl +++ b/src/SignalDecomposition.jl @@ -27,6 +27,9 @@ include("product/matrixinversion.jl") include("nonlinear/extremelysimple.jl") include("nonlinear/projection.jl") include("misc/anomaly.jl") +include("detrending/simple.jl") +include("detrending/smoothing.jl") + end # module diff --git a/src/detrending/simple.jl b/src/detrending/simple.jl index af3e507..b411c14 100644 --- a/src/detrending/simple.jl +++ b/src/detrending/simple.jl @@ -1,28 +1,30 @@ +export PolynomialDetrending, LinearDetrending import Polynomials """ - PolyNomialDetrending(degree::Int = 2) + PolyNnmialDetrending(degree::Int = 1) <: Decomposition Decompose timeseries `s` into a **sum** `x + r` where `x` is the trend -and `r` the residual, utilizing a polynomial guaranteed to have given `degree`. +and `r` the residual. The trend is a fitted polynomial guaranteed to have given `degree`. For `degree = 1` this is linear detrending (ordinary least squares). """ -@kwdef struct PonynomialDetrending <: Decomposition +@kwdef struct PolynomialDetrending <: Decomposition degree::Int = 1 end -function decompose(t, s, method::PonynomialDetrending) +function decompose(t, s, method::PolynomialDetrending) p = Polynomials.fit(t, s, method.degree) trend = p.(t) return trend, trend .- s end -@kwdef struct MovingAverage - window -end +""" + NoDecomposition <: Decomposition +Decompose timeseries `s` into `s` and zeros. +""" struct NoDecomposition <: Decomposition end function decompose(t, s, ::NoDecomposition) - return s, similar(s) + return s, zeros(eltype(s), length(s)) end diff --git a/src/detrending/smoothing.jl b/src/detrending/smoothing.jl index e69de29..ac54027 100644 --- a/src/detrending/smoothing.jl +++ b/src/detrending/smoothing.jl @@ -0,0 +1,23 @@ +export MovingAverageSmoothing + +""" + MovingAverageSmoothing(window::Int = 10) <: Decomposition + +TODO. +""" +@kwdef struct MovingAverageSmoothing + window::Int = 10 +end + +function decompose(t, s, mas::MovingAverageSmoothing) + window = mas.window + n = length(s) + smooth = zeros(eltype(s), n) + for i in 1:n + half = (window - 1) ÷ 2 + start_idx = max(1, i - half) + end_idx = min(n, i + half) + smooth[i] = mean(@view s[start_idx:end_idx]) + end + return smooth, s .- smooth +end \ No newline at end of file diff --git a/src/linear/fourier.jl b/src/linear/fourier.jl index 10d894b..a65c262 100644 --- a/src/linear/fourier.jl +++ b/src/linear/fourier.jl @@ -8,6 +8,7 @@ export Fourier, FrequencySplit """ Fourier([s, ] frequencies, x=true) <: Decomposition + Decompose a timeseries `s` into a **sum** `x + r`, by identifying specific `frequencies` at the Fourier space and removing them from the signal. `x` is the removed periodic component while `r` is the residual. From 3ab57a97dacea34dd8b8e8d7514485c3ff9d2c5b Mon Sep 17 00:00:00 2001 From: Datseris Date: Thu, 1 May 2025 22:28:04 +0100 Subject: [PATCH 04/22] correct changelog --- CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f4bdd4..1d77df2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,11 @@ -*changelog kept with respect to version 1.1* +*changelog kept with respect to version 1.0* # v1.2 - Add new functionality for the package: detrending - Add new functionality for the package: smoothing -- Add new `Decompositions`: `PolynomialDetrending, NoDecomposition, MovingAverageSmoothing`. \ No newline at end of file +- Add new `Decompositions`: `PolynomialDetrending, NoDecomposition, MovingAverageSmoothing`. + +# v1.1 + +Update to Julia 1.9, and bump dependency compatibility versions to those of Julia 1.9+. \ No newline at end of file From b987d968ee2423289f91cf0e6a4dceaff1b00b07 Mon Sep 17 00:00:00 2001 From: Datseris Date: Wed, 21 May 2025 10:55:49 +0100 Subject: [PATCH 05/22] update docs to reflect dynamical systems style --- CHANGELOG.md | 1 + README.md | 23 ++++++++++++++--------- docs/src/index.md | 4 ++-- src/SignalDecomposition.jl | 19 ++++++++++++------- src/linear/lpv.jl | 1 + 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d77df2..6ee11fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Add new functionality for the package: detrending - Add new functionality for the package: smoothing - Add new `Decompositions`: `PolynomialDetrending, NoDecomposition, MovingAverageSmoothing`. +- Package is now part of DynamicalSystems.jl as well. # v1.1 diff --git a/README.md b/README.md index eb993f8..50e8eb0 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,19 @@ -![SignalDecomposition.jl](https://github.com/JuliaDynamics/JuliaDynamics/blob/master/videos/other/signaldecomposition.gif?raw=true) +[![docsdev](https://img.shields.io/badge/docs-dev-lightblue.svg)](https://juliadynamics.github.io/DynamicalSystemsDocs.jl/signaldecomposition/dev/) +[![docsstable](https://img.shields.io/badge/docs-stable-blue.svg)](https://juliadynamics.github.io/DynamicalSystemsDocs.jl/signaldecomposition/stable/) +[![CI](https://github.com/JuliaDynamics/SignalDecomposition.jl/workflows/CI/badge.svg)](https://github.com/JuliaDynamics/SignalDecomposition.jl/actions?query=workflow%3ACI) +[![codecov](https://codecov.io/gh/JuliaDynamics/SignalDecomposition.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/JuliaDynamics/SignalDecomposition.jl) -| **Documentation** | **Tests** | **Chat** | -|:--------:|:---------------:|:-----:| -|[![](https://img.shields.io/badge/docs-online-blue.svg)](https://JuliaDynamics.github.io/SignalDecomposition.jl/dev)| [![CI](https://github.com/juliadynamics/SignalDecomposition.jl/workflows/CI/badge.svg)](https://github.com/JuliaDynamics/SignalDecomposition.jl/actions) | [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/JuliaDynamics/Lobby) - - -Decompose a signal/timeseries into one of: +SignalDecomposition.jl is a Julia package providing an interface and dozens of algorithm implementations for signal decomposition. +Given a signal (or timeseries), the function `decompose` splits it into two components. +These may be: - structure and noise (de-noising or smoothing) -- seasonal and residual (anomalies) +- seasonal and residual (climatologies or anomalies) - trend and residual (de-trending) -Further info in the docs! +It can be used as a standalone package, or as part of +[DynamicalSystems.jl](https://juliadynamics.github.io/DynamicalSystemsDocs.jl/dynamicalsystems/stable/). + +To install it, run `import Pkg; Pkg.add("SignalDecomposition")`. + +All further information is provided in the documentation, which you can either find [online](https://juliadynamics.github.io/DynamicalSystemsDocs.jl/signaldecomposition/stable/) or build locally by running the `docs/make.jl` file. diff --git a/docs/src/index.md b/docs/src/index.md index 7049e83..4d64136 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -4,13 +4,13 @@ ## API -`SignalDecomposition` is a simple Julia package that offers a single function: ```@docs decompose ``` + Subtypes of `Decomposition` are listed in the rest of this page. Feel free to add more methods to `decompose` by opening a PR, -SignalDecomposition.jl is very open in the methods it offers. +SignalDecomposition.jl is very open in the methods it offers! ## Linear methods diff --git a/src/SignalDecomposition.jl b/src/SignalDecomposition.jl index c86744d..0c57793 100644 --- a/src/SignalDecomposition.jl +++ b/src/SignalDecomposition.jl @@ -1,5 +1,12 @@ module SignalDecomposition +# Use the README as the module docs +@doc let + path = joinpath(dirname(@__DIR__), "README.md") + include_dependency(path) + read(path, String) +end SignalDecomposition + export decompose, Decomposition "Supertype of all decomposition methods." @@ -8,16 +15,14 @@ abstract type Decomposition end """ decompose([t, ] s, method::Decomposition) → x, r -Decompose an 1D input signal or timeseries `s(t)` into components, `x, r`, -using the given `method`. `t` defaults to `1:length(s)`. +Decompose an 1D input signal or timeseries `s(t)` into components `x, r` +using the given `method`. `t` defaults to `eachindex(s)`. -What are `x` and `r` really depends on your point of view and your application. -They can be structure `x` and noise `r` (i.e. noise reduction). They can be -seasonal/periodic `x` and residual component `r`. They can even be multiplier `x` and -input `r`. +What are `x` and `r` are, and how they combine to give `s`, depends on `method`. +See the online documentation for all subtypes of `Decomposition`. """ decompose(s::AbstractVector, method::Decomposition; kwargs...) = -decompose(1:length(s), s, method; kwargs...) +decompose(eachindex(s), s, method; kwargs...) using Statistics include("utils.jl") diff --git a/src/linear/lpv.jl b/src/linear/lpv.jl index 4bdd5f0..69bfeac 100644 --- a/src/linear/lpv.jl +++ b/src/linear/lpv.jl @@ -3,6 +3,7 @@ export Sinusoidal """ Sinusoidal(fs) + Decompose a timeseries `s` into a **sum** `x + r`, where `x` are sinusoidal components with the given frequencies `fs` that minimize coefficients ``A, \\phi`` of the expression From 0df119d04cfbd9b1c6c6431522a1696efc98eef4 Mon Sep 17 00:00:00 2001 From: Datseris Date: Wed, 21 May 2025 11:11:23 +0100 Subject: [PATCH 06/22] update doc generation to dynamical systems --- docs/Project.toml | 4 +-- docs/make.jl | 58 +++++++++----------------------------------- docs/src/examples.md | 16 +++--------- 3 files changed, 17 insertions(+), 61 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index 97c1fe9..b598671 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -2,7 +2,7 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DocumenterTools = "35a29f4d-8980-5a13-9543-d66fff28ecb8" -DynamicalSystems = "61744808-ddfa-5f27-97ff-6e42cc95d634" -Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +DynamicalSystemsBase = "6e36e845-645a-534a-86f2-f5d4aa5a06b4" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" SignalDecomposition = "11a47235-7b84-4c7c-b885-fc3e2a9cf955" +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" diff --git a/docs/make.jl b/docs/make.jl index 9f7713a..25df3a8 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,49 +1,15 @@ -using Pkg -Pkg.activate(@__DIR__) -CI = get(ENV, "CI", nothing) == "true" || get(ENV, "GITHUB_TOKEN", nothing) !== nothing -CI && Pkg.instantiate() -CI && (ENV["GKSwstype"] = "100") +cd(@__DIR__) +using SignalDecomposition +using DynamicalSystemsBase -using SignalDecomposition, Documenter, DocumenterTools -using DocumenterTools: Themes -using Plots - -# %% -# download the themes -for file in ("juliadynamics-lightdefs.scss", "juliadynamics-darkdefs.scss", "juliadynamics-style.scss") - download("https://raw.githubusercontent.com/JuliaDynamics/doctheme/master/$file", joinpath(@__DIR__, file)) -end -# create the themes -for w in ("light", "dark") - header = read(joinpath(@__DIR__, "juliadynamics-style.scss"), String) - theme = read(joinpath(@__DIR__, "juliadynamics-$(w)defs.scss"), String) - write(joinpath(@__DIR__, "juliadynamics-$(w).scss"), header*"\n"*theme) -end -# compile the themes -Themes.compile(joinpath(@__DIR__, "juliadynamics-light.scss"), joinpath(@__DIR__, "src/assets/themes/documenter-light.css")) -Themes.compile(joinpath(@__DIR__, "juliadynamics-dark.scss"), joinpath(@__DIR__, "src/assets/themes/documenter-dark.css")) - -makedocs(modules = [SignalDecomposition], -sitename= "SignalDecomposition.jl", -authors = "George Datseris and contributors.", -doctest = false, -format = Documenter.HTML( - prettyurls = CI, - assets = [ - "assets/logo.ico", - asset("https://fonts.googleapis.com/css?family=Quicksand|Montserrat|Source+Code+Pro|Lora&display=swap", class=:css), - ], - ), pages = [ - "Documentation" => "index.md", - "Examples" => "examples.md", - ], + "index.md", + "examples.md", +] + +import Downloads +Downloads.download( + "https://raw.githubusercontent.com/JuliaDynamics/doctheme/master/build_docs_with_style.jl", + joinpath(@__DIR__, "build_docs_with_style.jl") ) - -if CI - deploydocs( - repo = "github.com/JuliaDynamics/SignalDecomposition.jl.git", - target = "build", - push_preview = true - ) -end +include("build_docs_with_style.jl") diff --git a/docs/src/examples.md b/docs/src/examples.md index 3dc7ab1..77b8815 100644 --- a/docs/src/examples.md +++ b/docs/src/examples.md @@ -3,7 +3,7 @@ Only a few examples are shown here. Every method has an example (and plotting co ## Nonlinear ```@example docs -using SignalDecomposition, DynamicalSystems, Random, Plots, Statistics +using SignalDecomposition, DynamicalSystemsBase, Random, Statistics he = Systems.henon() tr = trajectory(he, 10000; Ttr = 100) @@ -11,26 +11,16 @@ Random.seed!(151521) z = tr[:, 1] s = z .+ randn(10001)*0.1*std(z) m = 5 -metric = Euclidean() k = 30 Q = [2, 2, 2, 3, 3, 3, 3] x, r = decompose(s, ManifoldProjection(m, Q, k)) summary(x) ``` -```@example docs -p1 = plot(s, label = "input") -plot!(p1, z, color = :black, ls = :dash, label = "real") -plot!(p1, x, alpha = 0.5, label = "output") -xlims!(p1, 0, 50) -p1 -``` - -Alright, this doesn't seem much of a difference to be honest. -One sees a big difference once going into the state space and looking at the attractor: +This method is nicely highlighted once going into the state space and looking at the attractor: ```@example docs -p2 = scatter(s[1:end-1], s[2:end], ms = 1, label = "input", msw = 0) +fig, ax = scatter(s[1:end-1], s[2:end]; markersize = 1, label = "input", msw = 0) scatter!(p2, z[1:end-1], z[2:end], ms = 1, label = "real", color = :black, msw = 0) scatter!(p2, x[1:end-1], x[2:end], ms = 1, label = "output", alpha = 0.5, msw = 0) savefig(p2, "henon.png") # hide From c023c63c6c30f558bdcc588ff18f765052f5bfbc Mon Sep 17 00:00:00 2001 From: Datseris Date: Wed, 21 May 2025 11:12:43 +0100 Subject: [PATCH 07/22] actually build docs --- docs/make.jl | 5 +++++ docs/src/index.md | 2 ++ 2 files changed, 7 insertions(+) diff --git a/docs/make.jl b/docs/make.jl index 25df3a8..b73e7f3 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -13,3 +13,8 @@ Downloads.download( joinpath(@__DIR__, "build_docs_with_style.jl") ) include("build_docs_with_style.jl") + +build_docs_with_style(pages, Attractors; + authors = "George Datseris ", + expandfirst = ["index.md"], # this is the first script that loads colorscheme +) diff --git a/docs/src/index.md b/docs/src/index.md index 4d64136..f7fb032 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,3 +1,5 @@ +# Documentation + ![SignalDecomposition.jl](https://github.com/JuliaDynamics/JuliaDynamics/blob/master/videos/other/signaldecomposition.gif?raw=true) --- From c25e1b4d492f8792202b31175083c407e7ed0b2a Mon Sep 17 00:00:00 2001 From: Datseris Date: Wed, 21 May 2025 12:32:31 +0100 Subject: [PATCH 08/22] add docstring to MAW --- src/detrending/simple.jl | 2 ++ src/detrending/smoothing.jl | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/detrending/simple.jl b/src/detrending/simple.jl index b411c14..8a85b5b 100644 --- a/src/detrending/simple.jl +++ b/src/detrending/simple.jl @@ -22,6 +22,8 @@ end NoDecomposition <: Decomposition Decompose timeseries `s` into `s` and zeros. +Dummy method provided simply so that one can loop over various decomposition +methods and no decomposition at all while still using the `decompose` function. """ struct NoDecomposition <: Decomposition end diff --git a/src/detrending/smoothing.jl b/src/detrending/smoothing.jl index ac54027..da64993 100644 --- a/src/detrending/smoothing.jl +++ b/src/detrending/smoothing.jl @@ -3,7 +3,10 @@ export MovingAverageSmoothing """ MovingAverageSmoothing(window::Int = 10) <: Decomposition -TODO. +Decompose timeseries `s` into a **sum** `x + r` where `x` is the smoothened singla +and `r` the residual. The smoothing is done via a basic moving average using a fixed +rectangualr window of size `window`. +At the end and start of teh timeseries only half of the window can be used for averaging. """ @kwdef struct MovingAverageSmoothing window::Int = 10 From 4081286a0fa584fc6fde88c207ad7370bb8431a8 Mon Sep 17 00:00:00 2001 From: Datseris Date: Wed, 21 May 2025 12:43:38 +0100 Subject: [PATCH 09/22] finish things and add to docs --- docs/src/index.md | 6 +- src/detrending/aryan_original_cde.jl | 113 --------------------------- src/detrending/simple.jl | 4 +- src/detrending/smoothing.jl | 37 ++++++++- 4 files changed, 41 insertions(+), 119 deletions(-) delete mode 100644 src/detrending/aryan_original_cde.jl diff --git a/docs/src/index.md b/docs/src/index.md index f7fb032..bdb6230 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -32,14 +32,16 @@ ManifoldProjection ## Detrending methods ```@docs +PolynomialDetrending ``` ## Smoothing (or detrending) methods -These methods can be either smoothing or detrending depending on how large -the smoothing window is taken! +These methods can be either smoothing or detrending depending on your context! ```@docs +MovingAverageSmoothing +LoessSmoothing ``` ## Product methods diff --git a/src/detrending/aryan_original_cde.jl b/src/detrending/aryan_original_cde.jl deleted file mode 100644 index 0d9eeb5..0000000 --- a/src/detrending/aryan_original_cde.jl +++ /dev/null @@ -1,113 +0,0 @@ -using Polynomials -using Statistics -using LinearAlgebra -using EmpiricalModeDecomposition -using HPFilter -using Loess -using MarketData -using ComplexityMeasures - -function closing_stock_timeseries(name::String, start::DateTime, finish::DateTime, freq::String) - opt = YahooOpt(period1 = start, period2 = finish, interval = freq) - stockdata = yahoo(name, opt) - stock_close = stockdata["Close"] - actual_numbers = values(stock_close) - return float.(actual_numbers) -end - -function Complexity_Measures(y; r = std(y)*0.2, a=6) - permutation = entropy_normalized(OrdinalPatterns{4}(), y) - spectral = entropy_normalized(PowerSpectrum(), y) - dispersion = entropy_normalized(Dispersion(c = a), y) - approximate = complexity(ApproximateEntropy(; r), y) - sample = complexity(SampleEntropy(; r), y) - return permutation, spectral, dispersion, approximate, sample -end - -function detrend_ols(y) - n = length(y) - X = hcat(ones(n), collect(1:n)) - β = X \ y # OLS estimation - trend = X * β # Fitted trend - y_detrended = y - trend # de-trend data - return y_detrended -end - -function polynomial_detrend(y, deg=2) - x = 1:length(y) - p = fit(x, y, deg) - trend = p.(x) - mse = sum((y - trend).^2) / length(y) - y_detrended = y .-trend - return y_detrended -end - -function moving_average(y, window = 30) - n = length(y) - trend = zeros(n) - - for i in 1:n - half = (window - 1) ÷ 2 - start_idx = max(1, i - half) - end_idx = min(n, i + half) - trend[i] = mean(y[start_idx:end_idx]) - end - mse = sum((y - trend).^2) / n - detrend = y .- trend - return detrend -end - -function detrend_emd(y) - n = length(y) - t = 1:n - imf = emd(y, t) - trend = imf[7] + imf[6] - residual = sum(imf[i] for i in 1:5) - detrend = y .- trend - return detrend -end - -function detrend_hp(y, lambda = 1600) - n = length(y) - trend = HP(y, lambda) - detrend = y .- trend - mse = sum((y .- trend).^2) / n - return detrend -end - -function detrend_loess(y, s = 0.2) - n = length(y) - x = 1:n - model = loess(x, y, span = s) - trend = predict(model, x) - detrend = y .- trend - mse = sum((y .- trend).^2) / n - return detrend -end - -function mutual_information(x, y, n=1000) - x = copy(x) - y = copy(y) - Hx = entropy(ValueHistogram(10), Dataset(x)) - Hy = entropy(ValueHistogram(10), Dataset(y)) - Hxy = entropy(ValueHistogram(10), Dataset(x, y)) - mi = Hx + Hy - Hxy - - null = zeros(n) - for i in 1:n - shuffle!(x), shuffle!(y) - Hxy = entropy(ValueHistogram(10), Dataset(x, y)) - null[i] = Hx + Hy - Hxy - end - - mu = mean(null) - sd = std(null) - return mi, mu, sd -end - - -function main_analysis(timeseries, detrending, complexity_measure) - detrended = detrending(timeseries)[1] - measure = complexity_measure(detrended) - return measure -end \ No newline at end of file diff --git a/src/detrending/simple.jl b/src/detrending/simple.jl index 8a85b5b..5d88b11 100644 --- a/src/detrending/simple.jl +++ b/src/detrending/simple.jl @@ -1,8 +1,8 @@ -export PolynomialDetrending, LinearDetrending +export PolynomialDetrending import Polynomials """ - PolyNnmialDetrending(degree::Int = 1) <: Decomposition + PolynomialDetrending(degree::Int = 1) <: Decomposition Decompose timeseries `s` into a **sum** `x + r` where `x` is the trend and `r` the residual. The trend is a fitted polynomial guaranteed to have given `degree`. diff --git a/src/detrending/smoothing.jl b/src/detrending/smoothing.jl index da64993..357492a 100644 --- a/src/detrending/smoothing.jl +++ b/src/detrending/smoothing.jl @@ -1,9 +1,9 @@ -export MovingAverageSmoothing +export MovingAverageSmoothing, LoessSmoothing """ MovingAverageSmoothing(window::Int = 10) <: Decomposition -Decompose timeseries `s` into a **sum** `x + r` where `x` is the smoothened singla +Decompose timeseries `s` into a **sum** `x + r` where `x` is the smoothened signal and `r` the residual. The smoothing is done via a basic moving average using a fixed rectangualr window of size `window`. At the end and start of teh timeseries only half of the window can be used for averaging. @@ -23,4 +23,37 @@ function decompose(t, s, mas::MovingAverageSmoothing) smooth[i] = mean(@view s[start_idx:end_idx]) end return smooth, s .- smooth +end + +import Loess + + + +""" + LoessSmoothing(; span, degree, cell) <: Decomposition + +Decompose timeseries `s` into a **sum** `x + r` where `x` is the smoothened signal +and `r` the residual. The smoothing is done via locally estimated scatterplot smoothing +(loess) using Loess.jl and the provided keywords. + +- `span`: The degree of smoothing, typically in [0,1]. Smaller values result in smaller + local context in fitting. +- `degree`: Polynomial degree. +- `cell`: Control parameter for bucket size. Internal interpolation nodes will be +added to the K-D tree until the number of bucket element is below `n * cell * span`. +""" +@kwdef struct LoessSmoothing{S, C} + span::S = 0.75 + degree::Int = 2 + cell::C = 0.2 +end + +function detrend_loess(y, s = 0.2) + n = length(y) + x = 1:n + model = loess(x, y, span = s) + trend = predict(model, x) + detrend = y .- trend + mse = sum((y .- trend).^2) / n + return detrend end \ No newline at end of file From af79185785914d72b5c52530d47e67320097a5c9 Mon Sep 17 00:00:00 2001 From: Datseris Date: Wed, 21 May 2025 12:51:56 +0100 Subject: [PATCH 10/22] update workflows --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56dd6cf..0968dec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: actions/cache@v1 + - uses: actions/cache@v4 env: cache-name: cache-artifacts with: @@ -45,6 +45,7 @@ jobs: ${{ runner.os }}-test- ${{ runner.os }}- - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v1 From b61d87d3ee4c309d5a8a1198dcbd4ed45cc66ec7 Mon Sep 17 00:00:00 2001 From: Datseris Date: Wed, 21 May 2025 12:58:07 +0100 Subject: [PATCH 11/22] add Loess --- Project.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Project.toml b/Project.toml index 47aef85..eb4912f 100644 --- a/Project.toml +++ b/Project.toml @@ -9,6 +9,7 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" DelayEmbeddings = "5732040d-69e3-5649-938a-b6b4f237613f" FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" LPVSpectral = "26dcc766-85df-5edc-b560-6076d5dbac63" +Loess = "4345ca2d-374a-55d4-8d30-97f9976e7612" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Neighborhood = "645ca80c-8b79-4109-87ea-e1f58159d116" Polynomials = "f27b6e38-b328-58d1-80ce-0feddd5e7a45" @@ -21,6 +22,7 @@ BandedMatrices = "1" DelayEmbeddings = "2" FFTW = "1.2" LPVSpectral = "0.3" +Loess = "0.6" Neighborhood = "0.2" Polynomials = "4" StaticArrays = "1" From 6387e2c26ecee80e54712dab12ca544d789b59c9 Mon Sep 17 00:00:00 2001 From: George Datseris Date: Fri, 11 Jul 2025 10:26:32 +0100 Subject: [PATCH 12/22] use correct module in docs --- docs/make.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/make.jl b/docs/make.jl index b73e7f3..1385baf 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -14,7 +14,7 @@ Downloads.download( ) include("build_docs_with_style.jl") -build_docs_with_style(pages, Attractors; +build_docs_with_style(pages, SignalDecomposition; authors = "George Datseris ", expandfirst = ["index.md"], # this is the first script that loads colorscheme ) From 21a8ee81c5fdaa038b365dd6fda4d91675c8b3f1 Mon Sep 17 00:00:00 2001 From: George Datseris Date: Fri, 11 Jul 2025 10:46:39 +0100 Subject: [PATCH 13/22] fix all tests --- Project.toml | 4 ++-- docs/Project.toml | 2 +- docs/make.jl | 2 +- docs/src/examples.md | 4 ++-- src/detrending/simple.jl | 2 +- src/detrending/smoothing.jl | 21 +++++++-------------- test/detrending_test.jl | 18 ++++++++++++++++++ test/runtests.jl | 2 +- 8 files changed, 33 insertions(+), 22 deletions(-) create mode 100644 test/detrending_test.jl diff --git a/Project.toml b/Project.toml index eb4912f..d52ed1b 100644 --- a/Project.toml +++ b/Project.toml @@ -31,8 +31,8 @@ julia = "1.9" [extras] DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" -DynamicalSystemsBase = "6e36e845-645a-534a-86f2-f5d4aa5a06b4" +PredefinedDynamicalSystems = "31e2f376-db9e-427a-b76e-a14f56347a14" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test", "DelimitedFiles", "DynamicalSystemsBase"] +test = ["Test", "DelimitedFiles", "PredefinedDynamicalSystems"] diff --git a/docs/Project.toml b/docs/Project.toml index b598671..251a5cc 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -2,7 +2,7 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DocumenterTools = "35a29f4d-8980-5a13-9543-d66fff28ecb8" -DynamicalSystemsBase = "6e36e845-645a-534a-86f2-f5d4aa5a06b4" +PredefinedDynamicalSystems = "31e2f376-db9e-427a-b76e-a14f56347a14" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" SignalDecomposition = "11a47235-7b84-4c7c-b885-fc3e2a9cf955" CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" diff --git a/docs/make.jl b/docs/make.jl index 1385baf..a432e67 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,6 +1,6 @@ cd(@__DIR__) using SignalDecomposition -using DynamicalSystemsBase +using PredefinedDynamicalSystems pages = [ "index.md", diff --git a/docs/src/examples.md b/docs/src/examples.md index 77b8815..5b87e27 100644 --- a/docs/src/examples.md +++ b/docs/src/examples.md @@ -3,7 +3,7 @@ Only a few examples are shown here. Every method has an example (and plotting co ## Nonlinear ```@example docs -using SignalDecomposition, DynamicalSystemsBase, Random, Statistics +using SignalDecomposition, PredefinedDynamicalSystems, Random, Statistics he = Systems.henon() tr = trajectory(he, 10000; Ttr = 100) @@ -59,7 +59,7 @@ Furthermore, by construction, the `x` component of `Sinusoidal` will always be a ## Product ```@example docs -using SignalDecomposition, DynamicalSystems, Plots +using SignalDecomposition, PredefinedDynamicalSystems, Plots ds = Systems.lorenz() tr = trajectory(ds, 20; dt = 0.002, Ttr = 100) diff --git a/src/detrending/simple.jl b/src/detrending/simple.jl index 5d88b11..6ee2282 100644 --- a/src/detrending/simple.jl +++ b/src/detrending/simple.jl @@ -1,4 +1,4 @@ -export PolynomialDetrending +export PolynomialDetrending, NoDecomposition import Polynomials """ diff --git a/src/detrending/smoothing.jl b/src/detrending/smoothing.jl index 357492a..3f900d5 100644 --- a/src/detrending/smoothing.jl +++ b/src/detrending/smoothing.jl @@ -1,4 +1,5 @@ export MovingAverageSmoothing, LoessSmoothing +import Loess """ MovingAverageSmoothing(window::Int = 10) <: Decomposition @@ -8,7 +9,7 @@ and `r` the residual. The smoothing is done via a basic moving average using a f rectangualr window of size `window`. At the end and start of teh timeseries only half of the window can be used for averaging. """ -@kwdef struct MovingAverageSmoothing +@kwdef struct MovingAverageSmoothing <: Decomposition window::Int = 10 end @@ -25,10 +26,6 @@ function decompose(t, s, mas::MovingAverageSmoothing) return smooth, s .- smooth end -import Loess - - - """ LoessSmoothing(; span, degree, cell) <: Decomposition @@ -42,18 +39,14 @@ and `r` the residual. The smoothing is done via locally estimated scatterplot sm - `cell`: Control parameter for bucket size. Internal interpolation nodes will be added to the K-D tree until the number of bucket element is below `n * cell * span`. """ -@kwdef struct LoessSmoothing{S, C} +@kwdef struct LoessSmoothing{S, C} <: Decomposition span::S = 0.75 degree::Int = 2 cell::C = 0.2 end -function detrend_loess(y, s = 0.2) - n = length(y) - x = 1:n - model = loess(x, y, span = s) - trend = predict(model, x) - detrend = y .- trend - mse = sum((y .- trend).^2) / n - return detrend +function decompose(t, s, method::LoessSmoothing) + model = Loess.loess(t, s, span = method.span) + trend = Loess.predict(model, t) + return trend, s .- trend end \ No newline at end of file diff --git a/test/detrending_test.jl b/test/detrending_test.jl new file mode 100644 index 0000000..87e34a9 --- /dev/null +++ b/test/detrending_test.jl @@ -0,0 +1,18 @@ +using SignalDecomposition, Test + +@testset "detrending" begin + detrending_methods = [ + PolynomialDetrending(1), PolynomialDetrending(2), NoDecomposition(), + MovingAverageSmoothing(), LoessSmoothing(), + ] + + N = 10000 + s = float.(1:N) + s .+= 0.1rand(N) + + @testset "$(nameof(typeof(m)))" for m in detrending_methods + trend, residual = decompose(s, m) + err = rmse(s, trend) + @test abs(err) < 1e-1 # noise is this magnitude. + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 4392e16..ce9cbd7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,7 +1,7 @@ cd(@__DIR__) using SignalDecomposition using DelimitedFiles, Test, Random, Statistics -using DynamicalSystemsBase +using PredefinedDynamicalSystems ds = Systems.lorenz() tr = trajectory(ds, 500.0; dt = 0.05, Ttr = 100) From c10f28eeeacccb1375450780d03cd87dcfca5fd5 Mon Sep 17 00:00:00 2001 From: George Datseris Date: Thu, 17 Jul 2025 08:49:28 +0100 Subject: [PATCH 14/22] make subtypes of decompo-sition --- src/detrending/smoothing.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/detrending/smoothing.jl b/src/detrending/smoothing.jl index 3f900d5..a8363e3 100644 --- a/src/detrending/smoothing.jl +++ b/src/detrending/smoothing.jl @@ -38,15 +38,17 @@ and `r` the residual. The smoothing is done via locally estimated scatterplot sm - `degree`: Polynomial degree. - `cell`: Control parameter for bucket size. Internal interpolation nodes will be added to the K-D tree until the number of bucket element is below `n * cell * span`. +- `normalize`: Normalize the scale of each predicitor. (default true when `m > 1`) """ @kwdef struct LoessSmoothing{S, C} <: Decomposition span::S = 0.75 degree::Int = 2 cell::C = 0.2 + normalize::Bool = true end function decompose(t, s, method::LoessSmoothing) - model = Loess.loess(t, s, span = method.span) + model = Loess.loess(t, s; span = method.span, normalize = method.normalize, degree = method.degree, cell = method.cell) trend = Loess.predict(model, t) return trend, s .- trend -end \ No newline at end of file +end From 02ebb5d4241914c36b5be8afa1585e764869f65d Mon Sep 17 00:00:00 2001 From: George Datseris Date: Thu, 17 Jul 2025 09:30:56 +0100 Subject: [PATCH 15/22] add HP filter --- Project.toml | 4 ++- docs/src/index.md | 5 +++ src/detrending/specialized.jl | 57 +++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 src/detrending/specialized.jl diff --git a/Project.toml b/Project.toml index d52ed1b..ddfa400 100644 --- a/Project.toml +++ b/Project.toml @@ -9,11 +9,12 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" DelayEmbeddings = "5732040d-69e3-5649-938a-b6b4f237613f" FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" LPVSpectral = "26dcc766-85df-5edc-b560-6076d5dbac63" -Loess = "4345ca2d-374a-55d4-8d30-97f9976e7612" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Loess = "4345ca2d-374a-55d4-8d30-97f9976e7612" Neighborhood = "645ca80c-8b79-4109-87ea-e1f58159d116" Polynomials = "f27b6e38-b328-58d1-80ce-0feddd5e7a45" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" @@ -25,6 +26,7 @@ LPVSpectral = "0.3" Loess = "0.6" Neighborhood = "0.2" Polynomials = "4" +SparseArrays = "1.11" StaticArrays = "1" Statistics = "1" julia = "1.9" diff --git a/docs/src/index.md b/docs/src/index.md index bdb6230..bd20842 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -24,6 +24,7 @@ Sinusoidal ``` ## Nonlinear methods + ```@docs ExtremelySimpleNL ManifoldProjection @@ -42,19 +43,23 @@ These methods can be either smoothing or detrending depending on your context! ```@docs MovingAverageSmoothing LoessSmoothing +HodrickPrescott ``` ## Product methods + ```@docs ProductInversion ``` ## Miscellaneous methods + ```@docs TimeAnomaly ``` ## Utilities + Simple utility functions to check how good the decomposition is for your data: ```@docs rmse diff --git a/src/detrending/specialized.jl b/src/detrending/specialized.jl new file mode 100644 index 0000000..cd4f2c1 --- /dev/null +++ b/src/detrending/specialized.jl @@ -0,0 +1,57 @@ +# Hodrick–Prescott filter obtained from +# https://github.com/sdBrinkmann/HPFilter.jl, +export HodrickPrescott +using SparseArrays, LinearAlgebra + +""" + HodrickPrescott(; λ = 1600, iter = 1) + +Decompose timeseries `s` into a **sum** `x + r` where `x` is the trend +and `r` the residual according to the +[Hodrick-Prescott](https://en.wikipedia.org/wiki/Hodrick%E2%80%93Prescott_filter) filter. +The residual is called the "cyclic" component in this context. +The keyword `iter` controls the boosted (or iterative) version of the filter, +based on Peter Phillips and Zhentao Shi (2019): "Boosting the Hodrick-Prescott Filter". +""" +@kwdef struct HodrickPrescott <: Decomposition + λ::Int = 1600 + iter::Int = 1 +end + +function decompose(t, x::AbstractVector, method::HodricPrescott) + (; λ, iter) = method.λ + n = length(x) + m = 2 + @assert n > m + I = Diagonal(ones(n)) + D = spdiagm( + 0 => fill(1, n-m), + -1 => fill(-2, n-m), + -2 => fill(1, n-m) + ) + @inbounds D = D[1:n,1:n-m] + S = (I + λ * D * D') + function solve(S,x,iter) + f = S \ x + if iter > 1 + return solve(S,x-f,iter-1) + else + return x - f + end + end + solution = solve(S, x, iter) + trend = x - solve(S, x, iter) + res = -solution + return trend, res +end + +# empirical orthogonal +function detrend_emd(y) + n = length(y) + t = 1:n + imf = emd(y, t) + trend = imf[7] + imf[6] + residual = sum(imf[i] for i in 1:5) + detrend = y .- trend + return detrend +end \ No newline at end of file From 97f413c4c74e8280603f08116288872b54860ae4 Mon Sep 17 00:00:00 2001 From: George Datseris Date: Thu, 17 Jul 2025 09:32:49 +0100 Subject: [PATCH 16/22] add it to tests --- test/detrending_test.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/detrending_test.jl b/test/detrending_test.jl index 87e34a9..4c47ec3 100644 --- a/test/detrending_test.jl +++ b/test/detrending_test.jl @@ -3,7 +3,7 @@ using SignalDecomposition, Test @testset "detrending" begin detrending_methods = [ PolynomialDetrending(1), PolynomialDetrending(2), NoDecomposition(), - MovingAverageSmoothing(), LoessSmoothing(), + MovingAverageSmoothing(), LoessSmoothing(), HodrickPrescott(), ] N = 10000 From c8bb0db9c592c5d03dc79bd199a2248c2782fb9c Mon Sep 17 00:00:00 2001 From: George Datseris Date: Thu, 17 Jul 2025 11:04:36 +0100 Subject: [PATCH 17/22] fix test call --- test/runtests.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index ce9cbd7..ed3b606 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,14 +4,14 @@ using DelimitedFiles, Test, Random, Statistics using PredefinedDynamicalSystems ds = Systems.lorenz() -tr = trajectory(ds, 500.0; dt = 0.05, Ttr = 100) +tr, tvec = trajectory(ds, 500.0; Δt = 0.05, Ttr = 100) lorenzx = tr[:, 1]/std(tr[:, 1]) -tr = trajectory(ds, 20; dt = 0.002, Ttr = 100) +tr, tvec = trajectory(ds, 20; Δt = 0.002, Ttr = 100) lorenzx_slow = tr[:, 1]/std(tr[:, 1]) ds = Systems.roessler() -tr = trajectory(ds, 500.0, dt = 0.05, Ttr = 10) +tr, tvec = trajectory(ds, 500.0, Δt = 0.05, Ttr = 10) roesslerz = tr[:, 3]/std(tr[:, 3]) L = length(lorenzx) From f3aced610056a5153088ec6836b9ce09708e478b Mon Sep 17 00:00:00 2001 From: George Datseris Date: Thu, 17 Jul 2025 12:02:30 +0100 Subject: [PATCH 18/22] make test project file --- Project.toml | 8 -------- docs/Project.toml | 3 +++ test/Project.toml | 9 +++++++++ 3 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 test/Project.toml diff --git a/Project.toml b/Project.toml index ddfa400..1c6a1ae 100644 --- a/Project.toml +++ b/Project.toml @@ -30,11 +30,3 @@ SparseArrays = "1.11" StaticArrays = "1" Statistics = "1" julia = "1.9" - -[extras] -DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" -PredefinedDynamicalSystems = "31e2f376-db9e-427a-b76e-a14f56347a14" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[targets] -test = ["Test", "DelimitedFiles", "PredefinedDynamicalSystems"] diff --git a/docs/Project.toml b/docs/Project.toml index 251a5cc..9ea2cd9 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -6,3 +6,6 @@ PredefinedDynamicalSystems = "31e2f376-db9e-427a-b76e-a14f56347a14" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" SignalDecomposition = "11a47235-7b84-4c7c-b885-fc3e2a9cf955" CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" + +[compat] +PredefinedDynamicalSystems = "1.4" \ No newline at end of file diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 0000000..09531d4 --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,9 @@ +[deps] +DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" +PredefinedDynamicalSystems = "31e2f376-db9e-427a-b76e-a14f56347a14" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[compat] +PredefinedDynamicalSystems = "1.4" From 591934c90a12ffd7bf07cf7b7f29b32e59ce040b Mon Sep 17 00:00:00 2001 From: George Datseris Date: Fri, 25 Jul 2025 10:25:13 +0100 Subject: [PATCH 19/22] add delay embedding in test --- test/Project.toml | 1 + test/nonlinear_test.jl | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/Project.toml b/test/Project.toml index 09531d4..406a085 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -4,6 +4,7 @@ PredefinedDynamicalSystems = "31e2f376-db9e-427a-b76e-a14f56347a14" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +DelayEmbeddings = "5732040d-69e3-5649-938a-b6b4f237613f" [compat] PredefinedDynamicalSystems = "1.4" diff --git a/test/nonlinear_test.jl b/test/nonlinear_test.jl index 85ae466..9012eec 100644 --- a/test/nonlinear_test.jl +++ b/test/nonlinear_test.jl @@ -1,3 +1,6 @@ +import DelayEmbeddings +using Test, SignalDecomposition + @testset "ExtremelySimpleNL" begin # input s1 = lorenzx + 0.1noise # input @@ -10,7 +13,7 @@ w = 1 # theiler window for (name, s) in zip(("lorenz", "roessler"), (s1, s2)) - τ = estimate_delay(s, "mi_min") # 5 # delaytime + τ = DelayEmbeddings.estimate_delay(s, "mi_min") # 5 # delaytime method = ExtremelySimpleNL(k, ℓ, τ, w, ε) x, r = decompose(s, method) From 3f86e8f0369cb4b76da368012da3c664ba2645a4 Mon Sep 17 00:00:00 2001 From: George Datseris Date: Fri, 25 Jul 2025 10:34:44 +0100 Subject: [PATCH 20/22] fix all tests --- CHANGELOG.md | 4 ++-- test/Project.toml | 1 + test/lpv_test.jl | 2 +- test/nonlinear_test.jl | 4 ++-- test/product_test.jl | 2 +- test/timeanomaly_test.jl | 1 + 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ee11fa..6c77fcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,8 @@ - Add new functionality for the package: detrending - Add new functionality for the package: smoothing -- Add new `Decompositions`: `PolynomialDetrending, NoDecomposition, MovingAverageSmoothing`. -- Package is now part of DynamicalSystems.jl as well. +- Add new `Decompositions`: `PolynomialDetrending, NoDecomposition, MovingAverageSmoothing, LoessSmoothing, HodrickPrescott`. +- Package is now part of DynamicalSystems.jl as well under the category "Nonlinear Timeseries Analysis". # v1.1 diff --git a/test/Project.toml b/test/Project.toml index 406a085..3f3442c 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -5,6 +5,7 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" DelayEmbeddings = "5732040d-69e3-5649-938a-b6b4f237613f" +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" [compat] PredefinedDynamicalSystems = "1.4" diff --git a/test/lpv_test.jl b/test/lpv_test.jl index c6d4cf7..b980f04 100644 --- a/test/lpv_test.jl +++ b/test/lpv_test.jl @@ -37,7 +37,7 @@ end @test errres < 0.1 errori = nrmse(s, x .+ r) @test errori < 1e-15 - + # figure() # ax1 = subplot(211) # plot(pu; alpha = 0.75, label = "x component") diff --git a/test/nonlinear_test.jl b/test/nonlinear_test.jl index 9012eec..0a7de01 100644 --- a/test/nonlinear_test.jl +++ b/test/nonlinear_test.jl @@ -32,7 +32,7 @@ end @testset "ManifoldProjection" begin @testset "Henon" begin he = Systems.henon() - tr = trajectory(he, 10000; Ttr = 100) + tr, _ = trajectory(he, 10000; Ttr = 100) Random.seed!(151521) z = tr[:, 1] s = z .+ randn(10001)*0.1*std(z) @@ -63,7 +63,7 @@ end @testset "lorenz" begin lo = Systems.lorenz() - tr = trajectory(lo, 1000; Ttr = 100, dt = 0.1) + tr, _ = trajectory(lo, 1000; Ttr = 100, Δt = 0.1) Random.seed!(151521) z = tr[:, 1] s = z .+ randn(10001)*0.2*std(z) diff --git a/test/product_test.jl b/test/product_test.jl index 7530d92..8b5fde1 100644 --- a/test/product_test.jl +++ b/test/product_test.jl @@ -24,7 +24,7 @@ cases = [ errres = nrmse(case[2], r) # by definition 0 fullerr = nrmse(s, x .* r) @test fullerr < 0.1 - println(" case $i errper=$errper, errres=$errres ") + # println(" case $i errper=$errper, errres=$errres ") # # figure() # ax1 = subplot(311) diff --git a/test/timeanomaly_test.jl b/test/timeanomaly_test.jl index 9af229a..7242dac 100644 --- a/test/timeanomaly_test.jl +++ b/test/timeanomaly_test.jl @@ -1,3 +1,4 @@ +using SignalDecomposition, Test using Dates, Random @testset "TimeAnomaly" begin From 46b92c3f1f3670bd374660e35a7b4ad417122abd Mon Sep 17 00:00:00 2001 From: George Datseris Date: Fri, 25 Jul 2025 12:16:30 +0100 Subject: [PATCH 21/22] fix docs, fix tests --- .gitignore | 1 + docs/make.jl | 1 + docs/src/examples.md | 51 ++++++++++++++++++++--------------- docs/src/index.md | 4 +-- docs/style.jl | 38 ++++++++++++++++++++++++++ src/SignalDecomposition.jl | 1 + src/detrending/specialized.jl | 15 ++--------- test/runtests.jl | 2 ++ 8 files changed, 76 insertions(+), 37 deletions(-) create mode 100644 docs/style.jl diff --git a/.gitignore b/.gitignore index 9c53da4..c7324ae 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ videos plots notebooks _research +docs/build_docs_with_style.jl ################################################################################ # Julia # diff --git a/docs/make.jl b/docs/make.jl index a432e67..99a9577 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,6 +1,7 @@ cd(@__DIR__) using SignalDecomposition using PredefinedDynamicalSystems +decompose = SignalDecomposition.decompose pages = [ "index.md", diff --git a/docs/src/examples.md b/docs/src/examples.md index 5b87e27..d778d32 100644 --- a/docs/src/examples.md +++ b/docs/src/examples.md @@ -4,9 +4,10 @@ Only a few examples are shown here. Every method has an example (and plotting co ## Nonlinear ```@example docs using SignalDecomposition, PredefinedDynamicalSystems, Random, Statistics +decompose = SignalDecomposition.decompose he = Systems.henon() -tr = trajectory(he, 10000; Ttr = 100) +tr, tvec = trajectory(he, 10000; Ttr = 100) Random.seed!(151521) z = tr[:, 1] s = z .+ randn(10001)*0.1*std(z) @@ -20,16 +21,18 @@ summary(x) This method is nicely highlighted once going into the state space and looking at the attractor: ```@example docs -fig, ax = scatter(s[1:end-1], s[2:end]; markersize = 1, label = "input", msw = 0) -scatter!(p2, z[1:end-1], z[2:end], ms = 1, label = "real", color = :black, msw = 0) -scatter!(p2, x[1:end-1], x[2:end], ms = 1, label = "output", alpha = 0.5, msw = 0) -savefig(p2, "henon.png") # hide +using CairoMakie +fig, ax = scatter(s[1:end-1], s[2:end]; markersize = 4, label = "input") +scatter!(ax, z[1:end-1], z[2:end], markersize = 4, label = "real", color = :black) +scatter!(ax, x[1:end-1], x[2:end], markersize = 3, label = "output", alpha = 0.5, color = :red) +axislegend(ax) +fig ``` -![](henon.png) ## TimeAnomaly and Sinusoidal ```@example docs -using SignalDecomposition, Dates, Random, Plots +using SignalDecomposition, Dates, Random, CairoMakie + Random.seed!(41516) y = Date(2001):Day(1):Date(2025) dy = dayofyear.(y) @@ -43,12 +46,14 @@ t = collect(1:length(y)) ./ 365.26 # true time in years x2, r2 = decompose(t, sy, Sinusoidal([1.0, 2.0])) -p3 = plot(t, sy, label = "input") -plot!(p3, t, cy, label = "true periodic", color = :black, ls = :dash) -plot!(p3, t, x, label = "TimeAnomaly", alpha = 1.0, color = :red) -plot!(p3, t, x2, label = "Sinusoidal", alpha = 0.5, color = :green) -xlabel!(p3, "years") -xlims!(p3, 0, 1) # zoom in +fig, ax = lines(t, sy, label = "input") +lines!(ax, t, cy; label = "true periodic", color = :black, linestyle = :dash) +lines!(ax, t, x; label = "TimeAnomaly", alpha = 1.0, color = :red) +lines!(ax, t, x2; label = "Sinusoidal", alpha = 0.5, color = (0.1, 0.8, 0.2)) +ax.xlabel = "years" +axislegend(ax) +xlims!(ax, 0, 1) # zoom in +fig ``` Although not immediately obvious from the figure, `Sinusoidal` performs better: @@ -59,14 +64,15 @@ Furthermore, by construction, the `x` component of `Sinusoidal` will always be a ## Product ```@example docs -using SignalDecomposition, PredefinedDynamicalSystems, Plots +using SignalDecomposition, PredefinedDynamicalSystems, CairoMakie +using Statistics: std ds = Systems.lorenz() -tr = trajectory(ds, 20; dt = 0.002, Ttr = 100) +tr, _ = trajectory(ds, 20; Δt = 0.002, Ttr = 100) lorenzx_slow = tr[:, 1]/std(tr[:, 1]) ds = Systems.roessler() -tr = trajectory(ds, 500.0, dt = 0.05, Ttr = 10) +tr, _ = trajectory(ds, 500.0, Δt = 0.05, Ttr = 10) roesslerz = tr[:, 3]/std(tr[:, 3]) roesslerz[roesslerz .≤ 0.1] .= 0 @@ -74,10 +80,11 @@ s = lorenzx_slow .* roesslerz m = ProductInversion(roesslerz, 0.1:0.1:10) x, r = decompose(s, m) -l = (4, 1) -p5 = plot(s, label = "input s") -plot!(p5, x .* r, label = "decomposed") -p6 = plot(lorenzx_slow, label = "original r") -plot!(p6, x, label = "decomposed r") -plot(p5, p6, layout=(2,1)) +fig, ax = lines(s, label = "input s") +lines!(ax, x .* r, label = "decomposed", linestyle = :dash) +axislegend(ax) +ax, = lines(fig[2,1], lorenzx_slow, label = "original r") +lines!(ax, x, label = "decomposed r") +axislegend(ax) +fig ``` diff --git a/docs/src/index.md b/docs/src/index.md index bd20842..dc373f3 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -7,7 +7,7 @@ ## API ```@docs -decompose +SignalDecomposition.decompose ``` Subtypes of `Decomposition` are listed in the rest of this page. @@ -34,6 +34,7 @@ ManifoldProjection ```@docs PolynomialDetrending +HodrickPrescott ``` ## Smoothing (or detrending) methods @@ -43,7 +44,6 @@ These methods can be either smoothing or detrending depending on your context! ```@docs MovingAverageSmoothing LoessSmoothing -HodrickPrescott ``` ## Product methods diff --git a/docs/style.jl b/docs/style.jl new file mode 100644 index 0000000..691fffa --- /dev/null +++ b/docs/style.jl @@ -0,0 +1,38 @@ +# Color theme definitions +struct CyclicContainer <: AbstractVector{String} + c::Vector{String} + n::Int +end +CyclicContainer(c) = CyclicContainer(c, 0) + +Base.length(c::CyclicContainer) = length(c.c) +Base.size(c::CyclicContainer) = size(c.c) +Base.iterate(c::CyclicContainer, state=1) = iterate(c.c, state) +Base.getindex(c::CyclicContainer, i) = c.c[(i-1)%length(c.c) + 1] +Base.getindex(c::CyclicContainer, i::AbstractArray) = c.c[i] +function Base.getindex(c::CyclicContainer) + c.n += 1 + c[c.n] +end +Base.iterate(c::CyclicContainer, i = 1) = iterate(c.c, i) + +COLORSCHEME = [ + "#7143E0", + "#191E44", + "#0A9A84", + "#AF9327", + "#791457", + "#6C768C", +] + +COLORS = CyclicContainer(COLORSCHEME) +LINESTYLES = CyclicContainer(["-", ":", "--", "-."]) + +# other styling elements for Makie +set_theme!(; + palette = (color = COLORSCHEME,), + fontsize = 22, + figure_padding = 8, + size = (800, 400), + linewidth = 3.0, +) diff --git a/src/SignalDecomposition.jl b/src/SignalDecomposition.jl index 0c57793..ceb870d 100644 --- a/src/SignalDecomposition.jl +++ b/src/SignalDecomposition.jl @@ -34,6 +34,7 @@ include("nonlinear/projection.jl") include("misc/anomaly.jl") include("detrending/simple.jl") include("detrending/smoothing.jl") +include("detrending/specialized.jl") diff --git a/src/detrending/specialized.jl b/src/detrending/specialized.jl index cd4f2c1..891c7eb 100644 --- a/src/detrending/specialized.jl +++ b/src/detrending/specialized.jl @@ -18,8 +18,8 @@ based on Peter Phillips and Zhentao Shi (2019): "Boosting the Hodrick-Prescott F iter::Int = 1 end -function decompose(t, x::AbstractVector, method::HodricPrescott) - (; λ, iter) = method.λ +function decompose(t, x::AbstractVector, method::HodrickPrescott) + (; λ, iter) = method n = length(x) m = 2 @assert n > m @@ -44,14 +44,3 @@ function decompose(t, x::AbstractVector, method::HodricPrescott) res = -solution return trend, res end - -# empirical orthogonal -function detrend_emd(y) - n = length(y) - t = 1:n - imf = emd(y, t) - trend = imf[7] + imf[6] - residual = sum(imf[i] for i in 1:5) - detrend = y .- trend - return detrend -end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index ed3b606..99fbf3a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -33,3 +33,5 @@ include("lpv_test.jl") include("product_test.jl") include("nonlinear_test.jl") include("timeanomaly_test.jl") +include("detrending_test.jl") + From 04ef1d93a68efc0c50b7e249f0f88a11ea08801e Mon Sep 17 00:00:00 2001 From: George Datseris Date: Fri, 25 Jul 2025 12:48:45 +0100 Subject: [PATCH 22/22] fix docs? --- docs/make.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/make.jl b/docs/make.jl index 99a9577..dfde6f8 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,7 +1,7 @@ cd(@__DIR__) using SignalDecomposition -using PredefinedDynamicalSystems decompose = SignalDecomposition.decompose +using PredefinedDynamicalSystems pages = [ "index.md",