├── __init__.py ├── PyEMD ├── tests │ ├── __init__.py │ ├── test_all.py │ ├── test_emd_matlab.py │ ├── test_utils.py │ ├── test_compact.py │ ├── test_splines.py │ ├── test_visualization.py │ ├── test_bemd.py │ ├── test_checks.py │ ├── test_ceemdan.py │ ├── test_eemd.py │ ├── test_emd.py │ ├── test_emd2d.py │ └── test_extrema.py ├── experimental │ └── __init__.py ├── .gitignore ├── __init__.py ├── types.py ├── splines.py ├── utils.py ├── compact.py ├── checks.py ├── visualisation.py ├── BEMD.py ├── EEMD.py └── EMD2d.py ├── doc ├── .gitignore ├── check.rst ├── visualisation.rst ├── emd.rst ├── contact.rst ├── index.rst ├── Makefile ├── #Makefile# ├── ceemdan.rst ├── eemd.rst ├── usage.rst ├── examples.rst ├── intro.rst ├── speedup.rst ├── experimental.rst └── conf.py ├── example ├── eemd_example.png ├── hht_example.png ├── image_decomp.png ├── simple_example.png ├── jit_eemd_example.py ├── jit_emd_class_example.py ├── jit_emd_function_example.py ├── simple_example.py ├── eemd_example.py ├── image_example.py ├── emd_comparison.py ├── hht_example.py └── example_spline_capabilities.py ├── requirements-dev.txt ├── changelog.md ├── MANIFEST.in ├── perf_test ├── Dockerfile └── perf_test.py ├── .gitignore ├── .github ├── workflows │ ├── ci-lint.yml │ ├── ci-pr-test.yml │ └── ci-main-build-test-deploy.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── CITATION.cff ├── .readthedocs.yaml ├── requirements.txt ├── codecov.yml ├── .codecov.yml ├── .coveragerc ├── noxfile.py ├── pyproject.toml ├── Makefile ├── README.md └── LICENSE.md /__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /PyEMD/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | -------------------------------------------------------------------------------- /PyEMD/experimental/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /PyEMD/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/* 2 | *.pyc 3 | -------------------------------------------------------------------------------- /example/eemd_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laszukdawid/PyEMD/HEAD/example/eemd_example.png -------------------------------------------------------------------------------- /example/hht_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laszukdawid/PyEMD/HEAD/example/hht_example.png -------------------------------------------------------------------------------- /example/image_decomp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laszukdawid/PyEMD/HEAD/example/image_decomp.png -------------------------------------------------------------------------------- /example/simple_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laszukdawid/PyEMD/HEAD/example/simple_example.png -------------------------------------------------------------------------------- /doc/check.rst: -------------------------------------------------------------------------------- 1 | Statistical tests 2 | ================= 3 | 4 | Whitenoise check 5 | ---------------- 6 | 7 | .. autofunction:: PyEMD.checks.whitenoise_check 8 | -------------------------------------------------------------------------------- /doc/visualisation.rst: -------------------------------------------------------------------------------- 1 | Visualisation 2 | ============= 3 | 4 | A simple visualisation helper. 5 | 6 | .. autoclass:: PyEMD.Visualisation 7 | :members: 8 | 9 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv export --no-hashes --only-dev --no-emit-project -o requirements-dev.txt 3 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ## 1.2.1 2 | 3 | * Removed version cap from NumPy and SciPy dependencies 4 | * Introduction of the changelog 5 | 6 | ## 1.2.0 7 | 8 | * Adds significance tests. Only currently added test is *Whitenoise significance check*. 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.md 2 | include *.py 3 | include setup.cfg 4 | include MANIFEST.in 5 | include README.md 6 | include LICENCE.txt 7 | include requirements.txt 8 | recursive-include PyEMD *.py 9 | recursive-include example *.py 10 | -------------------------------------------------------------------------------- /perf_test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10.13 2 | 3 | COPY requirements.txt /tmp/requirements.txt 4 | RUN pip install -r /tmp/requirements.txt 5 | 6 | COPY . /app 7 | WORKDIR /app 8 | 9 | RUN pip install -e . 10 | 11 | CMD ["python", "perf_test/main.py"] -------------------------------------------------------------------------------- /doc/emd.rst: -------------------------------------------------------------------------------- 1 | EMD 2 | === 3 | 4 | *Empirical Mode Decomposition (EMD)* is an iterative procedure which decomposes signal into a set of oscillatory components, called *Intrisic Mode Functions* (IMFs). 5 | 6 | .. autoclass:: PyEMD.EMD 7 | :members: 8 | :special-members: 9 | -------------------------------------------------------------------------------- /PyEMD/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | __version__ = "1.9.0" 4 | logger = logging.getLogger("pyemd") 5 | 6 | from PyEMD.CEEMDAN import CEEMDAN # noqa 7 | from PyEMD.EEMD import EEMD # noqa 8 | from PyEMD.EMD import EMD # noqa 9 | from PyEMD.visualisation import Visualisation # noqa 10 | -------------------------------------------------------------------------------- /PyEMD/types.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | import numpy as np 4 | 5 | EmdArray = np.ndarray 6 | EmdDType = np.dtype 7 | 8 | try: 9 | import torch 10 | 11 | EmdArray = Union[np.ndarray, torch.Tensor] 12 | EmdDType = Union[np.dtype, torch.dtype] 13 | except ImportError: 14 | pass 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | .venv/ 3 | build/ 4 | dist/ 5 | *.egg-info 6 | 7 | .vscode/ 8 | .idea/ 9 | 10 | .mypy_cache 11 | __pycache__ 12 | .pytest_cache/ 13 | .ipynb_checkpoints/ 14 | 15 | *.swp 16 | *.pyc 17 | *.ipynb 18 | 19 | # Performance test results (timestamped, machine-specific) 20 | perf_test/results/ -------------------------------------------------------------------------------- /PyEMD/tests/test_all.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | 4 | if __name__ == "__main__": 5 | test_suite = unittest.defaultTestLoader.discover(".", "*test*.py") 6 | test_runner = unittest.TextTestRunner(resultclass=unittest.TextTestResult) 7 | result = test_runner.run(test_suite) 8 | sys.exit(not result.wasSuccessful()) 9 | -------------------------------------------------------------------------------- /example/jit_eemd_example.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from PyEMD import EEMD 4 | from PyEMD.experimental.jitemd import JitEMD, get_timeline 5 | 6 | s = np.random.random(100) 7 | t = get_timeline(len(s), s.dtype) 8 | 9 | emd = JitEMD() 10 | eemd = EEMD(ext_EMD=emd) 11 | imfs = eemd(s, t) 12 | 13 | print(imfs) 14 | print(imfs.shape) 15 | -------------------------------------------------------------------------------- /example/jit_emd_class_example.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from PyEMD.experimental.jitemd import JitEMD, get_timeline 4 | 5 | rng = np.random.RandomState(4132) 6 | s = rng.random(500) 7 | t = get_timeline(len(s), s.dtype) 8 | 9 | emd = JitEMD(spline_kind="akima") 10 | 11 | imfs = emd(s, t) 12 | 13 | print(imfs) 14 | print(imfs.shape) 15 | -------------------------------------------------------------------------------- /.github/workflows/ci-lint.yml: -------------------------------------------------------------------------------- 1 | name: Linter 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: psf/black@stable 11 | with: 12 | options: "--check --verbose" 13 | version: "~=25.11" 14 | - uses: isort/isort-action@v1 15 | with: 16 | sort-paths: PyEMD 17 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "If you use this software, please cite it as below." 3 | authors: 4 | - family-names: "Laszuk" 5 | given-names: "Dawid" 6 | orcid: "https://orcid.org/0000-0001-6811-3253" 7 | title: "Python implementation of Empirical Mode Decomposition algorithm" 8 | version: 1.0.1 9 | date-released: 2017-01-01 10 | doi: 10.5281/zenodo.5459184 11 | url: "https://github.com/laszukdawid/PyEMD" -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Required 2 | version: 2 3 | 4 | # Set the version of Python and other tools you might need 5 | build: 6 | os: ubuntu-20.04 7 | tools: 8 | python: "3.8" 9 | 10 | # Build documentation in the docs/ directory with Sphinx 11 | sphinx: 12 | configuration: doc/conf.py 13 | 14 | # Installing deps with: pip install -e .[doc] 15 | python: 16 | install: 17 | - method: pip 18 | path: . 19 | extra_requirements: 20 | - doc 21 | -------------------------------------------------------------------------------- /example/jit_emd_function_example.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import numpy as np 3 | 4 | from PyEMD.experimental.jitemd import emd, default_emd_config 5 | from PyEMD.experimental.jitemd import get_timeline 6 | 7 | rng = np.random.RandomState(4132) 8 | s = rng.random(500) 9 | t = get_timeline(len(s), s.dtype) 10 | 11 | config = copy.copy(default_emd_config) 12 | config["FIXE"] = 5 13 | imfs = emd(s, t, spline_kind="cubic", config=config) 14 | 15 | print(imfs) 16 | print(imfs.shape) 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv export --no-hashes --no-dev --no-emit-project -o requirements.txt 3 | colorama==0.4.6 ; sys_platform == 'win32' 4 | dill==0.4.0 5 | multiprocess==0.70.18 6 | numpy==2.2.6 ; python_full_version < '3.11' 7 | numpy==2.3.5 ; python_full_version >= '3.11' 8 | pathos==0.3.4 9 | pox==0.3.6 10 | ppft==1.7.7 11 | scipy==1.15.3 ; python_full_version < '3.11' 12 | scipy==1.16.3 ; python_full_version >= '3.11' 13 | tqdm==4.67.1 14 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: yes 4 | 5 | coverage: 6 | precision: 2 7 | round: down 8 | range: "50...100" 9 | 10 | status: 11 | project: yes 12 | 13 | patch: yes 14 | 15 | changes: yes 16 | 17 | ignore: 18 | - "__init__.py" 19 | - "PyEMD/tests" 20 | - "PyEMD/BEMD.py" 21 | - "PyEMD/EMD2d.py" 22 | - "PyEMD/compact.py" 23 | - "doc" 24 | - "example" 25 | 26 | comment: 27 | layout: "header, diff, changes" 28 | behavior: default 29 | 30 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: yes 4 | 5 | coverage: 6 | precision: 2 7 | round: down 8 | range: "50...100" 9 | 10 | status: 11 | project: yes 12 | 13 | patch: yes 14 | 15 | changes: yes 16 | 17 | ignore: 18 | - "__init__.py" 19 | - "PyEMD/tests/*" 20 | - "PyEMD/BEMD.py" 21 | - "PyEMD/EMD2d.py" 22 | - "PyEMD/visualisation.py" 23 | - "doc/*" 24 | - "example/*" 25 | 26 | comment: 27 | layout: "header, diff, changes" 28 | behavior: default 29 | 30 | -------------------------------------------------------------------------------- /doc/contact.rst: -------------------------------------------------------------------------------- 1 | Contact 2 | ======= 3 | 4 | The best way to reach out is through creating an issue on the GitHub page. 5 | That way anyone can help or chime in on the severity of the issue. 6 | Although that might seem "slower" than email, it is honestly much faster. 7 | 8 | 9 | **Tickets** 10 | https://github.com/laszukdawid/PyEMD/issues 11 | 12 | **Email** 13 | \\pyemd@\\dawid.lasz.uk (remove slashes \\). 14 | 15 | **GitHub** 16 | You can also visit `PyEMD GitHub project `_ page for this project. 17 | 18 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | 2 | PyEMD's documentation 3 | ===================== 4 | 5 | Writing documentation is hard. If more clarifications are needed, or you 6 | think others might benefit from extra explanation, don't hesitate to contact 7 | me through contact page. 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Table of Content 12 | 13 | intro 14 | usage 15 | speedup 16 | examples 17 | emd 18 | eemd 19 | ceemdan 20 | visualisation 21 | check 22 | Experimental 23 | contact 24 | 25 | Indices and tables 26 | ================== 27 | 28 | * :ref:`genindex` 29 | * :ref:`search` 30 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | [run] 3 | branch = True 4 | 5 | [report] 6 | # Regexes for lines to exclude from consideration 7 | exclude_lines = 8 | # Have to re-enable the standard pragma 9 | pragma: no cover 10 | 11 | # Don't complain about missing debug-only code: 12 | def __repr__ 13 | if self\.debug 14 | 15 | # Don't complain if tests don't hit defensive assertion code: 16 | raise AssertionError 17 | raise NotImplementedError 18 | 19 | # Don't complain if non-runnable code isn't run: 20 | if 0: 21 | if __name__ == .__main__.: 22 | 23 | ignore_errors = True 24 | 25 | [html] 26 | directory = coverage_html_report 27 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = PyEMD 8 | SOURCEDIR = . 9 | BUILDDIR = .build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /doc/#Makefile#: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = PyEMD 8 | SOURCEDIR = . 9 | BUILDDIR = .build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: laszukdawid 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Short code showing how to reproduce the issue. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Running environment** 23 | Provide operating system (OS) information, PyEMD version and describe virtual environment (if any). 24 | 25 | **Additional context** 26 | Add any other context about the problem here. 27 | -------------------------------------------------------------------------------- /example/simple_example.py: -------------------------------------------------------------------------------- 1 | # Author: Dawid Laszuk 2 | # Last update: 7/07/2017 3 | import numpy as np 4 | import pylab as plt 5 | from PyEMD import EMD 6 | 7 | # Define signal 8 | t = np.linspace(0, 1, 200) 9 | s = np.cos(11 * 2 * np.pi * t * t) + 6 * t * t 10 | 11 | # Execute EMD on signal 12 | IMF = EMD().emd(s, t) 13 | N = IMF.shape[0] + 1 14 | 15 | # Plot results 16 | plt.subplot(N, 1, 1) 17 | plt.plot(t, s, "r") 18 | plt.title("Input signal: $S(t)=cos(22\pi t^2) + 6t^2$") 19 | plt.xlabel("Time [s]") 20 | 21 | for n, imf in enumerate(IMF): 22 | plt.subplot(N, 1, n + 2) 23 | plt.plot(t, imf, "g") 24 | plt.title("IMF " + str(n + 1)) 25 | plt.xlabel("Time [s]") 26 | 27 | plt.tight_layout() 28 | plt.savefig("simple_example") 29 | plt.show() 30 | -------------------------------------------------------------------------------- /perf_test/perf_test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from PyEMD import EMD 4 | 5 | 6 | def test_prepare_points_simple(benchmark): 7 | emd = EMD() 8 | T_max = 50 9 | T = np.linspace(0, T_max, 10000) 10 | S = np.sin(T * 2 * np.pi) 11 | # max_pos = np.array([0.25, 1.25, 2.25, 3.25], dtype=np.float32) 12 | max_pos = np.arange(T_max - 1, dtype=np.float32) + 0.25 13 | max_val = np.ones(T_max - 1, dtype=np.float32) 14 | min_pos = np.arange(1, T_max, dtype=np.float32) - 0.25 15 | min_val = (-1) * np.ones(T_max - 1, dtype=np.float32) 16 | # min_pos = np.array([0.75, 1.75, 2.75, 3.75], dtype=np.float32) 17 | # min_val = np.array([-1, -1, -1, -1], dtype=np.float32) 18 | 19 | benchmark.pedantic( 20 | emd.prepare_points_parabol, args=(T, S, max_pos, max_val, min_pos, min_val), iterations=100, rounds=100 21 | ) 22 | -------------------------------------------------------------------------------- /.github/workflows/ci-pr-test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'PyEMD/**.py' 7 | 8 | jobs: 9 | 10 | build-n-test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | python-version: 16 | - "3.8" 17 | - "3.9" 18 | - "3.10" 19 | - "3.11" 20 | - "3.12" 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Running Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | - name: Install test dependencies 29 | run: | 30 | pip install --only-binary=numpy,scipy numpy scipy 31 | pip install -e .[test] 32 | - name: Run tests 33 | run: | 34 | python -m PyEMD.tests.test_all 35 | -------------------------------------------------------------------------------- /example/eemd_example.py: -------------------------------------------------------------------------------- 1 | # Author: Dawid Laszuk 2 | # Last update: 7/07/2017 3 | from PyEMD import EEMD 4 | import numpy as np 5 | import pylab as plt 6 | 7 | # Define signal 8 | t = np.linspace(0, 1, 200) 9 | 10 | sin = lambda x, p: np.sin(2 * np.pi * x * t + p) 11 | S = 3 * sin(18, 0.2) * (t - 0.2) ** 2 12 | S += 5 * sin(11, 2.7) 13 | S += 3 * sin(14, 1.6) 14 | S += 1 * np.sin(4 * 2 * np.pi * (t - 0.8) ** 2) 15 | S += t**2.1 - t 16 | 17 | # Assign EEMD to `eemd` variable 18 | eemd = EEMD() 19 | 20 | # Say we want detect extrema using parabolic method 21 | emd = eemd.EMD 22 | emd.extrema_detection = "parabol" 23 | 24 | # Execute EEMD on S 25 | eIMFs = eemd.eemd(S, t) 26 | nIMFs = eIMFs.shape[0] 27 | 28 | # Plot results 29 | plt.figure(figsize=(12, 9)) 30 | plt.subplot(nIMFs + 1, 1, 1) 31 | plt.plot(t, S, "r") 32 | 33 | for n in range(nIMFs): 34 | plt.subplot(nIMFs + 1, 1, n + 2) 35 | plt.plot(t, eIMFs[n], "g") 36 | plt.ylabel("eIMF %i" % (n + 1)) 37 | plt.locator_params(axis="y", nbins=5) 38 | 39 | plt.xlabel("Time [s]") 40 | plt.tight_layout() 41 | plt.savefig("eemd_example", dpi=120) 42 | plt.show() 43 | -------------------------------------------------------------------------------- /doc/ceemdan.rst: -------------------------------------------------------------------------------- 1 | CEEMDAN 2 | ======= 3 | 4 | Info 5 | ---- 6 | 7 | Complete ensemble EMD with adaptive noise (CEEMDAN) performs an EEMD with 8 | the difference that the information about the noise is shared among all workers. 9 | 10 | .. note:: 11 | **Parallel execution is enabled by default.** CEEMDAN automatically uses all available 12 | CPU cores for faster computation. See :doc:`speedup ` for details on 13 | controlling parallelization. 14 | 15 | .. note:: 16 | Given the nature of CEEMDAN, each time you decompose a signal you will obtain a different set of components. 17 | That's the expected consequence of adding noise which is going to be random and different. 18 | To make the decomposition reproducible, one needs to set a seed for the random number generator used in CEEMDAN 19 | **and** set ``parallel=False``. This is done using :func:`PyEMD.CEEMDAN.noise_seed` method on the instance:: 20 | 21 | ceemdan = CEEMDAN(parallel=False) 22 | ceemdan.noise_seed(12345) 23 | imfs = ceemdan(signal) 24 | 25 | Class 26 | ----- 27 | 28 | .. autoclass:: PyEMD.CEEMDAN 29 | :members: 30 | -------------------------------------------------------------------------------- /doc/eemd.rst: -------------------------------------------------------------------------------- 1 | EEMD 2 | ==== 3 | 4 | Info 5 | ---- 6 | 7 | Ensemble empirical mode decomposition (EEMD) creates an ensemble of worker each 8 | of which performs an :doc:`EMD ` on a copy of the input signal with added noise. 9 | When all workers finish their work a mean over all workers is considered as 10 | the true result. 11 | 12 | .. note:: 13 | **Parallel execution is enabled by default.** EEMD automatically uses all available 14 | CPU cores for faster computation. See :doc:`speedup ` for details on 15 | controlling parallelization. 16 | 17 | .. note:: 18 | Given the nature of EEMD, each time you decompose a signal you will obtain a different set of components. 19 | That's the expected consequence of adding noise which is going to be random. 20 | To make the decomposition reproducible, one needs to set a seed for the random number generator used in EEMD 21 | **and** set ``parallel=False``. This is done using :func:`PyEMD.EEMD.noise_seed` method on the instance:: 22 | 23 | eemd = EEMD(parallel=False) 24 | eemd.noise_seed(12345) 25 | imfs = eemd(signal) 26 | 27 | Class 28 | ----- 29 | 30 | .. autoclass:: PyEMD.EEMD 31 | :members: 32 | -------------------------------------------------------------------------------- /example/image_example.py: -------------------------------------------------------------------------------- 1 | # Author: Dawid Laszuk 2 | # Last update: 7/07/2017 3 | import numpy as np 4 | import pylab as plt 5 | from PyEMD import EMD2D 6 | 7 | # Generate image 8 | print("Generating image... ", end="") 9 | rows, cols = 1024, 1024 10 | row_scale, col_scale = 256, 256 11 | x = np.arange(rows) / float(row_scale) 12 | y = np.arange(cols).reshape((-1, 1)) / float(col_scale) 13 | 14 | pi2 = 2 * np.pi 15 | img = np.zeros((rows, cols)) 16 | img = img + np.sin(2 * pi2 * x) * np.cos(y * 4 * pi2 + 4 * x * pi2) 17 | img = img + 3 * np.sin(2 * pi2 * x) + 2 18 | img = img + 5 * x * y + 2 * (y - 0.2) * y 19 | print("Done") 20 | 21 | # Perform decomposition 22 | print("Performing decomposition... ", end="") 23 | emd2d = EMD2D() 24 | # emd2d.FIXE_H = 5 25 | IMFs = emd2d.emd(img, max_imf=4) 26 | imfNo = IMFs.shape[0] 27 | print("Done") 28 | 29 | print("Plotting results... ", end="") 30 | 31 | # Save image for preview 32 | plt.figure(figsize=(4, 4 * (imfNo + 1))) 33 | plt.subplot(imfNo + 1, 1, 1) 34 | plt.imshow(img) 35 | plt.colorbar() 36 | plt.title("Input image") 37 | 38 | # Save reconstruction 39 | for n, imf in enumerate(IMFs): 40 | plt.subplot(imfNo + 1, 1, n + 2) 41 | plt.imshow(imf) 42 | plt.colorbar() 43 | plt.title("IMF %i" % (n + 1)) 44 | 45 | plt.savefig("image_decomp") 46 | print("Done") 47 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | import nox 2 | 3 | # Use uv for faster dependency installation and Python version management 4 | nox.options.default_venv_backend = "uv" 5 | 6 | # Test against all currently supported Python versions (3.10+) 7 | PYTHON_VERSIONS = ["3.9", "3.10", "3.11", "3.12", "3.13"] 8 | 9 | 10 | @nox.session(python=PYTHON_VERSIONS) 11 | def tests(session: nox.Session) -> None: 12 | """Run the test suite across Python versions.""" 13 | session.install(".[test]") 14 | session.run("python", "-m", "PyEMD.tests.test_all") 15 | 16 | 17 | @nox.session(python=PYTHON_VERSIONS) 18 | def tests_pytest(session: nox.Session) -> None: 19 | """Run the test suite with pytest across Python versions.""" 20 | session.install(".[test]") 21 | session.run("pytest", "PyEMD/tests/", "-v") 22 | 23 | 24 | @nox.session(python=PYTHON_VERSIONS[-1]) # Latest Python only 25 | def lint(session: nox.Session) -> None: 26 | """Run linting checks.""" 27 | session.install(".[dev]") 28 | session.run("isort", "--check", "PyEMD") 29 | session.run("black", "--check", "PyEMD", "doc", "example") 30 | 31 | 32 | @nox.session(python=PYTHON_VERSIONS[-1]) # Latest Python only 33 | def typecheck(session: nox.Session) -> None: 34 | """Run type checking (if mypy is added later).""" 35 | session.install(".", "mypy") 36 | session.run("mypy", "PyEMD", "--ignore-missing-imports") 37 | -------------------------------------------------------------------------------- /PyEMD/tests/test_emd_matlab.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | from PyEMD.EMD_matlab import EMD 6 | 7 | 8 | class EMDMatlabTest(unittest.TestCase): 9 | @staticmethod 10 | def test_default_call_EMD(): 11 | T = np.arange(0, 1, 0.01) 12 | S = np.cos(2 * T * 2 * np.pi) 13 | max_imf = 2 14 | 15 | emd = EMD() 16 | emd.emd(emd, S, T, max_imf) 17 | 18 | def test_different_length_input(self): 19 | T = np.arange(20) 20 | S = np.random.random(len(T) + 7) 21 | 22 | emd = EMD() 23 | with self.assertRaises(ValueError): 24 | emd.emd(emd, S, T) 25 | 26 | def test_trend(self): 27 | """ 28 | Input is trend. Expeting no shifting process. 29 | """ 30 | emd = EMD() 31 | 32 | T = np.arange(0, 1, 0.01) 33 | S = np.cos(2 * T * 2 * np.pi) 34 | 35 | # Input - linear function f(t) = 2*t 36 | output = emd.emd(emd, S, T) 37 | self.assertEqual(len(output), 4, "Expecting 4 outputs - IMF, EXT, ITER, imfNo") 38 | 39 | IMF, EXT, ITER, imfNo = output 40 | self.assertEqual(len(IMF), 2, "Expecting single IMF + residue") 41 | self.assertEqual(len(IMF[0]), len(S), "Expecting single IMF") 42 | self.assertTrue(np.allclose(S, IMF[0])) 43 | self.assertLessEqual(ITER[0], 5, "Expecting 5 iterations at most") 44 | self.assertEqual(imfNo, 2, "Expecting 1 IMF") 45 | self.assertEqual(EXT[0], 3, "Expecting single EXT") 46 | 47 | 48 | if __name__ == "__main__": 49 | unittest.main() 50 | -------------------------------------------------------------------------------- /example/emd_comparison.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows very high numbers for JIT because on import the JitEMD is compiled 3 | making it very efficient. It takes a good 20+ seconds to compile which is hidden. 4 | 5 | For this reason, JitEMD is worthy when iterating on live notebook or performing 6 | a lot of the same computation within a single execution. For a single script 7 | with a single EMD execution, it's still much more performant to use normal EMD. 8 | """ 9 | 10 | import time 11 | import numpy as np 12 | 13 | from PyEMD import EEMD, EMD 14 | from PyEMD.experimental.jitemd import get_timeline 15 | from PyEMD.experimental.jitemd import JitEMD 16 | 17 | s = np.random.random(2000) 18 | t = get_timeline(len(s), s.dtype) 19 | n_repeat = 20 20 | 21 | print( 22 | f"""Comparing EEMD execution on a larger signal with classic and JIT EMDs. 23 | Signal is random (uniform) noise of length: {len(s)}. The test is done by executing 24 | EEMD with either classic or JIT EMD {n_repeat} times and taking the average. Such 25 | setup favouries JitEMD which is compiled once and then reused {n_repeat-1} times. 26 | Compiltion is quite costly.""" 27 | ) 28 | 29 | time_0 = time.time() 30 | emd = EMD() 31 | eemd = EEMD(ext_EMD=emd) 32 | for _ in range(n_repeat): 33 | _ = eemd(s, t) 34 | time_1 = time.time() 35 | t_per_one = (time_1 - time_0) / n_repeat 36 | print(f"Classic EEMD on {len(s)} length random signal: {t_per_one:5.2} s per EEMD run") 37 | 38 | time_0 = time.time() 39 | emd = JitEMD() 40 | eemd = EEMD(ext_EMD=emd) 41 | for _ in range(n_repeat): 42 | _ = eemd(s, t) 43 | time_1 = time.time() 44 | t_per_one = (time_1 - time_0) / n_repeat 45 | print(f"JitEMD EEMD on {len(s)} length random signal: {t_per_one:5.2} per EEMD run") 46 | -------------------------------------------------------------------------------- /doc/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ============= 3 | 4 | Typical Usage 5 | ------------- 6 | 7 | Majority, if not all, methods follow the same usage pattern: 8 | 9 | * Import method 10 | * Initiate method 11 | * Apply method on data 12 | 13 | On vanilla EMD this is as 14 | 15 | .. code-block:: python 16 | 17 | from PyEMD import EMD 18 | emd = EMD() 19 | imfs = emd(s) 20 | 21 | Parameters 22 | ---------- 23 | 24 | The decomposition can be changed by adjusting parameters related to either sifting or stopping conditions. 25 | 26 | Sifting 27 | ``````` 28 | The sifting depends on the used method so these parameters ought to be looked within the methods. 29 | However, the typical parameters relate to spline method or the number of mirroring points. 30 | 31 | 32 | Stopping conditions 33 | ``````````````````` 34 | All methods have the same two conditions, `FIXE` and `FIXE_H`, for stopping which relate to the number of sifting iterations. 35 | Setting parameter `FIXE` to any positive value will fix the number of iterations for each IMF to be exactly `FIXE`. 36 | 37 | Example: 38 | 39 | .. code-block:: python 40 | 41 | emd = EMD() 42 | emd.FIXE = 10 43 | imfs = emd(s) 44 | 45 | Parameter `FIXE_H` relates to the number of iterations when the proto-IMF signal fulfils IMF conditions, i.e. number of extrema and zero-crossings differ at most by one and the mean is close to zero. This means that there will be at least `FIXE_H` iteration per IMF. 46 | 47 | Example: 48 | 49 | .. code-block:: python 50 | 51 | emd = EMD() 52 | emd.FIXE_H = 5 53 | imfs = emd(s) 54 | 55 | When both `FIXE` and `FIXE_H` are 0 then other conditions are checked. These can be checking for convergence between consecutive iterations or whether the amplitude of output is below acceptable range. 56 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "EMD-signal" 7 | dynamic = ["version"] 8 | description = "Implementation of the Empirical Mode Decomposition (EMD) and its variations" 9 | readme = "README.md" 10 | license = "Apache-2.0" 11 | authors = [{ name = "Dawid Laszuk", email = "pyemd@dawid.lasz.uk" }] 12 | keywords = ["signal", "decomposition", "data", "analysis"] 13 | classifiers = [ 14 | "Intended Audience :: Information Technology", 15 | "Intended Audience :: Science/Research", 16 | "Topic :: Scientific/Engineering", 17 | "Topic :: Scientific/Engineering :: Information Analysis", 18 | "Topic :: Scientific/Engineering :: Mathematics", 19 | ] 20 | requires-python = ">=3.9" 21 | dependencies = [ 22 | "numpy>=1.12", 23 | "scipy>=0.19", 24 | "pathos>=0.2.1", 25 | "tqdm>=4.64.0,<5.0", 26 | ] 27 | 28 | [project.optional-dependencies] 29 | doc = [ 30 | "sphinx", 31 | "sphinx_rtd_theme", 32 | "numpydoc", 33 | ] 34 | jit = [ 35 | "numba>=0.62.0; python_version>='3.10'", 36 | ] 37 | dev = [ 38 | "pycodestyle==2.11.*", 39 | "black==25.11.*", 40 | "isort==5.12.*", 41 | "nox", 42 | ] 43 | test = [ 44 | "pytest", 45 | "codecov", 46 | ] 47 | 48 | [project.urls] 49 | Homepage = "https://github.com/laszukdawid/PyEMD" 50 | Repository = "https://github.com/laszukdawid/PyEMD" 51 | 52 | [tool.setuptools.dynamic] 53 | version = { attr = "PyEMD.__version__" } 54 | 55 | [tool.setuptools.packages.find] 56 | include = ["PyEMD*"] 57 | 58 | [tool.black] 59 | line-length = 120 60 | 61 | [tool.isort] 62 | line_length = 120 63 | profile = "black" 64 | 65 | [tool.ruff] 66 | line-length = 120 67 | 68 | [tool.pycodestyle] 69 | max-line-length = 120 70 | ignore = ["E203", "W503", "W605"] 71 | -------------------------------------------------------------------------------- /PyEMD/splines.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.interpolate import Akima1DInterpolator, CubicHermiteSpline, CubicSpline, PchipInterpolator 3 | 4 | 5 | def cubic_spline_3pts(x, y, T): 6 | """ 7 | Apparently scipy.interpolate.interp1d does not support 8 | cubic spline for less than 4 points. 9 | """ 10 | x0, x1, x2 = x 11 | y0, y1, y2 = y 12 | 13 | x1x0, x2x1 = x1 - x0, x2 - x1 14 | y1y0, y2y1 = y1 - y0, y2 - y1 15 | _x1x0, _x2x1 = 1.0 / x1x0, 1.0 / x2x1 16 | 17 | m11, m12, m13 = 2 * _x1x0, _x1x0, 0 18 | m21, m22, m23 = _x1x0, 2.0 * (_x1x0 + _x2x1), _x2x1 19 | m31, m32, m33 = 0, _x2x1, 2.0 * _x2x1 20 | 21 | v1 = 3 * y1y0 * _x1x0 * _x1x0 22 | v3 = 3 * y2y1 * _x2x1 * _x2x1 23 | v2 = v1 + v3 24 | 25 | M = np.array([[m11, m12, m13], [m21, m22, m23], [m31, m32, m33]]) 26 | v = np.array([v1, v2, v3]).T 27 | k = np.linalg.inv(M).dot(v) 28 | 29 | a1 = k[0] * x1x0 - y1y0 30 | b1 = -k[1] * x1x0 + y1y0 31 | a2 = k[1] * x2x1 - y2y1 32 | b2 = -k[2] * x2x1 + y2y1 33 | 34 | t = T[np.r_[T >= x0] & np.r_[T <= x2]] 35 | t1 = (T[np.r_[T >= x0] & np.r_[T < x1]] - x0) / x1x0 36 | t2 = (T[np.r_[T >= x1] & np.r_[T <= x2]] - x1) / x2x1 37 | t11, t22 = 1.0 - t1, 1.0 - t2 38 | 39 | q1 = t11 * y0 + t1 * y1 + t1 * t11 * (a1 * t11 + b1 * t1) 40 | q2 = t22 * y1 + t2 * y2 + t2 * t22 * (a2 * t22 + b2 * t2) 41 | q = np.append(q1, q2) 42 | 43 | return t, q 44 | 45 | 46 | def akima(X, Y, x): 47 | spl = Akima1DInterpolator(X, Y) 48 | return spl(x) 49 | 50 | 51 | def cubic_hermite(X, Y, x): 52 | dydx = np.gradient(Y, X) 53 | spl = CubicHermiteSpline(X, Y, dydx) 54 | return spl(x) 55 | 56 | 57 | def cubic(X, Y, x): 58 | spl = CubicSpline(X, Y) 59 | return spl(x) 60 | 61 | 62 | def pchip(X, Y, x): 63 | spl = PchipInterpolator(X, Y) 64 | return spl(x) 65 | -------------------------------------------------------------------------------- /example/hht_example.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pylab as plt 3 | from scipy.signal import hilbert 4 | 5 | from PyEMD import EMD 6 | 7 | 8 | def instant_phase(imfs): 9 | """Extract analytical signal through Hilbert Transform.""" 10 | analytic_signal = hilbert(imfs) # Apply Hilbert transform to each row 11 | phase = np.unwrap(np.angle(analytic_signal)) # Compute angle between img and real 12 | return phase 13 | 14 | 15 | # Define signal 16 | t = np.linspace(0, 1, 200) 17 | dt = t[1] - t[0] 18 | 19 | sin = lambda x, p: np.sin(2 * np.pi * x * t + p) 20 | S = 3 * sin(18, 0.2) * (t - 0.2) ** 2 21 | S += 5 * sin(11, 2.7) 22 | S += 3 * sin(14, 1.6) 23 | S += 1 * np.sin(4 * 2 * np.pi * (t - 0.8) ** 2) 24 | S += t**2.1 - t 25 | 26 | # Compute IMFs with EMD 27 | emd = EMD() 28 | imfs = emd(S, t) 29 | 30 | # Extract instantaneous phases and frequencies using Hilbert transform 31 | instant_phases = instant_phase(imfs) 32 | instant_freqs = np.diff(instant_phases) / (2 * np.pi * dt) 33 | 34 | # Create a figure consisting of 3 panels which from the top are the input signal, IMFs and instantaneous frequencies 35 | fig, axes = plt.subplots(3, figsize=(12, 12)) 36 | 37 | # The top panel shows the input signal 38 | ax = axes[0] 39 | ax.plot(t, S) 40 | ax.set_ylabel("Amplitude [arb. u.]") 41 | ax.set_title("Input signal") 42 | 43 | # The middle panel shows all IMFs 44 | ax = axes[1] 45 | for num, imf in enumerate(imfs): 46 | ax.plot(t, imf, label="IMF %s" % (num + 1)) 47 | 48 | # Label the figure 49 | ax.legend() 50 | ax.set_ylabel("Amplitude [arb. u.]") 51 | ax.set_title("IMFs") 52 | 53 | # The bottom panel shows all instantaneous frequencies 54 | ax = axes[2] 55 | for num, instant_freq in enumerate(instant_freqs): 56 | ax.plot(t[:-1], instant_freq, label="IMF %s" % (num + 1)) 57 | 58 | # Label the figure 59 | ax.legend() 60 | ax.set_xlabel("Time [s]") 61 | ax.set_ylabel("Inst. Freq. [Hz]") 62 | ax.set_title("Huang-Hilbert Transform") 63 | 64 | plt.tight_layout() 65 | plt.savefig("hht_example", dpi=120) 66 | plt.show() 67 | -------------------------------------------------------------------------------- /PyEMD/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | from PyEMD.utils import deduce_common_type, get_timeline, unify_types 6 | 7 | 8 | class MyTestCase(unittest.TestCase): 9 | def test_get_timeline_default_dtype(self): 10 | S = np.random.random(100) 11 | T = get_timeline(len(S)) 12 | 13 | self.assertEqual(len(T), len(S), "Lengths must be equal") 14 | self.assertEqual(T.dtype, np.int64, "Default dtype is np.int64") 15 | self.assertEqual(T[-1], len(S) - 1, "Range is kept") 16 | 17 | def test_get_timeline_signal_dtype(self): 18 | S = np.random.random(100) 19 | T = get_timeline(len(S), dtype=S.dtype) 20 | 21 | self.assertEqual(len(T), len(S), "Lengths must be equal") 22 | self.assertEqual(T.dtype, S.dtype, "Dtypes must be equal") 23 | self.assertEqual(T[-1], len(S) - 1, "Range is kept") 24 | 25 | def test_get_timeline_does_not_overflow_int16(self): 26 | S = np.random.randint(100, size=(np.iinfo(np.int16).max + 10,), dtype=np.int16) 27 | T = get_timeline(len(S), dtype=S.dtype) 28 | 29 | self.assertGreater(len(S), np.iinfo(S.dtype).max, "Length of the signal is greater than its type max value") 30 | self.assertEqual(len(T), len(S), "Lengths must be equal") 31 | self.assertEqual(T[-1], len(S) - 1, "Range is kept") 32 | self.assertEqual(T.dtype, np.uint16, "UInt16 is the min type that matches requirements") 33 | 34 | def test_deduce_common_types(self): 35 | self.assertEqual(deduce_common_type(np.int16, np.int32), np.int32) 36 | self.assertEqual(deduce_common_type(np.int32, np.int16), np.int32) 37 | self.assertEqual(deduce_common_type(np.int32, np.int32), np.int32) 38 | self.assertEqual(deduce_common_type(np.float32, np.float64), np.float64) 39 | 40 | def test_unify_types(self): 41 | x = np.array([1, 2, 3], dtype=np.int16) 42 | y = np.array([1.1, 2.2, 3.3], dtype=np.float32) 43 | x, y = unify_types(x, y) 44 | self.assertEqual(x.dtype, np.float32) 45 | self.assertEqual(y.dtype, np.float32) 46 | 47 | 48 | if __name__ == "__main__": 49 | unittest.main() 50 | -------------------------------------------------------------------------------- /PyEMD/tests/test_compact.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Coding: UTF-8 3 | 4 | import unittest 5 | 6 | import numpy as np 7 | import scipy as sp 8 | 9 | from PyEMD.compact import * 10 | 11 | 12 | class CompactTest(unittest.TestCase): 13 | @staticmethod 14 | def create_signal(): 15 | # Do NOT modify this function! If you do, the following can happen: 16 | # -- Possible test errors for filter (tolerace of np.allclose). 17 | # -- Error in the derivative test (analytical derivative is hardcoded) 18 | t = np.linspace(0.0, np.pi, 200) 19 | return 0.1 * np.cos(2.0 * np.pi * t) 20 | 21 | def test_TDMA(self): 22 | diags = np.array([0.5 * np.ones(10), 1.0 * np.ones(10), 0.5 * np.ones(10)]) 23 | positions = [-1, 0, 1] 24 | tridiag = sp.sparse.spdiags(diags, positions, 10, 10).todense() 25 | 26 | # change some diagonal values to make sure it is working 27 | diags[0][3] = 2.0 28 | diags[1][1] = 2.0 29 | tridiag[3, 2] = 2.0 30 | tridiag[1, 1] = 2.0 31 | 32 | rhs = np.arange(10) 33 | 34 | answer = np.linalg.solve(tridiag, rhs) 35 | # result = TDMAsolver(*diags, rhs) 36 | result = TDMAsolver(diags[0], diags[1], diags[2], rhs) 37 | 38 | self.assertTrue(np.allclose(answer, result)) 39 | 40 | def test_filter_off(self): 41 | S = self.create_signal() 42 | self.assertTrue(np.allclose(S, filt6(S, 0.5), atol=1e-5)) 43 | 44 | def test_filter_small(self): 45 | S = self.create_signal() 46 | self.assertTrue(np.allclose(S, filt6(S, 0.45), atol=1e-5)) 47 | 48 | def test_filter_medium(self): 49 | S = self.create_signal() 50 | self.assertTrue(np.allclose(S, filt6(S, 0.0), atol=1e-5)) 51 | 52 | def test_filter_high(self): 53 | S = self.create_signal() 54 | self.assertTrue(np.allclose(S, filt6(S, -0.5), atol=1e-5)) 55 | 56 | def test_pade6(self): 57 | t = np.linspace(0.0, np.pi, 200) 58 | S = self.create_signal() 59 | Sprime = -0.1 * 2.0 * np.pi * np.sin(2.0 * np.pi * t) 60 | self.assertTrue(np.allclose(Sprime, pade6(S, t[1] - t[0]), atol=1e-5)) 61 | 62 | 63 | if __name__ == "__main__": 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /PyEMD/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import Optional, Tuple 3 | 4 | import numpy as np 5 | 6 | if sys.version_info >= (3, 9): 7 | from functools import cache 8 | else: 9 | from functools import lru_cache as cache 10 | 11 | 12 | def get_timeline(range_max: int, dtype: Optional[np.dtype] = None) -> np.ndarray: 13 | """Returns timeline array for requirements. 14 | 15 | Parameters 16 | ---------- 17 | range_max : int 18 | Largest value in range. Assume `range(range_max)`. Commonly that's length of the signal. 19 | dtype : np.dtype 20 | Minimal definition type. Returned timeline will have dtype that's the same or with higher byte size. 21 | 22 | """ 23 | timeline = np.arange(0, range_max, dtype=dtype) 24 | if timeline[-1] != range_max - 1: 25 | inclusive_dtype = smallest_inclusive_dtype(timeline.dtype, range_max) 26 | timeline = np.arange(0, range_max, dtype=inclusive_dtype) 27 | return timeline 28 | 29 | 30 | def smallest_inclusive_dtype(ref_dtype: np.dtype, ref_value) -> np.dtype: 31 | """Returns a numpy dtype with the same base as reference dtype (ref_dtype) 32 | but with the range that includes reference value (ref_value). 33 | 34 | Parameters 35 | ---------- 36 | ref_dtype : dtype 37 | Reference dtype. Used to select the base, i.e. int or float, for returned type. 38 | ref_value : value 39 | A value which needs to be included in returned dtype. Value will be typically int or float. 40 | 41 | """ 42 | # Integer path 43 | if np.issubdtype(ref_dtype, np.integer): 44 | for dtype in [np.uint16, np.uint32, np.uint64]: 45 | if ref_value < np.iinfo(dtype).max: 46 | return dtype 47 | max_val = np.iinfo(np.uint64).max 48 | raise ValueError("Requested too large integer range. Exceeds max( uint64 ) == '{}.".format(max_val)) 49 | 50 | # Float path 51 | if np.issubdtype(ref_dtype, np.floating): 52 | for dtype in [np.float16, np.float32, np.float64]: 53 | if ref_value < np.finfo(dtype).max: 54 | return dtype 55 | max_val = np.finfo(np.float64).max 56 | raise ValueError("Requested too large integer range. Exceeds max( float64 ) == '{}.".format(max_val)) 57 | 58 | raise ValueError("Unsupported dtype '{}'. Only intX and floatX are supported.".format(ref_dtype)) 59 | 60 | 61 | @cache 62 | def deduce_common_type(xtype: np.dtype, ytype: np.dtype) -> np.dtype: 63 | if xtype == ytype: 64 | return xtype 65 | if np.version.version[0] == "1": 66 | dtype = np.find_common_type([xtype, ytype], []) 67 | else: 68 | dtype = np.promote_types(xtype, ytype) 69 | return dtype 70 | 71 | 72 | def unify_types(x: np.ndarray, y: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: 73 | dtype = deduce_common_type(x.dtype, y.dtype) 74 | if x.dtype != dtype: 75 | x = x.astype(dtype) 76 | if y.dtype != dtype: 77 | y = y.astype(dtype) 78 | 79 | return x, y 80 | -------------------------------------------------------------------------------- /doc/examples.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ******** 3 | 4 | Some examples can be found in PyEMD/example directory. 5 | 6 | EMD 7 | === 8 | 9 | Quick start 10 | ----------- 11 | In most cases default settings are enough. Simply 12 | import :py:class:`EMD` and pass your signal to `emd` method. 13 | 14 | .. code:: python 15 | 16 | from PyEMD import EMD 17 | 18 | s = np.random.random(100) 19 | emd = EMD() 20 | IMFs = emd.emd(s) 21 | 22 | Something more 23 | `````````````` 24 | Here is a complete script on how to create and plot results. 25 | 26 | .. code:: python 27 | 28 | from PyEMD import EMD 29 | import numpy as np 30 | import pylab as plt 31 | 32 | # Define signal 33 | t = np.linspace(0, 1, 200) 34 | s = np.cos(11*2*np.pi*t*t) + 6*t*t 35 | 36 | # Execute EMD on signal 37 | IMF = EMD().emd(s,t) 38 | N = IMF.shape[0]+1 39 | 40 | # Plot results 41 | plt.subplot(N,1,1) 42 | plt.plot(t, s, 'r') 43 | plt.title("Input signal: $S(t)=cos(22\pi t^2) + 6t^2$") 44 | plt.xlabel("Time [s]") 45 | 46 | for n, imf in enumerate(IMF): 47 | plt.subplot(N,1,n+2) 48 | plt.plot(t, imf, 'g') 49 | plt.title("IMF "+str(n+1)) 50 | plt.xlabel("Time [s]") 51 | 52 | plt.tight_layout() 53 | plt.savefig('simple_example') 54 | plt.show() 55 | 56 | 57 | The Figure below was produced with input: 58 | 59 | :math:`S(t) = cos(22 \pi t^2) + 6t^2` 60 | 61 | |simpleExample| 62 | 63 | EEMD 64 | ==== 65 | 66 | Simplest case of using Ensemble EMD (EEMD) is by importing `EEMD` and passing your signal to `eemd` method. 67 | 68 | .. code:: python 69 | 70 | from PyEMD import EEMD 71 | import numpy as np 72 | import pylab as plt 73 | 74 | # Define signal 75 | t = np.linspace(0, 1, 200) 76 | 77 | sin = lambda x,p: np.sin(2*np.pi*x*t+p) 78 | S = 3*sin(18,0.2)*(t-0.2)**2 79 | S += 5*sin(11,2.7) 80 | S += 3*sin(14,1.6) 81 | S += 1*np.sin(4*2*np.pi*(t-0.8)**2) 82 | S += t**2.1 -t 83 | 84 | # Assign EEMD to `eemd` variable 85 | eemd = EEMD() 86 | 87 | # Say we want detect extrema using parabolic method 88 | emd = eemd.EMD 89 | emd.extrema_detection="parabol" 90 | 91 | # Execute EEMD on S 92 | eIMFs = eemd.eemd(S, t) 93 | nIMFs = eIMFs.shape[0] 94 | 95 | # Plot results 96 | plt.figure(figsize=(12,9)) 97 | plt.subplot(nIMFs+1, 1, 1) 98 | plt.plot(t, S, 'r') 99 | 100 | for n in range(nIMFs): 101 | plt.subplot(nIMFs+1, 1, n+2) 102 | plt.plot(t, eIMFs[n], 'g') 103 | plt.ylabel("eIMF %i" %(n+1)) 104 | plt.locator_params(axis='y', nbins=5) 105 | 106 | plt.xlabel("Time [s]") 107 | plt.tight_layout() 108 | plt.savefig('eemd_example', dpi=120) 109 | plt.show() 110 | 111 | |eemdExample| 112 | 113 | 114 | .. |simpleExample| image:: https://github.com/laszukdawid/PyEMD/raw/master/example/simple_example.png 115 | :align: middle 116 | :alt: Oh, the quality. Please click on the image for better resolution. 117 | :target: https://github.com/laszukdawid/PyEMD/raw/master/example/simple_example.png 118 | 119 | .. |eemdExample| image:: https://github.com/laszukdawid/PyEMD/raw/master/example/eemd_example.png?raw=true 120 | :width: 720px 121 | :height: 540px 122 | -------------------------------------------------------------------------------- /doc/intro.rst: -------------------------------------------------------------------------------- 1 | Intro 2 | ===== 3 | 4 | General 5 | ------- 6 | 7 | **PyEMD** is a Python implementation of `Empirical Mode Decomposition (EMD) `_ and its variations. 8 | One of the most popular expansion is `Ensemble Empirical Mode Decomposition (EEMD) `_, which utilises an ensemble of noise-assisted executions. 9 | 10 | As the name suggests, methods in this package take data (signal) and decompose it into a set of component. 11 | All these methods theoretically should decompose a signal into the same set of components but in practise 12 | there are plenty of nuances and different ways to handle noise. Regardless of the method, obtained 13 | components are often called *Intrinsic Mode Functions* (IMF) to highlight that they contain an intrinsic (self) 14 | property which is a specific oscillation (mode). These are generic oscillations; their frequency and 15 | amplitude can change, however, they are distinct within analyzed signal. 16 | 17 | Installation 18 | ------------ 19 | 20 | Simplest (pip) 21 | `````````````` 22 | 23 | Using `pip` to install is the quickest way to try and play. The package has had plenty of time to mature 24 | and at this point there aren't that many changes, especially nothing breaking. In the end, the basic EMD 25 | is the same as it was published in 1998. 26 | 27 | The easiest way is to install `EMD-signal`_ from the PyPi, for example using 28 | 29 | $ pip install EMD-signal 30 | 31 | Once the package is installed it should be accessible in your Python as `PyEMD`, e.g. :: 32 | 33 | >>> from PyEMD import EMD 34 | 35 | Conda 36 | ````` 37 | 38 | If you are using `conda` and prefer to install packages from `conda-forge` channel, you can do so with :: 39 | 40 | $ conda install -c conda-forge emd-signal 41 | 42 | This will install the package and all dependencies from the `conda-forge` channel. 43 | 44 | Research (github) 45 | ````````````````` 46 | 47 | Do you want to see the code by yourself? Update it? Make it better? Or worse (no judgement)? 48 | Then you likely want to check out the package and install it manually. **Don't worry, installation is simple**. 49 | 50 | PyEMD is an open source project hosted on the GitHub on the main author's account, i.e. https://github.com/laszukdawid/PyEMD. 51 | This github page is where all changes are done first and where all `issues`_ should be reported. 52 | The page should have clear instructions on how to download the code. Currently that's a (only) green 53 | button and then following options. 54 | 55 | In case you like using command line and want a copy-paste line :: 56 | 57 | $ git clone https://github.com/laszukdawid/PyEMD 58 | 59 | 60 | Once the code is downloaded, enter package's directory and execute :: 61 | 62 | $ python -m pip install . 63 | 64 | Or you can do it at once with :: 65 | 66 | $ python -m pip install git+https://github.com/laszukdawid/PyEMD.git 67 | 68 | This will download all required dependencies and will install PyEMD in your environment. 69 | Once it's done do a sanity check with quick import and version print: :: 70 | 71 | $ python -c "import PyEMD; print(PyEMD.__version__)" 72 | 73 | It should print out some value concluding that you're good to go. In case of troubles, don't hesitate to submit 74 | an issue ticket via the link provided a bit earlier. 75 | 76 | .. _EMD-signal: https://pypi.org/project/EMD-signal/ 77 | .. _issues: https://github.com/laszukdawid/PyEMD/issues 78 | -------------------------------------------------------------------------------- /.github/workflows/ci-main-build-test-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - ci-test 8 | 9 | jobs: 10 | build-n-test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | python-version: 16 | - "3.10" 17 | - "3.11" 18 | - "3.12" 19 | - "3.13" 20 | - "3.14" 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Running Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | - name: Install test dependencies 29 | run: | 30 | pip install --only-binary=numpy,scipy numpy scipy 31 | pip install -e .[test] 32 | - name: Run tests 33 | run: | 34 | python -m PyEMD.tests.test_all 35 | 36 | version-updated: 37 | runs-on: ubuntu-latest 38 | needs: build-n-test 39 | outputs: 40 | pyemd_version: ${{ steps.version.outputs.pyemd_version }} 41 | steps: 42 | - uses: actions/checkout@v4 43 | - uses: dorny/paths-filter@v3 44 | id: version 45 | with: 46 | filters: | 47 | pyemd_version: 48 | - 'PyEMD/__init__.py' 49 | - name: Check if version is updated 50 | if: ${{ steps.version.outputs.pyemd_version == 'true'}} 51 | run: echo "PyEMD version is updated" 52 | - name: Check if version is not updated 53 | if: ${{ steps.version.outputs.pyemd_version == 'false'}} 54 | run: echo "PyEMD version is not updated" 55 | 56 | deploy: 57 | # Run 'deploy' job only if `PyEMD/__init__.py` is modified 58 | if: needs.version-updated.outputs.pyemd_version == 'true' 59 | needs: version-updated 60 | runs-on: ubuntu-latest 61 | steps: 62 | - uses: actions/checkout@v4 63 | - name: Set up Python 64 | uses: actions/setup-python@v5 65 | with: 66 | python-version: "3.x" 67 | - name: Install dependencies 68 | run: | 69 | python -m pip install --upgrade pip 70 | pip install build 71 | - name: Build package 72 | run: python -m build 73 | - name: Publish package 74 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 75 | with: 76 | user: __token__ 77 | password: ${{ secrets.EMD_PYPI_API_TOKEN }} 78 | 79 | create-release: 80 | # Create GitHub release when version is updated 81 | if: needs.version-updated.outputs.pyemd_version == 'true' 82 | needs: [version-updated, deploy] 83 | runs-on: ubuntu-latest 84 | permissions: 85 | contents: write 86 | steps: 87 | - uses: actions/checkout@v4 88 | - name: Get version 89 | id: get_version 90 | run: | 91 | VERSION=$(grep -oP '__version__ = "\K[^"]+' PyEMD/__init__.py) 92 | echo "version=$VERSION" >> $GITHUB_OUTPUT 93 | - name: Create GitHub Release 94 | uses: softprops/action-gh-release@v2 95 | with: 96 | tag_name: v${{ steps.get_version.outputs.version }} 97 | name: Release v${{ steps.get_version.outputs.version }} 98 | body: | 99 | ## PyEMD v${{ steps.get_version.outputs.version }} 100 | 101 | Install via pip: 102 | ```bash 103 | pip install EMD-signal==${{ steps.get_version.outputs.version }} 104 | ``` 105 | generate_release_notes: true 106 | -------------------------------------------------------------------------------- /PyEMD/tests/test_splines.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Coding: UTF-8 3 | 4 | import unittest 5 | 6 | import numpy as np 7 | 8 | from PyEMD import EMD 9 | from PyEMD.splines import * 10 | 11 | 12 | class IMFTest(unittest.TestCase): 13 | """ 14 | Since these tests depend heavily on NumPy & SciPy, 15 | make sure you have NumPy >= 1.12 and SciPy >= 0.19. 16 | """ 17 | 18 | def test_unsupporter_spline(self): 19 | emd = EMD() 20 | emd.spline_kind = "waterfall" 21 | 22 | S = np.random.random(20) 23 | 24 | with self.assertRaises(ValueError): 25 | emd.emd(S) 26 | 27 | def test_akima(self): 28 | dtype = np.float32 29 | 30 | emd = EMD() 31 | emd.spline_kind = "akima" 32 | emd.DTYPE = dtype 33 | 34 | # Test error: len(X)!=len(Y) 35 | with self.assertRaises(ValueError): 36 | akima(np.array([0]), np.array([1, 2]), np.array([0, 1, 2])) 37 | 38 | # Test error: any(dt) <= 0 39 | with self.assertRaises(ValueError): 40 | akima(np.array([1, 0, 2]), np.array([1, 2]), np.array([0, 1, 2])) 41 | with self.assertRaises(ValueError): 42 | akima(np.array([0, 0, 2]), np.array([1, 2]), np.array([0, 1, 1])) 43 | 44 | # Test for correct responses 45 | T = np.array([0, 1, 2, 3, 4], dtype) 46 | S = np.array([0, 1, -1, -1, 5], dtype) 47 | t = np.array([i / 2.0 for i in range(9)], dtype) 48 | 49 | _t, s = emd.spline_points(t, np.array((T, S))) 50 | s_true = np.array([S[0], 0.9125, S[1], 0.066666, S[2], -1.35416667, S[3], 1.0625, S[4]], dtype) 51 | 52 | self.assertTrue(np.allclose(s_true, s), "Comparing akima with true") 53 | 54 | s_np = akima(np.array(T), np.array(S), np.array(t)) 55 | self.assertTrue(np.allclose(s, s_np), "Shouldn't matter if with numpy") 56 | 57 | def test_cubic(self): 58 | dtype = np.float64 59 | 60 | emd = EMD() 61 | emd.spline_kind = "cubic" 62 | emd.DTYPE = dtype 63 | 64 | T = np.array([0, 1, 2, 3, 4], dtype=dtype) 65 | S = np.array([0, 1, -1, -1, 5], dtype=dtype) 66 | t = np.arange(9, dtype=dtype) / 2.0 67 | 68 | # TODO: Something weird with float32. 69 | # Seems to be SciPy problem. 70 | _t, s = emd.spline_points(t, np.array((T, S))) 71 | 72 | s_true = np.array([S[0], 1.203125, S[1], 0.046875, S[2], -1.515625, S[3], 1.015625, S[4]], dtype=dtype) 73 | self.assertTrue(np.allclose(s, s_true, atol=0.01), "Comparing cubic") 74 | 75 | T = T[:-2].copy() 76 | S = S[:-2].copy() 77 | t = np.arange(5, dtype=dtype) / 2.0 78 | 79 | _t, s3 = emd.spline_points(t, np.array((T, S))) 80 | 81 | s3_true = np.array([S[0], 0.78125, S[1], 0.28125, S[2]], dtype=dtype) 82 | self.assertTrue(np.allclose(s3, s3_true), "Compare cubic 3pts") 83 | 84 | def test_slinear(self): 85 | dtype = np.float64 86 | 87 | emd = EMD() 88 | emd.spline_kind = "slinear" 89 | emd.DTYPE = dtype 90 | 91 | T = np.array([0, 1, 2, 3, 4], dtype=dtype) 92 | S = np.array([0, 1, -1, -1, 5], dtype=dtype) 93 | t = np.arange(9, dtype=dtype) / 2.0 94 | 95 | _t, s = emd.spline_points(t, np.array((T, S))) 96 | 97 | s_true = np.array([S[0], 0.5, S[1], 0, S[2], -1, S[3], 2, S[4]], dtype=dtype) 98 | self.assertTrue(np.allclose(s, s_true), "Comparing SLinear") 99 | 100 | 101 | if __name__ == "__main__": 102 | unittest.main() 103 | -------------------------------------------------------------------------------- /example/example_spline_capabilities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Coding: UTF-8 3 | 4 | 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | 8 | from PyEMD import EMD 9 | from PyEMD.splines import * 10 | from PyEMD.utils import get_timeline 11 | 12 | 13 | def test_spline(X, T, s_kind): 14 | """ 15 | Test the fitting with the given spline. 16 | 17 | Parameters 18 | ---------- 19 | X : 1D numpy array 20 | the signal 21 | T : 1D numpy array 22 | Position or time array. It has the same length as X 23 | s_kind : string 24 | spline kind. can be one of the following splines: 25 | 'akima', 'cubic', 'pchip', 'cubic_hermite' 26 | 27 | Returns 28 | ------- 29 | max_env : 1D numpy array 30 | max spline envelope 31 | min_env : 1D numpy array 32 | min spline envelope 33 | eMax : numpy array 34 | max extrema of the signal 35 | eMin : numpy array 36 | min extrema of the signal 37 | """ 38 | 39 | emd = EMD() 40 | emd.spline_kind = s_kind 41 | max_env, min_env, eMax, eMin = emd.extract_max_min_spline(T, X) 42 | return max_env, min_env, eMax, eMin 43 | 44 | 45 | def test_akima(X, T, ax): 46 | """ 47 | test the fitting with akima spline. 48 | 49 | Parameters 50 | ---------- 51 | X : 1D numpy array 52 | the signal 53 | T : 1D numpy array 54 | Position or time array. It has the same length as X 55 | ax : matplotlib axis 56 | the axis used for plotting 57 | Returns 58 | ------- 59 | eMax : numpy array 60 | max extrema of the signal 61 | eMin : numpy array 62 | min extrema of the signal 63 | the plot of the spline envelope 64 | """ 65 | 66 | max_env, min_env, eMax, eMin = test_spline(X, T, "akima") 67 | 68 | ax.plot(max_env, label="max akima") 69 | ax.plot(min_env, label="min akima") 70 | return eMax, eMin 71 | 72 | 73 | def test_cubic(X, T, ax): 74 | """ 75 | test the fitting with cubic spline 76 | 77 | Parameters 78 | ---------- 79 | see 'test_akima' 80 | 81 | Returns 82 | ------- 83 | see 'test_akima' 84 | """ 85 | 86 | max_env, min_env, eMax, eMin = test_spline(X, T, "cubic") 87 | 88 | ax.plot(max_env, label="max cubic") 89 | ax.plot(min_env, label="min cubic") 90 | return eMax, eMin 91 | 92 | 93 | def test_pchip(X, T, ax): 94 | """ 95 | test the fitting with pchip spline 96 | 'Piecewise Cubic Hermite Interpolating Polynomial' 97 | 98 | Parameters 99 | ---------- 100 | see 'test_akima' 101 | 102 | Returns 103 | ------- 104 | see 'test_akima' 105 | """ 106 | 107 | max_env, min_env, eMax, eMin = test_spline(X, T, "pchip") 108 | 109 | ax.plot(max_env, label="max pchip") 110 | ax.plot(min_env, label="min pchip") 111 | return eMax, eMin 112 | 113 | 114 | def test_cubic_hermite(X, T, ax): 115 | """ 116 | test the fitting with cubic_hermite spline 117 | 118 | Parameters 119 | ---------- 120 | see 'test_akima' 121 | 122 | Returns 123 | ------- 124 | see 'test_akima' 125 | """ 126 | 127 | max_env, min_env, eMax, eMin = test_spline(X, T, "cubic_hermite") 128 | 129 | ax.plot(max_env, label="max cubic_hermite") 130 | ax.plot(min_env, label="min cubic_hermite") 131 | return eMax, eMin 132 | 133 | 134 | if __name__ == "__main__": 135 | 136 | X = np.random.normal(size=200) 137 | T = get_timeline(len(X), X.dtype) 138 | T = EMD._normalize_time(T) 139 | 140 | fig, ax = plt.subplots() 141 | ax.plot(X, "--", lw=2, c="k") 142 | emax_akima, emin_akima = test_akima(X, T, ax) 143 | emax_cubic, emin_cubic = test_cubic(X, T, ax) 144 | emax_pchip, emin_pchip = test_pchip(X, T, ax) 145 | emax_chermite, emin_chermite = test_cubic_hermite(X, T, ax) 146 | ax.plot(emax_akima[0], emax_akima[1], "--") 147 | ax.legend() 148 | plt.show() 149 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | LINT_TARGET_DIRS := PyEMD doc example 2 | 3 | .PHONY: init sync test clean doc format lint-check freeze nox nox-lint 4 | .PHONY: perf perf-quick perf-scaling perf-splines perf-extrema perf-eemd perf-ceemdan perf-complexity perf-sifting perf-compare perf-list perf/build 5 | 6 | # Development setup 7 | init: 8 | uv sync --all-extras 9 | @echo "Development environment ready. Run 'source .venv/bin/activate' to activate." 10 | 11 | sync: 12 | uv sync --all-extras 13 | 14 | # Testing 15 | test: 16 | uv run python -m PyEMD.tests.test_all 17 | 18 | test-pytest: 19 | uv run pytest PyEMD/tests/ 20 | 21 | # Multi-version testing with nox 22 | nox: 23 | uv run nox -s tests 24 | 25 | nox-lint: 26 | uv run nox -s lint 27 | 28 | nox-all: 29 | uv run nox 30 | 31 | # Code quality 32 | format: 33 | uv run black $(LINT_TARGET_DIRS) 34 | uv run isort PyEMD 35 | 36 | lint-check: 37 | uv run isort --check PyEMD 38 | uv run black --check $(LINT_TARGET_DIRS) 39 | 40 | # Documentation 41 | doc: 42 | cd doc && make html 43 | 44 | # Cleanup 45 | clean: 46 | find PyEMD -name __pycache__ -execdir rm -r {} + 47 | rm -rf .venv 48 | 49 | # Export requirements for pip users 50 | freeze: 51 | uv export --no-hashes --no-dev --no-emit-project -o requirements.txt 52 | uv export --no-hashes --only-dev --no-emit-project -o requirements-dev.txt 53 | @echo "Exported requirements.txt and requirements-dev.txt" 54 | 55 | # Performance tests 56 | # Results saved to perf_test/results// 57 | 58 | perf: 59 | @echo "Running full performance test suite..." 60 | uv run python perf_test/perf_test_comprehensive.py 61 | 62 | perf-quick: 63 | @echo "Running quick performance test suite..." 64 | uv run python perf_test/perf_test_comprehensive.py --quick 65 | 66 | perf-scaling: 67 | @echo "Running EMD scaling test..." 68 | uv run python perf_test/perf_test_comprehensive.py --test scaling 69 | 70 | perf-splines: 71 | @echo "Running spline comparison test..." 72 | uv run python perf_test/perf_test_comprehensive.py --test splines 73 | 74 | perf-extrema: 75 | @echo "Running extrema detection test..." 76 | uv run python perf_test/perf_test_comprehensive.py --test extrema 77 | 78 | perf-eemd: 79 | @echo "Running EEMD parallel scaling test..." 80 | uv run python perf_test/perf_test_comprehensive.py --test eemd 81 | 82 | perf-ceemdan: 83 | @echo "Running CEEMDAN performance test..." 84 | uv run python perf_test/perf_test_comprehensive.py --test ceemdan 85 | 86 | perf-complexity: 87 | @echo "Running signal complexity test..." 88 | uv run python perf_test/perf_test_comprehensive.py --test complexity 89 | 90 | perf-sifting: 91 | @echo "Running sifting parameters test..." 92 | uv run python perf_test/perf_test_comprehensive.py --test sifting 93 | 94 | perf-compare: 95 | @if [ $$(ls -d perf_test/results/*/ 2>/dev/null | wc -l) -lt 2 ]; then \ 96 | echo "Error: Need at least 2 result directories to compare"; \ 97 | exit 1; \ 98 | fi 99 | @BASELINE=$$(ls -dt perf_test/results/*/ | sed -n '2p' | sed 's/\/$$//'); \ 100 | COMPARISON=$$(ls -dt perf_test/results/*/ | sed -n '1p' | sed 's/\/$$//'); \ 101 | echo "Comparing:"; \ 102 | echo " Baseline: $$BASELINE"; \ 103 | echo " Comparison: $$COMPARISON"; \ 104 | echo ""; \ 105 | uv run python perf_test/compare_results.py "$$BASELINE" "$$COMPARISON" 106 | 107 | perf-list: 108 | @echo "Available performance test targets:" 109 | @echo " make perf - Full test suite" 110 | @echo " make perf-quick - Quick test suite (smaller parameters)" 111 | @echo " make perf-scaling - EMD scaling with signal length" 112 | @echo " make perf-splines - Spline method comparison" 113 | @echo " make perf-extrema - Extrema detection comparison" 114 | @echo " make perf-eemd - EEMD parallel scaling" 115 | @echo " make perf-ceemdan - CEEMDAN performance" 116 | @echo " make perf-complexity - Signal complexity impact" 117 | @echo " make perf-sifting - Sifting parameters impact" 118 | @echo "" 119 | @echo "Comparison:" 120 | @echo " make perf-compare - Compare two most recent results" 121 | @echo " uv run python perf_test/compare_results.py " 122 | @echo "" 123 | @echo "Results saved to: perf_test/results/_/" 124 | 125 | perf/build: 126 | docker build -t pyemd-perf -f perf_test/Dockerfile . 127 | -------------------------------------------------------------------------------- /PyEMD/tests/test_visualization.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | from PyEMD import EMD 6 | from PyEMD.visualisation import Visualisation 7 | 8 | 9 | class VisTest(unittest.TestCase): 10 | def test_instantiation(self): 11 | emd = EMD() 12 | with self.assertRaises(ValueError): 13 | Visualisation(emd) 14 | 15 | def test_instantiation2(self): 16 | t = np.linspace(0, 1, 50) 17 | S = t + np.cos(np.cos(4.0 * t**2)) 18 | emd = EMD() 19 | emd.emd(S, t) 20 | imfs, res = emd.get_imfs_and_residue() 21 | vis = Visualisation(emd) 22 | self.assertTrue(np.all(vis.imfs == imfs)) 23 | self.assertTrue(np.all(vis.residue == res)) 24 | 25 | def test_check_imfs(self): 26 | vis = Visualisation() 27 | imfs = np.arange(50).reshape(2, 25) 28 | res = np.arange(25) 29 | imfs, res = vis._check_imfs(imfs, res, False) 30 | self.assertEqual(len(imfs), 2) 31 | 32 | def test_check_imfs2(self): 33 | vis = Visualisation() 34 | with self.assertRaises(AttributeError): 35 | vis._check_imfs(None, None, False) 36 | 37 | def test_check_imfs3(self): 38 | vis = Visualisation() 39 | imfs = np.arange(50).reshape(2, 25) 40 | 41 | out_imfs, out_res = vis._check_imfs(imfs, None, False) 42 | 43 | self.assertTrue(np.all(imfs == out_imfs)) 44 | self.assertIsNone(out_res) 45 | 46 | def test_check_imfs4(self): 47 | vis = Visualisation() 48 | imfs = np.arange(50).reshape(2, 25) 49 | with self.assertRaises(AttributeError): 50 | vis._check_imfs(imfs, None, True) 51 | 52 | def test_check_imfs5(self): 53 | t = np.linspace(0, 1, 50) 54 | S = t + np.cos(np.cos(4.0 * t**2)) 55 | emd = EMD() 56 | emd.emd(S, t) 57 | imfs, res = emd.get_imfs_and_residue() 58 | vis = Visualisation(emd) 59 | imfs2, res2 = vis._check_imfs(imfs, res, False) 60 | self.assertTrue(np.all(imfs == imfs2)) 61 | self.assertTrue(np.all(res == res2)) 62 | 63 | def test_plot_imfs(self): 64 | vis = Visualisation() 65 | with self.assertRaises(AttributeError): 66 | vis.plot_imfs() 67 | 68 | # Does not work for Python 2.7 (TravisCI), even with Agg backend 69 | # def test_plot_imfs2(self): 70 | # t = np.linspace(0, 1, 50) 71 | # S = t + np.cos(np.cos(4.*t**2)) 72 | # emd = EMD() 73 | # emd.emd(S, t) 74 | # vis = Visualisation(emd) 75 | # vis.plot_imfs() 76 | 77 | def test_calc_instant_phase(self): 78 | sig = np.arange(10) 79 | vis = Visualisation() 80 | phase = vis._calc_inst_phase(sig, None) 81 | assert len(sig) == len(phase) 82 | 83 | def test_calc_instant_phase2(self): 84 | t = np.linspace(0, 1, 50) 85 | S = t + np.cos(np.cos(4.0 * t**2)) 86 | emd = EMD() 87 | imfs = emd.emd(S, t) 88 | vis = Visualisation() 89 | phase = vis._calc_inst_phase(imfs, 0.4) 90 | assert len(imfs) == len(phase) 91 | 92 | def test_calc_instant_phase3(self): 93 | t = np.linspace(0, 1, 50) 94 | S = t + np.cos(np.cos(4.0 * t**2)) 95 | emd = EMD() 96 | imfs = emd.emd(S, t) 97 | vis = Visualisation() 98 | with self.assertRaises(AssertionError): 99 | _ = vis._calc_inst_phase(imfs, 0.8) 100 | 101 | def test_calc_instant_freq_alphaNone(self): 102 | t = np.linspace(0, 1, 50) 103 | S = t + np.cos(np.cos(4.0 * t**2)) 104 | emd = EMD() 105 | imfs = emd.emd(S, t) 106 | vis = Visualisation() 107 | freqs = vis._calc_inst_freq(imfs, t, False, None) 108 | self.assertEqual(imfs.shape, freqs.shape) 109 | 110 | def test_calc_instant_freq(self): 111 | t = np.linspace(0, 1, 50) 112 | S = t + np.cos(np.cos(4.0 * t**2)) 113 | emd = EMD() 114 | imfs = emd.emd(S, t) 115 | vis = Visualisation() 116 | freqs = vis._calc_inst_freq(imfs, t, False, 0.4) 117 | self.assertEqual(imfs.shape, freqs.shape) 118 | 119 | def test_plot_instant_freq(self): 120 | vis = Visualisation() 121 | t = np.arange(20) 122 | with self.assertRaises(AttributeError): 123 | vis.plot_instant_freq(t) 124 | -------------------------------------------------------------------------------- /PyEMD/tests/test_bemd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Coding: UTF-8 3 | import unittest 4 | 5 | import numpy as np 6 | 7 | try: 8 | from PyEMD.BEMD import BEMD 9 | except (ImportError, ModuleNotFoundError): 10 | # Not supported until supported. 11 | pass 12 | 13 | 14 | @unittest.skip("Not supported until supported") 15 | class BEMDTest(unittest.TestCase): 16 | def setUp(self) -> None: 17 | self.bemd = BEMD() 18 | 19 | @staticmethod 20 | def _generate_image(r=64, c=64): 21 | return np.random.random((r, c)) 22 | 23 | @staticmethod 24 | def _generate_linear_image(r=16, c=16): 25 | rows = np.arange(r) 26 | return np.repeat(rows, c).reshape(r, c) 27 | 28 | @staticmethod 29 | def _generate_Gauss(x, y, pos, std, amp=1): 30 | x_s = x - pos[0] 31 | y_s = y - pos[1] 32 | x2 = x_s * x_s 33 | y2 = y_s * y_s 34 | exp = np.exp(-(x2 + y2) / (2 * std * std)) 35 | # exp[exp<1e-6] = 0 36 | scale = amp / np.linalg.norm(exp) 37 | return scale * exp 38 | 39 | @classmethod 40 | def _sin(cls, x_n=128, y_n=128, x_f=[1], y_f=[0], dx=0, dy=0): 41 | x = np.linspace(0, 1, x_n) - dx 42 | y = np.linspace(0, 1, y_n) - dy 43 | xv, yv = np.meshgrid(x, y) 44 | img = np.zeros(xv.shape) 45 | for f in x_f: 46 | img += np.sin(f * 2 * np.pi * xv) 47 | for f in y_f: 48 | img += np.cos(f * 2 * np.pi * yv) 49 | return 255 * (img - img.min()) / (img.max() - img.min()) 50 | 51 | def test_extract_maxima(self): 52 | image = self._sin(x_n=32, y_n=32, y_f=[1], dy=1) 53 | max_peak_x, max_peak_y = BEMD.extract_maxima_positions(image) 54 | self.assertEqual(max_peak_x.size, 6) # Clustering 55 | self.assertEqual(max_peak_y.size, 6) # Clustering 56 | 57 | def test_extract_minima(self): 58 | image = self._sin(x_n=64, y_n=64, y_f=[2]) 59 | min_peak_x, min_peak_y = BEMD.extract_minima_positions(image) 60 | self.assertEqual(min_peak_x.size, 16) # Clustering 61 | self.assertEqual(min_peak_y.size, 16) # Clustering 62 | 63 | def test_find_extrema(self): 64 | image = self._sin() 65 | min_peaks, max_peaks = BEMD.find_extrema_positions(image) 66 | self.assertTrue(isinstance(min_peaks, tuple)) 67 | self.assertTrue(isinstance(min_peaks[0], np.ndarray)) 68 | self.assertTrue(isinstance(max_peaks[1], np.ndarray)) 69 | 70 | def test_default_call_BEMD(self): 71 | x = np.arange(50) 72 | y = np.arange(50) 73 | xv, yv = np.meshgrid(x, y) 74 | img = self._generate_Gauss(xv, yv, (10, 20), 5) 75 | img += self._sin(x_n=x.size, y_n=y.size) 76 | 77 | max_imf = 2 78 | self.bemd(img, max_imf) 79 | 80 | def test_endCondition_perfectReconstruction(self): 81 | c1 = self._generate_image() 82 | c2 = self._generate_image() 83 | IMFs = np.stack((c1, c2)) 84 | org_img = np.sum(IMFs, axis=0) 85 | self.assertTrue(self.bemd.end_condition(org_img, IMFs)) 86 | 87 | def test_bemd_simpleIMF(self): 88 | image = self._sin(x_f=[3, 5], y_f=[2]) 89 | IMFs = self.bemd(image) 90 | 91 | # One of the reasons this algorithm isn't the preferred one 92 | self.assertTrue(IMFs.shape[0] == 7, "Depending on spline, there should be an IMF and possibly trend") 93 | 94 | def test_bemd_limitImfNo(self): 95 | # Create image 96 | rows, cols = 64, 64 97 | linear_background = 0.2 * self._generate_linear_image(rows, cols) 98 | 99 | # Sinusoidal IMF 100 | X = np.arange(cols)[None, :].T 101 | Y = np.arange(rows) 102 | x_comp_1d = np.sin(X * 0.3) + np.cos(X * 2.9) ** 2 103 | y_comp_1d = np.sin(Y * 0.2) 104 | comp_2d = 10 * x_comp_1d * y_comp_1d 105 | comp_2d = comp_2d 106 | 107 | image = linear_background + comp_2d 108 | 109 | # Limit number of IMFs 110 | max_imf = 2 111 | 112 | # decompose image 113 | IMFs = self.bemd(image, max_imf=max_imf) 114 | 115 | # It should have no more than 2 (max_imf) + residue 116 | self.assertEqual(IMFs.shape[0], 1 + max_imf) 117 | 118 | 119 | if __name__ == "__main__": 120 | unittest.main() 121 | -------------------------------------------------------------------------------- /PyEMD/compact.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def TDMAsolver(a, b, c, d): 5 | """Thomas algorithm to solve tridiagonal linear systems with 6 | non-periodic BC. 7 | 8 | | b0 c0 | | . | | . | 9 | | a1 b1 c1 | | . | | . | 10 | | a2 b2 c2 | | x | = | d | 11 | | .......... | | . | | . | 12 | | an bn cn | | . | | . | 13 | """ 14 | n = len(b) 15 | 16 | cp = np.zeros(n) 17 | cp[0] = c[0] / b[0] 18 | for i in range(1, n - 1): 19 | cp[i] = c[i] / (b[i] - a[i] * cp[i - 1]) 20 | 21 | dp = np.zeros(n) 22 | dp[0] = d[0] / b[0] 23 | for i in range(1, n): 24 | dp[i] = (d[i] - a[i] * dp[i - 1]) / (b[i] - a[i] * cp[i - 1]) 25 | 26 | x = np.zeros(n) 27 | x[-1] = dp[-1] 28 | for i in range(n - 2, -1, -1): 29 | x[i] = dp[i] - cp[i] * x[i + 1] 30 | 31 | return x 32 | 33 | 34 | def filt6(f, alpha): 35 | """ 36 | 6th Order compact filter (non-periodic BC). 37 | 38 | References: 39 | ----------- 40 | Lele, S. K. - Compact finite difference schemes with spectral-like 41 | resolution. Journal of Computational Physics 103 (1992) 16-42 42 | 43 | Visbal, M. R. and Gaitonde, D. V. - On the use of higher-order finite- 44 | difference schemes on curvilinear and deforming meshes. Journal of 45 | Computational Physics 181 (2002) 155-185 46 | """ 47 | Ca = (11.0 + 10.0 * alpha) / 16.0 48 | Cb = (15.0 + 34.0 * alpha) / 32.0 49 | Cc = (-3.0 + 6.0 * alpha) / 16.0 50 | Cd = (1.0 - 2.0 * alpha) / 32.0 51 | 52 | n = len(f) 53 | 54 | rhs = np.zeros(n) 55 | 56 | rhs[3:-3] = ( 57 | Cd * 0.5 * (f[6:] + f[:-6]) + Cc * 0.5 * (f[5:-1] + f[1:-5]) + Cb * 0.5 * (f[4:-2] + f[2:-4]) + Ca * f[3:-3] 58 | ) 59 | 60 | # Non-periodic BC: 61 | rhs[0] = (15.0 / 16.0) * f[0] + (4.0 * f[1] - 6.0 * f[2] + 4.0 * f[3] - f[4]) / 16.0 62 | 63 | rhs[1] = (3.0 / 4.0) * f[1] + (f[0] + 6.0 * f[2] - 4.0 * f[3] + f[4]) / 16.0 64 | 65 | rhs[2] = (5.0 / 8.0) * f[2] + (-f[0] + 4.0 * f[1] + 4.0 * f[3] - f[4]) / 16.0 66 | 67 | rhs[-1] = (15.0 / 16.0) * f[-1] + (4.0 * f[-2] - 6.0 * f[-3] + 4.0 * f[-4] - f[-5]) / 16.0 68 | 69 | rhs[-2] = (3.0 / 4.0) * f[-2] + (f[-1] + 6.0 * f[-3] - 4.0 * f[-4] + f[-5]) / 16.0 70 | 71 | rhs[-3] = (5.0 / 8.0) * f[-3] + (-f[-1] + 4.0 * f[-2] + 4.0 * f[-4] - f[-5]) / 16.0 72 | 73 | Da = alpha * np.ones(n) 74 | Db = np.ones(n) 75 | Dc = alpha * np.ones(n) 76 | 77 | # 1st point 78 | Dc[0] = 0.0 79 | # 2nd point 80 | Da[1] = Dc[1] = 0.0 81 | # 3rd point 82 | Da[2] = Dc[2] = 0.0 83 | # last point 84 | Da[-1] = 0.0 85 | # 2nd from last 86 | Da[-2] = Dc[-2] = 0.0 87 | # 3rd from last 88 | Da[-3] = Dc[-3] = 0.0 89 | 90 | return TDMAsolver(Da, Db, Dc, rhs) 91 | 92 | 93 | def pade6(vec, h): 94 | """ 95 | 6th Order compact finite difference scheme (non-periodic BC). 96 | 97 | Lele, S. K. - Compact finite difference schemes with spectral-like 98 | resolution. Journal of Computational Physics 103 (1992) 16-42 99 | """ 100 | n = len(vec) 101 | rhs = np.zeros(n) 102 | 103 | a = 14.0 / 18.0 104 | b = 1.0 / 36.0 105 | 106 | rhs[2:-2] = (vec[3:-1] - vec[1:-3]) * (a / h) + (vec[4:] - vec[0:-4]) * (b / h) 107 | 108 | # boundaries: 109 | rhs[0] = ( 110 | (-197.0 / 60.0) * vec[0] 111 | + (-5.0 / 12.0) * vec[1] 112 | + 5.0 * vec[2] 113 | + (-5.0 / 3.0) * vec[3] 114 | + (5.0 / 12.0) * vec[4] 115 | + (-1.0 / 20.0) * vec[5] 116 | ) / h 117 | 118 | rhs[1] = ( 119 | (-20.0 / 33.0) * vec[0] 120 | + (-35.0 / 132.0) * vec[1] 121 | + (34.0 / 33.0) * vec[2] 122 | + (-7.0 / 33.0) * vec[3] 123 | + (2.0 / 33.0) * vec[4] 124 | + (-1.0 / 132.0) * vec[5] 125 | ) / h 126 | 127 | rhs[-1] = ( 128 | (197.0 / 60.0) * vec[-1] 129 | + (5.0 / 12.0) * vec[-2] 130 | + (-5.0) * vec[-3] 131 | + (5.0 / 3.0) * vec[-4] 132 | + (-5.0 / 12.0) * vec[-5] 133 | + (1.0 / 20.0) * vec[-6] 134 | ) / h 135 | 136 | rhs[-2] = ( 137 | (20.0 / 33.0) * vec[-1] 138 | + (35.0 / 132.0) * vec[-2] 139 | + (-34.0 / 33.0) * vec[-3] 140 | + (7.0 / 33.0) * vec[-4] 141 | + (-2.0 / 33.0) * vec[-5] 142 | + (1.0 / 132.0) * vec[-6] 143 | ) / h 144 | 145 | alpha1 = 5.0 # j = 1 and n 146 | alpha2 = 2.0 / 11 # j = 2 and n-1 147 | alpha = 1.0 / 3.0 148 | 149 | Db = np.ones(n) 150 | Da = alpha * np.ones(n) 151 | Dc = alpha * np.ones(n) 152 | 153 | # boundaries: 154 | Da[1] = alpha2 155 | Da[-1] = alpha1 156 | Da[-2] = alpha2 157 | Dc[0] = alpha1 158 | Dc[1] = alpha2 159 | Dc[-2] = alpha2 160 | 161 | return TDMAsolver(Da, Db, Dc, rhs) 162 | -------------------------------------------------------------------------------- /doc/speedup.rst: -------------------------------------------------------------------------------- 1 | Speedup tricks 2 | ============== 3 | 4 | EMD is inherently slow with little chances on improving its performance. This is mainly due to it being a serial method. That's both on within IMF stage, i.e. iterative sifting, or between IMFs, i.e. the next IMF depends on the previous. On top of that, the common configuration of the EMD uses the natural cubic spline to span envelopes, which in turn additionally decreases performance since it depends on all extrema in the signal. 5 | 6 | Since the EMD is the basis for other methods like EEMD and CEEMDAN these will also suffer from the same problem. What's more, these two methods perform the EMD many (hundreds) times which significantly increases any imperfections. It is expected that when it'll take more than a minute to perform an EEMD/CEEMDAN with default settings on a 10k+ samples long signal with a "medium complexity". There are, however, a couple of tweaks one can do to do make the computation finish sooner. 7 | 8 | Sections below describe a tweaks one can do to improve performance of the EMD. In short, these changes are: 9 | 10 | - `Parallel execution`_ (enabled by default for EEMD/CEEMDAN) 11 | - `Change data type`_ (downscale) 12 | - `Change spline method`_ to piecewise 13 | - `Decrease number of trials`_ 14 | - `Limit numer of output IMFs`_ 15 | 16 | 17 | Parallel execution 18 | ------------------ 19 | 20 | **This is enabled by default since version 1.8.0.** 21 | 22 | EEMD and CEEMDAN can take advantage of multiple CPU cores to speed up computation significantly. Both methods run multiple independent EMD decompositions on noise-perturbed signals, making them well-suited for parallel execution. 23 | 24 | As of version 1.8.0, both EEMD and CEEMDAN have ``parallel=True`` by default and will automatically use all available CPU cores. This typically provides near-linear speedup with the number of cores. 25 | 26 | To explicitly control parallelization:: 27 | 28 | from PyEMD import EEMD, CEEMDAN 29 | 30 | # Use all available CPUs (default behavior) 31 | eemd = EEMD(trials=100) 32 | 33 | # Use specific number of processes 34 | eemd = EEMD(trials=100, processes=4) 35 | 36 | # Disable parallelization (for reproducibility with seeds or debugging) 37 | eemd = EEMD(trials=100, parallel=False) 38 | 39 | .. note:: 40 | When using ``noise_seed()`` for reproducible results, you must set ``parallel=False``. 41 | Parallel execution does not guarantee deterministic ordering of results. 42 | 43 | **When to disable parallelization:** 44 | 45 | - When you need reproducible results using ``noise_seed()`` 46 | - For very short signals (< 100 samples) where multiprocessing overhead exceeds the benefit 47 | - For very few trials (< 4) where overhead isn't amortized 48 | - When debugging or profiling 49 | 50 | 51 | Change data type 52 | ---------------- 53 | 54 | Many programming frameworks by default casts numerical values to the largest data type it has. In case of Python's Numpy that's going to be numpy.float64. It's unlikely that one needs such resolution when using EMD [*]_. A suggestion is to downcast your data, e.g. to float16. The PyEMD should handle the same data type without upcasting but it can be additionally enforce a specific data type. To enable data type enforcement one needs to pass the DTYPE, i.e. :: 55 | 56 | from PyEMD import EMD 57 | 58 | emd = EMD(DTYPE=np.float16) 59 | 60 | Change spline method 61 | -------------------- 62 | 63 | EMD was presented with the natural cubic spline method to span envelopes and that's the default option in the PyEMD. It's great for signals with not many extrema but its not suggested for longer/more complex signals. The suggestion is to change the spline method to some piecewise splines like 'Akima' or 'piecewise cubic'. 64 | 65 | Example: :: 66 | 67 | from PyEMD import EEMD 68 | 69 | eemd = EEMD(spline_kind='akima') 70 | 71 | Decrease number of trials 72 | ---------------------------- 73 | 74 | This relates more to EEMD and CEEMDAN since they perform an EMD a multiple times with slightly modified signal. It's difficult to choose a correct number of iterations. This definitely relates to the signal in question. The more iterations the more certain that the solution is convergent but there is likely a point beyond which more evaluations change little. On the other side, the quicker we can get output the quicker we can use it. 75 | 76 | In the PyEMD, the number of iterations is referred to by `trials` and it's an explicit parameter to EEMD and CEEMDAN. The default value was selected arbitrarily and it's most likely wrong. An example on updating it: :: 77 | 78 | from PyEMD import CEEMDAN 79 | 80 | ceemdan = CEEMAN(trials=20) 81 | 82 | Limit numer of output IMFs 83 | -------------------------- 84 | 85 | Each method, by default, will perform decomposition until all components are returned. However, many use cases only require the first component. One can limit the number of returned components by setting up an implicit variable `max_imf` to the desired value. 86 | 87 | Example: :: 88 | 89 | from PyEMD import EEMD 90 | 91 | eemd = EEMD(max_imfs=2) 92 | 93 | 94 | .. [*] I, the PyEMD's author, will go even a bit further. If one needs such large resolution then the EMD is not suitable for them. The EMD is not robust. Hundreds of iterations make any small difference to be emphasised and potentially leading to a significant change in final decomposition. This is the reason for creating EEMD and CEEMDAN which add small perturbation in a hope that the ensemble provides a robust solution. 95 | -------------------------------------------------------------------------------- /doc/experimental.rst: -------------------------------------------------------------------------------- 1 | Experimental 2 | ============ 3 | 4 | Also known as **not supported**. 5 | 6 | Methods discussed and provided here have no guarantee to work or provide any meaningful results. 7 | These are somehow abandoned projects and unfortunately mid-way through. They aren't completely 8 | discarded simply because of hope that maybe someday someone will come and help fix them. 9 | We all know that the best motivation to do something is to be annoyed by the current state. 10 | Seriously though, mode decomposition in 2D and multi-dim is an interesting topic. Please? 11 | 12 | JIT EMD 13 | ------- 14 | .. note:: 15 | 16 | To use JitEMD you need to install PyEMD wiht ``jit`` option. 17 | If you're using ``pip`` then use this command 18 | 19 | .. code:: shell 20 | 21 | pip install EMD-signal[jit] 22 | 23 | Uses Numba (https://numba.pydata.org/) as a Just-in-Time (JIT) 24 | compiler for Python, mostly Numpy. Just-in-time compilation means that 25 | the code is compiled (machine code) during execution, and thus shows 26 | benefit when there's plenty of repeated code or same code used a lot. 27 | 28 | This EMD implementation is experimental as it only provides value 29 | when there's significant amount of computation required, e.g. when 30 | analyzing HUGE time series with a lot of internal complexity, 31 | or reuses the instance/method many times, e.g. in a script, 32 | iPython REPL or jupyter notebook. 33 | 34 | Additional reason for this being experimental is that the author (me) 35 | isn't well veristile in Numba optimization. There's definitely a lot 36 | that can be improved. It's being added as maybe it'll be helpful or 37 | an inspiration for others to learn something and contribute to the PyEMD. 38 | 39 | Comparison 40 | ********** 41 | 42 | There's an `example `_ which compares the pefromance between JIT and classic EMD. 43 | 44 | ❯ python example/emd_comparison.py 45 | 46 | Comparing EEMD execution on a larger signal with classic and JIT EMDs. 47 | Signal is random (uniform) noise of length: 2000. 48 | The test is done by executing EEMD with either classic or JIT EMD 20 times 49 | and taking the average. Such setup favouries JitEMD which is compiled once 50 | and then reused 19 times. Compiltion is quite costly. 51 | 52 | Classic EEMD on 2000 length random signal: 5.7 s per EEMD run 53 | 54 | JitEMD EEMD on 2000 length random signal: 4.2 per EEMD run 55 | 56 | Usage 57 | ***** 58 | 59 | There are two ways of interacting with JIT EMD; either as a function, 60 | or as a class compatible with the rest of PyEMD ecosystem. Please take 61 | a look at `code examples `_ for a quick start. 62 | 63 | Class JitEMD 64 | ************ 65 | 66 | When using class ``experimental.JitEMD`` it'll be compatible with other PyEMD classes, for example with EEMD. 67 | That's why it'll accept the same inputs and will provide the same outputs. 68 | The only difference is in configuring the class. It now has to be a copy of ``default_emd_config`` 69 | with updated values. 70 | 71 | .. code:: python 72 | 73 | from PyEMD.experimental.jitemd import default_emd_config, JitEMD, get_timeline 74 | 75 | rng = np.random.RandomState(4132) 76 | s = rng.random(500) 77 | t = get_timeline(len(s), s.dtype) 78 | 79 | config = default_emd_config 80 | config["FIXE"] = 4 81 | emd = JitEMD(config=config, spline_kind="akima") 82 | imfs = emd(s, t) 83 | 84 | 85 | Function JIT emd 86 | **************** 87 | 88 | When using ``emd`` function directly, you only get the *imfs* as the result and only once. 89 | There's also a differnce where the ``config`` is passed; it's directly to the method. 90 | Other than that, it should be the same. The class ``JitEMD`` uses this function and provides 91 | some abstraction on to of it for, hopefully, easier use, but there might be benefits to 92 | access the function directly. 93 | 94 | .. code:: python 95 | 96 | from PyEMD.experimental.jitemd import default_emd_config, emd, get_timeline 97 | 98 | s = rng.random(500) 99 | t = get_timeline(len(s), s.dtype) 100 | 101 | config = default_emd_config 102 | config["FIXE"] = 4 103 | imfs = emd(s, t, spline_kind="cubic", config=config) 104 | 105 | BEMD 106 | ---- 107 | 108 | .. warning:: 109 | 110 | Important This is an experimental module. Please use it with care as no 111 | guarantee can be given for obtaining reasonable results, or that they will be 112 | computed index the most computation optimal way. 113 | 114 | Info 115 | **** 116 | 117 | **BEMD** performed on bidimensional data such as images. 118 | This procedure uses morphological operators to detect regional maxima 119 | which are then used to span surface envelope with a radial basis function. 120 | 121 | Class 122 | ***** 123 | 124 | .. autoclass:: PyEMD.BEMD.BEMD 125 | :members: 126 | :special-members: 127 | 128 | EMD2D 129 | ----- 130 | 131 | .. warning:: 132 | 133 | Important This is an experimental module. Please use it with care as no 134 | guarantee can be given for obtaining reasonable results, or that they will be 135 | computed index the most computation optimal way. 136 | 137 | Info 138 | **** 139 | 140 | **EMD** performed on images. This version uses for envelopes 2D splines, 141 | which are span on extrema defined through maximum filter. 142 | 143 | Class 144 | ***** 145 | 146 | .. autoclass:: PyEMD.EMD2d.EMD2D 147 | :members: 148 | :special-members: 149 | -------------------------------------------------------------------------------- /PyEMD/checks.py: -------------------------------------------------------------------------------- 1 | """Calculate the statistical Significance of IMFs.""" 2 | 3 | import logging 4 | import math 5 | 6 | import numpy as np 7 | from scipy import stats 8 | from scipy.signal import find_peaks 9 | 10 | 11 | # helper function: Find mean period of an IMF 12 | def mean_period(data): 13 | """Return mean-period of signal.""" 14 | peaks = len(find_peaks(data, height=0)[0]) 15 | return len(data) / peaks if peaks > 0 else len(data) 16 | 17 | 18 | # helper function: find energy of signal/IMF 19 | def energy(data): 20 | """Return energy of signal.""" 21 | return sum(pow(data, 2)) 22 | 23 | 24 | # helper function: find IMF significance in 'a priori' test 25 | def significance_apriori(energy_density, T, N, alpha): 26 | """Check a priori significance and Return True if significant else False.""" 27 | k = abs(stats.norm.ppf((1 - alpha) / 2)) 28 | upper_limit = -T + (k * (math.sqrt(2 / N) * math.exp(T / 2))) 29 | lower_limit = -T - (k * (math.sqrt(2 / N) * math.exp(T / 2))) 30 | 31 | return not (lower_limit <= energy_density <= upper_limit) 32 | 33 | 34 | # helper function: find significance in 'a posteriori' test 35 | def significance_aposteriori(scaled_energy_density, T, N, alpha): 36 | """Check a posteriori significance and Return True if significant else False.""" 37 | k = abs(stats.norm.ppf((1 - alpha) / 2)) 38 | upper_limit = -T + (k * (math.sqrt(2 / N) * math.exp(T / 2))) 39 | return not (scaled_energy_density <= upper_limit) 40 | 41 | 42 | def whitenoise_check(IMFs: np.ndarray, test_name: str = "aposteriori", rescaling_imf: int = 1, alpha: float = 0.95): 43 | """Whitenoise statistical significance test. 44 | 45 | Performs whitenoise test as described by Wu & Huang [Wu2004]_. 46 | 47 | References 48 | ---------- 49 | .. [Wu2004] Zhaohua Wu, and Norden E. Huang. "A Study of the Characteristics of White Noise Using the 50 | Empirical Mode Decomposition Method." Proceedings: Mathematical, Physical and Engineering 51 | Sciences, vol. 460, no. 2046, The Royal Society, 2004, pp. 1597–611, http://www.jstor.org/stable/4143111. 52 | 53 | Parameters 54 | ---------- 55 | IMFs: np.ndarray 56 | (Required) numpy array containing IMFs computed from a normalized signal 57 | test_name: str 58 | (Optional) Test type. Supported values: 'apriori', 'aposteriori'. (default 'aposteriori') 59 | rescaling_imf: int 60 | (Optional) ith IMF of the signal used in rescaling for 'a posteriori' test. (default 1) 61 | alpha: float 62 | (Optional) The percentiles at which the test is to be performed; 0 < alpha < 1; (default 0.95) 63 | 64 | Returns 65 | ------- 66 | Optional dictionary 67 | Returns dictionary with keys and values as IMFs' number and test result, respetively, 68 | Test results can be either 0 (fail) or 1 (pass). 69 | In case of problems with the input imfs, e.g. NaN values or no imfs, we return None. 70 | 71 | Examples 72 | -------- 73 | >>> import numpy as np 74 | >>> from PyEMD import EMD 75 | >>> from PyEMD.checks import whitenoise_check 76 | >>> T = np.linspace(0, 1, 100) 77 | >>> S = np.sin(2*2*np.pi*T) 78 | >>> emd = EMD() 79 | >>> imfs = emd(S) 80 | >>> significant_imfs = whitenoise_check(imfs, test_name='apriori') 81 | >>> significant_imfs 82 | {1: 1, 2: 1} 83 | >>> type(significant_imfs) 84 | 85 | 86 | """ 87 | assert isinstance(IMFs, np.ndarray), "Invalid Data type, Pass a numpy.ndarray containing IMFs" 88 | # check if IMFs are empty or not 89 | if len(IMFs) == 0 or len(IMFs[0]) == 0: 90 | logging.getLogger("PyEMD").warning("Detected empty input. Skipping check.") 91 | return None 92 | 93 | assert isinstance(alpha, float), "Invalid Data type for alpha, pass a float value between (0,1)" 94 | assert 0 < alpha < 1, "alpha value should be in between (0,1)" 95 | assert test_name in ("apriori", "aposteriori"), "Invalid test type" 96 | assert isinstance(rescaling_imf, int), "Invalid data type for rescaling_imf, pass a int value" 97 | assert 0 < rescaling_imf <= len(IMFs), "Invalid rescaling IMF" 98 | 99 | if np.any(np.isnan(IMFs)): 100 | # Return None if input has NaN 101 | logging.getLogger("PyEMD").warning("Detected NaN values during whitenoise check. Skipping check.") 102 | return None 103 | 104 | N = len(IMFs[0]) 105 | output = {} 106 | if test_name == "apriori": 107 | for idx, imf in enumerate(IMFs): 108 | log_T = math.log(mean_period(imf)) 109 | energy_density = math.log(energy(imf) / N) 110 | sig_priori = significance_apriori(energy_density, log_T, N, alpha) 111 | 112 | output[idx + 1] = int(sig_priori) 113 | 114 | elif test_name == "aposteriori": 115 | scaling_imf_mean_period = math.log(mean_period(IMFs[rescaling_imf - 1])) 116 | scaling_imf_energy_density = math.log(energy(IMFs[rescaling_imf - 1]) / N) 117 | 118 | k = abs(stats.norm.ppf((1 - alpha) / 2)) 119 | up_limit = -scaling_imf_mean_period + (k * math.sqrt(2 / N) * math.exp(scaling_imf_mean_period) / 2) 120 | 121 | scaling_factor = math.exp(up_limit) 122 | 123 | for idx, imf in enumerate(IMFs): 124 | log_T = math.log(mean_period(imf)) 125 | if idx != rescaling_imf - 1: 126 | scaled_energy_density = math.log((energy(imf) / N) / scaling_factor) 127 | else: 128 | scaled_energy_density = math.log(scaling_factor) 129 | 130 | sig_aposteriori = significance_aposteriori(scaled_energy_density, log_T, N, alpha) 131 | 132 | output[idx + 1] = int(sig_aposteriori) 133 | 134 | else: 135 | raise AssertionError("Only 'apriori' and 'aposteriori' are allowed") 136 | 137 | return output 138 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # PyEMD documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Jun 6 17:46:14 2017. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | import datetime 20 | import sys 21 | import sphinx_rtd_theme 22 | 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | # 28 | # needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | "sphinx.ext.autodoc", 35 | "numpydoc", 36 | "sphinx.ext.napoleon", 37 | "sphinx.ext.autosummary", 38 | "sphinx.ext.doctest", 39 | "sphinx.ext.todo", 40 | "sphinx.ext.coverage", 41 | "sphinx.ext.mathjax", 42 | "sphinx.ext.viewcode", 43 | "sphinx.ext.intersphinx", 44 | "sphinx.ext.githubpages", 45 | "sphinx_rtd_theme", 46 | ] 47 | 48 | sys.path.insert(0, "../") 49 | from PyEMD import * 50 | 51 | numpydoc_show_class_members = False 52 | 53 | # -- General configuration ------------------------------------------------ 54 | 55 | # Add any paths that contain templates here, relative to this directory. 56 | templates_path = [".templates"] 57 | 58 | # The suffix(es) of source filenames. 59 | # You can specify multiple suffix as a list of string: 60 | # 61 | # source_suffix = ['.rst', '.md'] 62 | source_suffix = ".rst" 63 | 64 | # The master toctree document. 65 | master_doc = "index" 66 | 67 | # General information about the project. 68 | project = "PyEMD" 69 | copyright = "2016-{year}, Dawid Laszuk".format(year=datetime.datetime.now().year) 70 | author = "Dawid Laszuk" 71 | 72 | # The version info for the project you're documenting, acts as replacement for 73 | # |version| and |release|, also used in various other places throughout the 74 | # built documents. 75 | # 76 | # The short X.Y version. 77 | version = "0.4.0" 78 | # The full version, including alpha/beta/rc tags. 79 | release = "0.4.0" 80 | 81 | # The language for content autogenerated by Sphinx. Refer to documentation 82 | # for a list of supported languages. 83 | # 84 | # This is also used if you do content translation via gettext catalogs. 85 | # Usually you set "language" from the command line for these cases. 86 | language = "en" 87 | 88 | # List of patterns, relative to source directory, that match files and 89 | # directories to ignore when looking for source files. 90 | # This patterns also effect to html_static_path and html_extra_path 91 | exclude_patterns = [".build", "Thumbs.db", ".DS_Store"] 92 | 93 | # The name of the Pygments (syntax highlighting) style to use. 94 | pygments_style = "sphinx" 95 | 96 | # If true, `todo` and `todoList` produce output, else they produce nothing. 97 | todo_include_todos = True 98 | 99 | 100 | # -- Options for HTML output ---------------------------------------------- 101 | 102 | # html_theme = "bootstrap-limix" 103 | html_theme = "sphinx_rtd_theme" 104 | # Theme options are theme-specific and customize the look and feel of a theme 105 | # further. For a list of options available for each theme, see the 106 | # documentation. 107 | # 108 | # html_theme_options = {} 109 | 110 | # Add any paths that contain custom static files (such as style sheets) here, 111 | # relative to this directory. They are copied after the builtin static files, 112 | # so a file named "default.css" will overwrite the builtin "default.css". 113 | # html_static_path = ['.static'] 114 | # html_static_path = [] 115 | # html_static_path = limix_sphinx_theme.get_html_theme_path() 116 | 117 | 118 | # -- Options for HTMLHelp output ------------------------------------------ 119 | 120 | # Output file base name for HTML help builder. 121 | htmlhelp_basename = "PyEMDdoc" 122 | 123 | 124 | # -- Options for LaTeX output --------------------------------------------- 125 | 126 | latex_elements = { 127 | # The paper size ('letterpaper' or 'a4paper'). 128 | # 129 | # 'papersize': 'letterpaper', 130 | # The font size ('10pt', '11pt' or '12pt'). 131 | # 132 | # 'pointsize': '10pt', 133 | # Additional stuff for the LaTeX preamble. 134 | # 135 | # 'preamble': '', 136 | # Latex figure (float) alignment 137 | # 138 | # 'figure_align': 'htbp', 139 | } 140 | 141 | # Grouping the document tree into LaTeX files. List of tuples 142 | # (source start file, target name, title, 143 | # author, documentclass [howto, manual, or own class]). 144 | latex_documents = [ 145 | (master_doc, "PyEMD.tex", "PyEMD Documentation", "Dawid Laszuk", "manual"), 146 | ] 147 | 148 | 149 | # -- Options for manual page output --------------------------------------- 150 | 151 | # One entry per manual page. List of tuples 152 | # (source start file, name, description, authors, manual section). 153 | man_pages = [(master_doc, "PyEMD", "PyEMD Documentation", [author], 1)] 154 | 155 | 156 | # -- Options for Texinfo output ------------------------------------------- 157 | 158 | # Grouping the document tree into Texinfo files. List of tuples 159 | # (source start file, target name, title, author, 160 | # dir menu entry, description, category) 161 | texinfo_documents = [ 162 | (master_doc, "PyEMD", "PyEMD Documentation", author, "PyEMD", "One line description of project.", "Miscellaneous"), 163 | ] 164 | 165 | 166 | # intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} 167 | -------------------------------------------------------------------------------- /PyEMD/tests/test_checks.py: -------------------------------------------------------------------------------- 1 | """Tests for checks.py.""" 2 | 3 | import unittest 4 | 5 | import numpy as np 6 | 7 | from PyEMD.checks import energy, mean_period, significance_aposteriori, significance_apriori, whitenoise_check 8 | 9 | 10 | class TestCase(unittest.TestCase): 11 | """Test cases.""" 12 | 13 | def test_mean_period(self): 14 | """Test to check if mean period output is correct.""" 15 | T = np.linspace(0, 2, 100) 16 | S = np.sin(2 * np.pi * T) 17 | res = mean_period(S) 18 | self.assertEqual(type(res), float, "Default data type is float") 19 | self.assertTrue(res > 0, "mean-period cannot be zero") 20 | 21 | def test_mean_period_zero_peaks(self): 22 | """Tect to check if mean period function can handle zero peaks.""" 23 | T = np.linspace(0, 2, 100) 24 | res = mean_period(T) 25 | self.assertEqual(res, len(T), "mean-period is same as signal length in case of monotonic curve") 26 | 27 | def test_energy(self): 28 | """Test to check if energy of signal is being computed properly.""" 29 | T = np.linspace(0, 2, 200) 30 | S = np.sin(2 * 2 * np.pi * T) 31 | res = energy(S) 32 | self.assertEqual(type(res), np.float64, "Default data type is float") 33 | 34 | def test_significance_apriori(self): 35 | """a priori significance test.""" 36 | T = np.linspace(0, 2, 200) 37 | S = np.sin(2 * 2 * np.pi * T) 38 | energy_density = energy(S) / len(S) 39 | res = significance_apriori(energy_density, 2, len(S), 0.9) 40 | self.assertEqual(type(res), bool, "Default data type is bool") 41 | 42 | def test_significance_aposteriori(self): 43 | """a posteriori significance test.""" 44 | T = np.linspace(0, 2, 200) 45 | S = np.sin(2 * 2 * np.pi * T) 46 | energy_density = energy(S) / len(S) 47 | res = significance_aposteriori(energy_density, 2, len(S), 0.9) 48 | self.assertEqual(type(res), bool, "Default data type is bool") 49 | 50 | def test_whitenoise_check_apriori(self): 51 | """a priori whitenoise_check.""" 52 | T = [np.linspace(0, i, 200) for i in range(5, 0, -1)] 53 | S = np.array([list(np.sin(2 * 2 * np.pi * i)) for i in T]) 54 | res = whitenoise_check(S, test_name="apriori") 55 | self.assertEqual(type(res), dict or None, "Default data type is dict") 56 | 57 | def test_whitenoise_check_apriori_alpha(self): 58 | """a priori whitenoise_check with custom alpha.""" 59 | T = [np.linspace(0, i, 200) for i in range(5, 0, -1)] 60 | S = np.array([list(np.sin(2 * 2 * np.pi * i)) for i in T]) 61 | res = whitenoise_check(S, test_name="apriori", alpha=0.99) 62 | self.assertEqual(type(res), dict or None, "Default data type is dict") 63 | 64 | def test_whitenoise_check_alpha(self): 65 | """a posteriori whitenoise check with custom alpha value.""" 66 | T = [np.linspace(0, i, 200) for i in range(5, 0, -1)] 67 | S = np.array([list(np.sin(2 * 2 * np.pi * i)) for i in T]) 68 | res = whitenoise_check(S, alpha=0.9) 69 | self.assertEqual(type(res), dict or None, "Default data type is dict") 70 | 71 | def test_whitenoise_check_rescaling_imf(self): 72 | """a posteriori whitenoise check with custom rescaling imf.""" 73 | T = [np.linspace(0, i, 200) for i in range(5, 0, -1)] 74 | S = np.array([list(np.sin(2 * 2 * np.pi * i)) for i in T]) 75 | res = whitenoise_check(S, rescaling_imf=2) 76 | self.assertEqual(type(res), dict or None, "Default data type is dict") 77 | 78 | def test_whitenoise_check_nan_values(self): 79 | """whitenoise check with nan in IMF.""" 80 | S = np.array([np.full(100, np.nan) for i in range(5, 0, -1)]) 81 | res = whitenoise_check(S) 82 | self.assertEqual(res, None, "Input nan returns None") 83 | 84 | def test_invalid_alpha(self): 85 | """Test if invalid alpha return AssertionError.""" 86 | S = np.array([np.full(100, np.nan) for i in range(5, 0, -1)]) 87 | self.assertRaises(AssertionError, whitenoise_check, S, alpha=1) 88 | self.assertRaises(AssertionError, whitenoise_check, S, alpha=0) 89 | self.assertRaises(AssertionError, whitenoise_check, S, alpha=-10) 90 | self.assertRaises(AssertionError, whitenoise_check, S, alpha=2) 91 | self.assertRaises(AssertionError, whitenoise_check, S, alpha="0.5") 92 | 93 | def test_invalid_test_name(self): 94 | """Test if invalid test return AssertionError.""" 95 | S = np.random.random((5, 100)) 96 | self.assertRaises(AssertionError, whitenoise_check, S, test_name="apri") 97 | self.assertRaises(AssertionError, whitenoise_check, S, test_name="apost") 98 | self.assertRaises(AssertionError, whitenoise_check, S, test_name=None) 99 | 100 | def test_invalid_input_type(self): 101 | """Test if invalid input type return AssertionError.""" 102 | S = [np.full(100, np.nan) for i in range(5, 0, -1)] 103 | self.assertRaises(AssertionError, whitenoise_check, S) 104 | self.assertRaises(AssertionError, whitenoise_check, 1) 105 | self.assertRaises(AssertionError, whitenoise_check, 1.2) 106 | self.assertRaises(AssertionError, whitenoise_check, "[1,2,3,4,5]") 107 | 108 | def test_invalid_rescaling_imf(self): 109 | """Test if invalid rescaling imf return AssertionError.""" 110 | T = [np.linspace(0, i, 200) for i in range(5, 0, -1)] 111 | S = np.array([list(np.sin(2 * 2 * np.pi * i)) for i in T]) 112 | self.assertRaises(AssertionError, whitenoise_check, S, rescaling_imf=10) 113 | self.assertRaises(AssertionError, whitenoise_check, S, rescaling_imf=1.2) 114 | 115 | def test_empty_input_imf(self): 116 | """Test if empty IMF input return AssertionError.""" 117 | T1 = np.array([[], []]) 118 | T2 = np.array([]) 119 | res1 = whitenoise_check(T1) 120 | res2 = whitenoise_check(T2) 121 | self.assertEqual(res1, None, "Empty input returns None") 122 | self.assertEqual(res2, None, "Empty input returns None") 123 | 124 | 125 | if __name__ == "__main__": 126 | unittest.main() 127 | -------------------------------------------------------------------------------- /PyEMD/visualisation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.signal import hilbert 3 | 4 | from PyEMD.compact import filt6, pade6 5 | 6 | # Visualisation is an optional module. To minimise installation, `matplotlib` is not added 7 | # by default. Please install extras with `pip install -r requirement-extra.txt`. 8 | try: 9 | import pylab as plt 10 | except ImportError: 11 | pass 12 | 13 | 14 | class Visualisation(object): 15 | """Simple visualisation helper. 16 | 17 | This class is for quick and simple result visualisation. 18 | """ 19 | 20 | PLOT_WIDTH = 6 21 | PLOT_HEIGHT_PER_IMF = 1.5 22 | 23 | def __init__(self, emd_instance=None): 24 | self.emd_instance = emd_instance 25 | 26 | self.imfs = None 27 | self.residue = None 28 | 29 | if emd_instance is not None: 30 | self.imfs, self.residue = self.emd_instance.get_imfs_and_residue() 31 | 32 | def _check_imfs(self, imfs, residue, include_residue): 33 | """Checks for passed imfs and residue.""" 34 | imfs = imfs if imfs is not None else self.imfs 35 | residue = residue if residue is not None else self.residue 36 | 37 | if imfs is None: 38 | raise AttributeError("No imfs passed to plot") 39 | 40 | if include_residue and residue is None: 41 | raise AttributeError("Requested to plot residue but no residue provided") 42 | 43 | return imfs, residue 44 | 45 | def plot_imfs(self, imfs=None, residue=None, t=None, include_residue=True): 46 | """Plots and shows all IMFs. 47 | 48 | All parameters are optional since the `emd` object could have been passed when instantiating this object. 49 | 50 | The residual is an optional and can be excluded by setting `include_residue=False`. 51 | """ 52 | imfs, residue = self._check_imfs(imfs, residue, include_residue) 53 | 54 | num_rows, t_length = imfs.shape 55 | num_rows += include_residue is True 56 | 57 | t = t if t is not None else range(t_length) 58 | 59 | fig, axes = plt.subplots(num_rows, 1, figsize=(self.PLOT_WIDTH, num_rows * self.PLOT_HEIGHT_PER_IMF)) 60 | 61 | if num_rows == 1: 62 | axes = list(axes) 63 | 64 | axes[0].set_title("Time series") 65 | 66 | for num, imf in enumerate(imfs): 67 | ax = axes[num] 68 | ax.plot(t, imf) 69 | ax.set_ylabel("IMF " + str(num + 1)) 70 | 71 | if include_residue: 72 | ax = axes[-1] 73 | ax.plot(t, residue) 74 | ax.set_ylabel("Res") 75 | 76 | # Making the layout a bit more pleasant to the eye 77 | plt.tight_layout() 78 | 79 | def plot_instant_freq(self, t, imfs=None, order=False, alpha=None): 80 | """Plots and shows instantaneous frequencies for all provided imfs. 81 | 82 | The necessary parameter is `t` which is the time array used to compute the EMD. 83 | One should pass `imfs` if no `emd` instances is passed when creating the Visualisation object. 84 | 85 | Parameters 86 | ---------- 87 | 88 | order : bool (default: False) 89 | Represents whether the finite difference scheme is 90 | low-order (1st order forward scheme) or high-order (6th order 91 | compact scheme). The default value is False (low-order) 92 | 93 | alpha : float (default: None) 94 | Filter intensity. Default value is None, which 95 | is equivalent to `alpha` = 0.5, meaning that no filter is applied. 96 | The `alpha` values must be in between -0.5 (fully active) and 0.5 97 | (no filter). 98 | """ 99 | if alpha is not None: 100 | assert -0.5 < alpha < 0.5, "`alpha` must be in between -0.5 and 0.5" 101 | 102 | imfs, _ = self._check_imfs(imfs, None, False) 103 | num_rows = imfs.shape[0] 104 | 105 | imfs_inst_freqs = self._calc_inst_freq(imfs, t, order=order, alpha=alpha) 106 | 107 | fig, axes = plt.subplots(num_rows, 1, figsize=(self.PLOT_WIDTH, num_rows * self.PLOT_HEIGHT_PER_IMF)) 108 | 109 | if num_rows == 1: 110 | axes = fig.axes 111 | 112 | axes[0].set_title("Instantaneous frequency") 113 | 114 | for num, imf_inst_freq in enumerate(imfs_inst_freqs): 115 | ax = axes[num] 116 | ax.plot(t, imf_inst_freq) 117 | ax.set_ylabel("IMF {} [Hz]".format(num + 1)) 118 | 119 | # Making the layout a bit more pleasant to the eye 120 | plt.tight_layout() 121 | 122 | def _calc_inst_phase(self, sig, alpha): 123 | """Extract analytical signal through the Hilbert Transform.""" 124 | analytic_signal = hilbert(sig) # Apply Hilbert transform to each row 125 | if alpha is not None: 126 | assert -0.5 < alpha < 0.5, "`alpha` must be in between -0.5 and 0.5" 127 | real_part = np.array([filt6(row.real, alpha) for row in analytic_signal]) 128 | imag_part = np.array([filt6(row.imag, alpha) for row in analytic_signal]) 129 | analytic_signal = real_part + 1j * imag_part 130 | phase = np.unwrap(np.angle(analytic_signal)) # Compute angle between img and real 131 | if alpha is not None: 132 | phase = np.array([filt6(row, alpha) for row in phase]) # Filter phase 133 | return phase 134 | 135 | def _calc_inst_freq(self, sig, t, order, alpha): 136 | """Extracts instantaneous frequency through the Hilbert Transform.""" 137 | inst_phase = self._calc_inst_phase(sig, alpha=alpha) 138 | if order is False: 139 | inst_freqs = np.diff(inst_phase) / (2 * np.pi * (t[1] - t[0])) 140 | inst_freqs = np.concatenate((inst_freqs, inst_freqs[:, -1].reshape(inst_freqs[:, -1].shape[0], 1)), axis=1) 141 | else: 142 | inst_freqs = [pade6(row, t[1] - t[0]) / (2.0 * np.pi) for row in inst_phase] 143 | if alpha is None: 144 | return np.array(inst_freqs) 145 | else: 146 | return np.array([filt6(row, alpha) for row in inst_freqs]) # Filter freqs 147 | 148 | def show(self): 149 | plt.show() 150 | 151 | 152 | if __name__ == "__main__": 153 | from PyEMD import EMD 154 | 155 | # Simple signal example 156 | t = np.arange(0, 3, 0.01) 157 | S = np.sin(13 * t + 0.2 * t**1.4) - np.cos(3 * t) 158 | 159 | emd = EMD() 160 | emd.emd(S) 161 | imfs, res = emd.get_imfs_and_residue() 162 | 163 | # Initiate visualisation with emd instance 164 | vis = Visualisation(emd) 165 | 166 | # Create a plot with all IMFs and residue 167 | vis.plot_imfs(imfs=imfs, residue=res, t=t, include_residue=True) 168 | 169 | # Create a plot with instantaneous frequency of all IMFs 170 | vis.plot_instant_freq(t, imfs=imfs) 171 | 172 | # Show both plots 173 | vis.show() 174 | -------------------------------------------------------------------------------- /PyEMD/tests/test_ceemdan.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | from PyEMD import CEEMDAN 6 | 7 | 8 | class CEEMDANTest(unittest.TestCase): 9 | @staticmethod 10 | def cmp_msg(a, b): 11 | return "Expected {}, Returned {}".format(a, b) 12 | 13 | @staticmethod 14 | def test_default_call_CEEMDAN(): 15 | T = np.arange(50) 16 | S = np.cos(T * 0.1) 17 | max_imf = 2 18 | 19 | ceemdan = CEEMDAN(trials=5) 20 | ceemdan(S, T, max_imf) 21 | 22 | def test_ceemdan_simpleRun(self): 23 | T = np.linspace(0, 1, 100) 24 | S = np.sin(2 * np.pi * T) 25 | 26 | config = {"processes": 1} 27 | ceemdan = CEEMDAN(trials=10, max_imf=1, **config) 28 | ceemdan.EMD.FIXE_H = 5 29 | ceemdan.ceemdan(S) 30 | self.assertTrue("processes" in ceemdan.__dict__) 31 | 32 | def test_ceemdan_completeRun(self): 33 | S = np.random.random(200) 34 | 35 | ceemdan = CEEMDAN() 36 | cIMFs = ceemdan(S) 37 | 38 | self.assertTrue(cIMFs.shape[0] > 1) 39 | self.assertTrue(cIMFs.shape[1] == S.size) 40 | 41 | def test_ceemdan_passingArgumentsViaDict(self): 42 | trials = 10 43 | noise_kind = "uniform" 44 | spline_kind = "linear" 45 | 46 | # Making sure that we are not testing default options 47 | ceemdan = CEEMDAN() 48 | 49 | self.assertFalse(ceemdan.trials == trials, self.cmp_msg(ceemdan.trials, trials)) 50 | 51 | self.assertFalse( 52 | ceemdan.noise_kind == noise_kind, 53 | self.cmp_msg(ceemdan.noise_kind, noise_kind), 54 | ) 55 | 56 | self.assertFalse( 57 | ceemdan.EMD.spline_kind == spline_kind, 58 | self.cmp_msg(ceemdan.EMD.spline_kind, spline_kind), 59 | ) 60 | 61 | # Testing for passing attributes via params 62 | params = { 63 | "trials": trials, 64 | "noise_kind": noise_kind, 65 | "spline_kind": spline_kind, 66 | } 67 | ceemdan = CEEMDAN(**params) 68 | 69 | self.assertTrue(ceemdan.trials == trials, self.cmp_msg(ceemdan.trials, trials)) 70 | 71 | self.assertTrue( 72 | ceemdan.noise_kind == noise_kind, 73 | self.cmp_msg(ceemdan.noise_kind, noise_kind), 74 | ) 75 | 76 | self.assertTrue( 77 | ceemdan.EMD.spline_kind == spline_kind, 78 | self.cmp_msg(ceemdan.EMD.spline_kind, spline_kind), 79 | ) 80 | 81 | def test_ceemdan_testMaxImf(self): 82 | S = np.random.random(100) 83 | 84 | ceemdan = CEEMDAN(trials=10) 85 | 86 | max_imf = 1 87 | cIMFs = ceemdan(S, max_imf=max_imf) 88 | self.assertTrue(cIMFs.shape[0] == max_imf + 1) 89 | 90 | max_imf = 3 91 | cIMFs = ceemdan(S, max_imf=max_imf) 92 | self.assertTrue(cIMFs.shape[0] == max_imf + 1) 93 | 94 | @staticmethod 95 | def test_ceemdan_constantEpsilon(): 96 | S = np.random.random(100) 97 | 98 | ceemdan = CEEMDAN(trials=10, max_imf=2) 99 | ceemdan.beta_progress = False 100 | ceemdan(S) 101 | 102 | @staticmethod 103 | def test_ceemdan_noiseKind_uniform(): 104 | ceemdan = CEEMDAN() 105 | ceemdan.noise_kind = "uniform" 106 | ceemdan.generate_noise(1.0, 100) 107 | 108 | def test_ceemdan_noiseKind_unknown(self): 109 | ceemdan = CEEMDAN() 110 | ceemdan.noise_kind = "bernoulli" 111 | with self.assertRaises(ValueError): 112 | ceemdan.generate_noise(1.0, 100) 113 | 114 | def test_ceemdan_passingCustomEMD(self): 115 | spline_kind = "linear" 116 | params = {"spline_kind": spline_kind} 117 | 118 | ceemdan = CEEMDAN() 119 | self.assertFalse( 120 | ceemdan.EMD.spline_kind == spline_kind, 121 | "Not" + self.cmp_msg(ceemdan.EMD.spline_kind, spline_kind), 122 | ) 123 | 124 | from PyEMD import EMD 125 | 126 | emd = EMD(**params) 127 | 128 | ceemdan = CEEMDAN(ext_EMD=emd) 129 | self.assertTrue( 130 | ceemdan.EMD.spline_kind == spline_kind, 131 | self.cmp_msg(ceemdan.EMD.spline_kind, spline_kind), 132 | ) 133 | 134 | def test_ceemdan_noiseSeed(self): 135 | T = np.linspace(0, 1, 100) 136 | S = np.sin(2 * np.pi * T + 4**T) + np.cos((T - 0.4) ** 2) 137 | 138 | # Compare up to machine epsilon 139 | def cmpMachEps(x, y): 140 | return np.abs(x - y) <= 2 * np.finfo(x.dtype).eps 141 | 142 | # parallel=False required for reproducible seed behavior 143 | ceemdan = CEEMDAN(trials=10, parallel=False) 144 | 145 | # First run random seed 146 | cIMF1 = ceemdan(S) 147 | 148 | # Second run with defined seed, diff than first 149 | ceemdan.noise_seed(12345) 150 | cIMF2 = ceemdan(S) 151 | 152 | # Extremly unlikely to have same seed, thus different results 153 | msg_false = "Different seeds, expected different outcomes" 154 | if cIMF1.shape == cIMF2.shape: 155 | self.assertFalse(np.all(cmpMachEps(cIMF1, cIMF2)), msg_false) 156 | 157 | # Third run with same seed as with 2nd 158 | ceemdan.noise_seed(12345) 159 | cIMF3 = ceemdan(S) 160 | 161 | # Using same seeds, thus expecting same results 162 | msg_true = "Used same seed, expected same results" 163 | self.assertTrue(np.all(cmpMachEps(cIMF2, cIMF3)), msg_true) 164 | 165 | def test_ceemdan_originalSignal(self): 166 | T = np.linspace(0, 1, 100) 167 | S = 2 * np.cos(3 * np.pi * T) + np.cos(2 * np.pi * T + 4**T) 168 | 169 | # Make a copy of S for comparsion 170 | Scopy = np.copy(S) 171 | 172 | # Compare up to machine epsilon 173 | def cmpMachEps(x, y): 174 | return np.abs(x - y) <= 2 * np.finfo(x.dtype).eps 175 | 176 | ceemdan = CEEMDAN(trials=10) 177 | ceemdan(S) 178 | 179 | # The original signal should not be changed after the 'ceemdan' function. 180 | msg_true = "Expected no change of the original signal" 181 | self.assertTrue(np.all(cmpMachEps(Scopy, S)), msg_true) 182 | 183 | def test_ceemdan_notParallel(self): 184 | S = np.random.random(100) 185 | 186 | ceemdan = CEEMDAN(parallel=False) 187 | cIMFs = ceemdan(S) 188 | 189 | self.assertTrue(cIMFs.shape[0] > 1) 190 | self.assertTrue(cIMFs.shape[1] == S.size) 191 | 192 | def test_imfs_and_residue_accessor(self): 193 | S = np.random.random(100) 194 | ceemdan = CEEMDAN(parallel=False) 195 | cIMFs = ceemdan(S) 196 | 197 | imfs, residue = ceemdan.get_imfs_and_residue() 198 | self.assertEqual(cIMFs.shape[0], imfs.shape[0], "Compare number of components") 199 | self.assertEqual(len(residue), 100, "Check if residue exists") 200 | 201 | def test_imfs_and_residue_accessor2(self): 202 | ceemdan = CEEMDAN() 203 | with self.assertRaises(ValueError): 204 | _, _ = ceemdan.get_imfs_and_residue() 205 | 206 | 207 | if __name__ == "__main__": 208 | unittest.main() 209 | -------------------------------------------------------------------------------- /PyEMD/tests/test_eemd.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | from PyEMD import EEMD 6 | 7 | 8 | class EEMDTest(unittest.TestCase): 9 | @staticmethod 10 | def cmp_msg(a, b): 11 | return "Expected {}, Returned {}".format(a, b) 12 | 13 | @staticmethod 14 | def test_default_call_EEMD(): 15 | T = np.arange(50) 16 | S = np.cos(T * 0.1) 17 | max_imf = 2 18 | 19 | eemd = EEMD() 20 | eemd(S, T, max_imf) 21 | 22 | def test_eemd_simpleRun(self): 23 | T = np.linspace(0, 1, 100) 24 | S = np.sin(2 * np.pi * T) 25 | 26 | config = {"processes": 1} 27 | eemd = EEMD(trials=10, max_imf=1, **config) 28 | eemd.EMD.FIXE_H = 5 29 | eemd.eemd(S) 30 | 31 | self.assertTrue("processes" in eemd.__dict__) 32 | self.assertTrue(eemd.processes == 1) 33 | 34 | def test_eemd_passingArgumentsViaDict(self): 35 | trials = 10 36 | noise_kind = "uniform" 37 | spline_kind = "linear" 38 | 39 | # Making sure that we are not testing default options 40 | eemd = EEMD() 41 | 42 | self.assertFalse(eemd.trials == trials, self.cmp_msg(eemd.trials, trials)) 43 | 44 | self.assertFalse(eemd.noise_kind == noise_kind, self.cmp_msg(eemd.noise_kind, noise_kind)) 45 | 46 | self.assertFalse(eemd.EMD.spline_kind == spline_kind, self.cmp_msg(eemd.EMD.spline_kind, spline_kind)) 47 | 48 | # Testing for passing attributes via params 49 | params = {"trials": trials, "noise_kind": noise_kind, "spline_kind": spline_kind} 50 | eemd = EEMD(**params) 51 | 52 | self.assertTrue(eemd.trials == trials, self.cmp_msg(eemd.trials, trials)) 53 | 54 | self.assertTrue(eemd.noise_kind == noise_kind, self.cmp_msg(eemd.noise_kind, noise_kind)) 55 | 56 | self.assertTrue(eemd.EMD.spline_kind == spline_kind, self.cmp_msg(eemd.EMD.spline_kind, spline_kind)) 57 | 58 | def test_eemd_passingArgumentsDirectly(self): 59 | trials = 10 60 | noise_kind = "uniform" 61 | spline_kind = "linear" 62 | 63 | # Making sure that we are not testing default options 64 | eemd = EEMD() 65 | 66 | self.assertFalse(eemd.trials == trials, self.cmp_msg(eemd.trials, trials)) 67 | 68 | self.assertFalse(eemd.noise_kind == noise_kind, self.cmp_msg(eemd.noise_kind, noise_kind)) 69 | 70 | self.assertFalse(eemd.EMD.spline_kind == spline_kind, self.cmp_msg(eemd.EMD.spline_kind, spline_kind)) 71 | 72 | # Testing for passing attributes via params 73 | eemd = EEMD(trials=trials, noise_kind=noise_kind, spline_kind=spline_kind) 74 | 75 | self.assertTrue(eemd.trials == trials, self.cmp_msg(eemd.trials, trials)) 76 | 77 | self.assertTrue(eemd.noise_kind == noise_kind, self.cmp_msg(eemd.noise_kind, noise_kind)) 78 | 79 | self.assertTrue(eemd.EMD.spline_kind == spline_kind, self.cmp_msg(eemd.EMD.spline_kind, spline_kind)) 80 | 81 | def test_eemd_unsupportedNoiseKind(self): 82 | noise_kind = "whoever_supports_this_is_wrong" 83 | eemd = EEMD(noise_kind=noise_kind) 84 | 85 | with self.assertRaises(ValueError): 86 | eemd.generate_noise(1.0, 100) 87 | 88 | def test_eemd_passingCustomEMD(self): 89 | spline_kind = "linear" 90 | params = {"spline_kind": spline_kind} 91 | 92 | eemd = EEMD() 93 | self.assertFalse(eemd.EMD.spline_kind == spline_kind, "Not" + self.cmp_msg(eemd.EMD.spline_kind, spline_kind)) 94 | 95 | from PyEMD import EMD 96 | 97 | emd = EMD(**params) 98 | 99 | eemd = EEMD(ext_EMD=emd) 100 | self.assertTrue(eemd.EMD.spline_kind == spline_kind, self.cmp_msg(eemd.EMD.spline_kind, spline_kind)) 101 | 102 | def test_eemd_noiseSeed(self): 103 | T = np.linspace(0, 1, 100) 104 | S = np.sin(2 * np.pi * T + 4**T) + np.cos((T - 0.4) ** 2) 105 | 106 | # Compare up to machine epsilon 107 | def cmpMachEps(x, y): 108 | return np.abs(x - y) <= 2 * np.finfo(x.dtype).eps 109 | 110 | # parallel=False required for reproducible seed behavior 111 | eemd = EEMD(trials=10, parallel=False) 112 | 113 | # First run random seed 114 | eIMF1 = eemd(S) 115 | 116 | # Second run with defined seed, diff than first 117 | eemd.noise_seed(12345) 118 | eIMF2 = eemd(S) 119 | 120 | # Extremly unlikely to have same seed, thus different results 121 | msg_false = "Different seeds, expected different outcomes" 122 | if eIMF1.shape == eIMF2.shape: 123 | self.assertFalse(np.all(cmpMachEps(eIMF1, eIMF2)), msg_false) 124 | 125 | # Third run with same seed as with 2nd 126 | eemd.noise_seed(12345) 127 | eIMF3 = eemd(S) 128 | 129 | # Using same seeds, thus expecting same results 130 | msg_true = "Used same seed, expected same results" 131 | self.assertTrue(np.all(cmpMachEps(eIMF2, eIMF3)), msg_true) 132 | 133 | def test_eemd_notParallel(self): 134 | S = np.random.random(100) 135 | 136 | eemd = EEMD(trials=5, max_imf=2, parallel=False) 137 | eemd.EMD.FIXE_H = 2 138 | eIMFs = eemd.eemd(S) 139 | 140 | self.assertTrue(eIMFs.shape[0] > 0) 141 | self.assertTrue(eIMFs.shape[1], len(S)) 142 | 143 | def test_eemd_yesParallel(self): 144 | S = np.random.random(100) 145 | 146 | eemd = EEMD(trials=5, max_imf=2, parallel=True) 147 | eemd.EMD.FIXE_H = 2 148 | eIMFs = eemd.eemd(S) 149 | 150 | self.assertTrue(eIMFs.shape[0] > 0) 151 | self.assertTrue(eIMFs.shape[1], len(S)) 152 | 153 | def test_imfs_and_residue_accessor(self): 154 | S = np.random.random(100) 155 | eemd = EEMD(trials=5, max_imf=2, parallel=False) 156 | eIMFs = eemd(S) 157 | 158 | imfs, residue = eemd.get_imfs_and_residue() 159 | self.assertEqual(eIMFs.shape[0], imfs.shape[0], "Compare number of components") 160 | self.assertEqual(len(residue), 100, "Check if residue exists") 161 | 162 | def test_imfs_and_residue_accessor2(self): 163 | eemd = EEMD() 164 | with self.assertRaises(ValueError): 165 | imfs, residue = eemd.get_imfs_and_residue() 166 | 167 | def test_separate_trends(self): 168 | T = np.linspace(0, 2 * np.pi, 100) 169 | S = np.sin(T) + 3 * np.sin(3 * T + 0.1) + 0.2 * (T + 0.5) * (T - 2) 170 | eemd = EEMD(trials=20, separate_trends=True) 171 | 172 | eIMFs = eemd(S) 173 | for imf in eIMFs[:-1]: 174 | self.assertLess(abs(imf.mean()), 0.5) 175 | self.assertGreaterEqual(eIMFs[-1].mean(), 1) 176 | 177 | def test_eemd_ensemble_stats(self): 178 | T = np.linspace(0, 2 * np.pi, 100) 179 | S = np.sin(T) + 3 * np.sin(3 * T + 0.1) + 0.2 * (T + 0.5) * (T - 2) 180 | eemd = EEMD(trials=20, separate_trends=True) 181 | 182 | eIMFs = eemd(S) 183 | self.assertEqual(type(eemd.all_imfs), dict, "All imfs are stored as a dict") 184 | self.assertTrue(np.all(eIMFs == eemd.ensemble_mean()), "eIMFs are the mean over ensemble") 185 | self.assertEqual(eemd.ensemble_count(), [len(imfs) for imfs in eemd.all_imfs.values()]) 186 | self.assertEqual(type(eemd.ensemble_std()), np.ndarray, "Ensemble std exists and it's a numpy array") 187 | 188 | 189 | if __name__ == "__main__": 190 | unittest.main() 191 | -------------------------------------------------------------------------------- /PyEMD/tests/test_emd.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | from PyEMD import EMD 6 | 7 | 8 | class EMDTest(unittest.TestCase): 9 | @staticmethod 10 | def test_default_call_EMD(): 11 | T = np.arange(50) 12 | S = np.cos(T * 0.1) 13 | max_imf = 2 14 | 15 | emd = EMD() 16 | emd(S, T, max_imf) 17 | 18 | def test_different_length_input(self): 19 | T = np.arange(20) 20 | S = np.random.random(len(T) + 7) 21 | 22 | emd = EMD() 23 | with self.assertRaises(ValueError): 24 | emd.emd(S, T) 25 | 26 | def test_trend(self): 27 | """ 28 | Input is trend. Expeting no shifting process. 29 | """ 30 | emd = EMD() 31 | 32 | t = np.arange(0, 1, 0.01) 33 | S = 2 * t 34 | 35 | # Input - linear function f(t) = 2*t 36 | imfs = emd.emd(S, t) 37 | self.assertEqual(imfs.shape[0], 1, "Expecting single IMF") 38 | self.assertTrue(np.allclose(S, imfs[0])) 39 | 40 | def test_single_imf(self): 41 | """ 42 | Input is IMF. Expecint single shifting. 43 | """ 44 | 45 | def max_diff(a, b): 46 | return np.max(np.abs(a - b)) 47 | 48 | emd = EMD() 49 | emd.FIXE_H = 2 50 | 51 | t = np.arange(0, 1, 0.001) 52 | c1 = np.cos(4 * 2 * np.pi * t) # 2 Hz 53 | S = c1.copy() 54 | 55 | # Input - linear function f(t) = sin(2Hz t) 56 | imfs = emd.emd(S, t) 57 | self.assertEqual(imfs.shape[0], 1, "Expecting sin + trend") 58 | 59 | diff = np.allclose(imfs[0], c1) 60 | self.assertTrue(diff, "Expecting 1st IMF to be sin\nMaxDiff = " + str(max_diff(imfs[0], c1))) 61 | 62 | # Input - linear function f(t) = siin(2Hz t) + 2*t 63 | c2 = 5 * (t + 2) 64 | S += c2.copy() 65 | imfs = emd.emd(S, t) 66 | 67 | self.assertEqual(imfs.shape[0], 2, "Expecting sin + trend") 68 | diff1 = np.allclose(imfs[0], c1, atol=0.2) 69 | self.assertTrue(diff1, "Expecting 1st IMF to be sin\nMaxDiff = " + str(max_diff(imfs[0], c1))) 70 | diff2 = np.allclose(imfs[1], c2, atol=0.2) 71 | self.assertTrue(diff2, "Expecting 2nd IMF to be trend\nMaxDiff = " + str(max_diff(imfs[1], c2))) 72 | 73 | def test_emd_passArgsViaDict(self): 74 | FIXE = 10 75 | params = {"FIXE": FIXE, "nothing": 0} 76 | 77 | # First test without initiation 78 | emd = EMD() 79 | self.assertFalse(emd.FIXE == FIXE, "{} == {}".format(emd.FIXE, FIXE)) 80 | 81 | # Second: test with passing 82 | emd = EMD(**params) 83 | self.assertTrue(emd.FIXE == FIXE, "{} == {}".format(emd.FIXE, FIXE)) 84 | 85 | def test_emd_passImplicitParamsDirectly(self): 86 | FIXE = 10 87 | svar_thr = 0.2 88 | 89 | # First test without initiation 90 | emd = EMD() 91 | self.assertFalse(emd.FIXE == FIXE, "{} == {}".format(emd.FIXE, FIXE)) 92 | 93 | # Second: test with passing 94 | emd = EMD(FIXE=FIXE, svar_thr=svar_thr, nothing=0) 95 | self.assertTrue(emd.FIXE == FIXE, "{} == {}".format(emd.FIXE, FIXE)) 96 | self.assertTrue(emd.svar_thr == svar_thr, "{} == {}".format(emd.svar_thr, svar_thr)) 97 | 98 | def test_emd_FIXE(self): 99 | T = np.linspace(0, 1, 100) 100 | c = np.sin(9 * 2 * np.pi * T) 101 | offset = 4 102 | S = c + offset 103 | 104 | emd = EMD() 105 | 106 | # Default state: converge 107 | self.assertTrue(emd.FIXE == 0) 108 | self.assertTrue(emd.FIXE_H == 0) 109 | 110 | # Set 1 iteration per each sift, 111 | # same as removing offset 112 | FIXE = 1 113 | emd.FIXE = FIXE 114 | 115 | # Check flags correctness 116 | self.assertTrue(emd.FIXE == FIXE) 117 | self.assertTrue(emd.FIXE_H == 0) 118 | 119 | # Extract IMFs 120 | IMFs = emd.emd(S) 121 | 122 | # Check that IMFs are correct 123 | self.assertTrue(np.allclose(IMFs[0], c)) 124 | self.assertTrue(np.allclose(IMFs[1], offset)) 125 | 126 | def test_emd_FIXEH(self): 127 | T = np.linspace(0, 2, 200) 128 | c1 = 1 * np.sin(11 * 2 * np.pi * T + 0.1) 129 | c2 = 11 * np.sin(1 * 2 * np.pi * T + 0.1) 130 | offset = 9 131 | S = c1 + c2 + offset 132 | 133 | emd = EMD() 134 | 135 | # Default state: converge 136 | self.assertTrue(emd.FIXE == 0) 137 | self.assertTrue(emd.FIXE_H == 0) 138 | 139 | # Set 5 iterations per each protoIMF 140 | FIXE_H = 6 141 | emd.FIXE_H = FIXE_H 142 | 143 | # Check flags correctness 144 | self.assertTrue(emd.FIXE == 0) 145 | self.assertTrue(emd.FIXE_H == FIXE_H) 146 | 147 | # Extract IMFs 148 | imfs = emd.emd(S) 149 | 150 | # Check that IMFs are correct 151 | self.assertTrue(imfs.shape[0] == 3) 152 | 153 | close_imf1 = np.allclose(c1[2:-2], imfs[0, 2:-2], atol=0.2) 154 | self.assertTrue(close_imf1) 155 | self.assertTrue(np.allclose(c1, imfs[0], atol=1.0)) 156 | 157 | close_imf2 = np.allclose(c2[2:-2], imfs[1, 2:-2], atol=0.21) 158 | self.assertTrue(close_imf2) 159 | self.assertTrue(np.allclose(c2, imfs[1], atol=1.0)) 160 | 161 | close_offset = np.allclose(offset, imfs[2, 2:-2], atol=0.1) 162 | self.assertTrue(close_offset) 163 | 164 | close_offset = np.allclose(offset, imfs[2, 1:-1], atol=0.5) 165 | self.assertTrue(close_offset) 166 | 167 | def test_emd_default(self): 168 | T = np.linspace(0, 2, 200) 169 | c1 = 1 * np.sin(11 * 2 * np.pi * T + 0.1) 170 | c2 = 11 * np.sin(1 * 2 * np.pi * T + 0.1) 171 | offset = 9 172 | S = c1 + c2 + offset 173 | 174 | emd = EMD(spline_kind="akima") 175 | imfs = emd.emd(S, T) 176 | self.assertTrue(imfs.shape[0] == 3) 177 | 178 | close_imfs1 = np.allclose(c1[2:-2], imfs[0, 2:-2], atol=0.21) 179 | self.assertTrue(close_imfs1) 180 | 181 | close_imfs2 = np.allclose(c2[2:-2], imfs[1, 2:-2], atol=0.24) 182 | self.assertTrue(close_imfs2) 183 | 184 | close_offset = np.allclose(offset, imfs[2, 1:-1], atol=0.5) 185 | self.assertTrue(close_offset) 186 | 187 | def test_max_iteration_flag(self): 188 | S = np.random.random(200) 189 | emd = EMD() 190 | emd.MAX_ITERATION = 10 191 | emd.FIXE = 20 192 | 193 | imfs = emd.emd(S) 194 | 195 | # There's not much to test, except that it doesn't fail. 196 | # With low MAX_ITERATION value for random signal it's 197 | # guaranteed to have at least 2 imfs. 198 | self.assertTrue(imfs.shape[0] > 1) 199 | 200 | def test_get_imfs_and_residue(self): 201 | S = np.random.random(200) 202 | emd = EMD(**{"MAX_ITERATION": 10, "FIXE": 20}) 203 | all_imfs = emd(S, max_imf=3) 204 | 205 | imfs, residue = emd.get_imfs_and_residue() 206 | self.assertEqual(all_imfs.shape[0], imfs.shape[0] + 1, "Compare number of components") 207 | self.assertTrue(np.array_equal(all_imfs[:-1], imfs), "Shouldn't matter where imfs are from") 208 | self.assertTrue(np.array_equal(all_imfs[-1], residue), "Residue, if any, is the last row") 209 | 210 | def test_get_imfs_and_residue_without_running(self): 211 | emd = EMD() 212 | with self.assertRaises(ValueError): 213 | _, _ = emd.get_imfs_and_residue() 214 | 215 | def test_get_imfs_and_trend(self): 216 | emd = EMD() 217 | T = np.linspace(0, 2 * np.pi, 100) 218 | expected_trend = 5 * T 219 | S = 2 * np.sin(4.1 * 6.28 * T) + 1.2 * np.cos(7.4 * 6.28 * T) + expected_trend 220 | 221 | all_imfs = emd(S) 222 | imfs, trend = emd.get_imfs_and_trend() 223 | 224 | onset_trend = trend - trend.mean() 225 | onset_expected_trend = expected_trend - expected_trend.mean() 226 | self.assertEqual(all_imfs.shape[0], imfs.shape[0] + 1, "Compare number of components") 227 | self.assertTrue(np.array_equal(all_imfs[:-1], imfs), "Shouldn't matter where imfs are from") 228 | self.assertTrue( 229 | np.allclose(onset_trend, onset_expected_trend, rtol=0.1, atol=0.5), 230 | "Extracted trend should be close to the actual trend", 231 | ) 232 | 233 | 234 | if __name__ == "__main__": 235 | unittest.main() 236 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![codecov](https://codecov.io/gh/laszukdawid/PyEMD/branch/master/graph/badge.svg)](https://codecov.io/gh/laszukdawid/PyEMD) 2 | [![DocStatus](https://readthedocs.org/projects/pyemd/badge/?version=latest)](https://pyemd.readthedocs.io/) 3 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/f56b6fc3f855476dbaebd3c02ae88f3e)](https://www.codacy.com/gh/laszukdawid/PyEMD/dashboard?utm_source=github.com&utm_medium=referral&utm_content=laszukdawid/PyEMD&utm_campaign=Badge_Grade) 4 | [![DOI](https://zenodo.org/badge/65324353.svg)](https://zenodo.org/badge/latestdoi/65324353) 5 | [![Conda](https://anaconda.org/conda-forge/emd-signal/badges/version.svg)](https://anaconda.org/conda-forge/emd-signal/badges/version.svg) 6 | 7 | # PyEMD 8 | 9 | ## Links 10 | 11 | - Online documentation: 12 | - Issue tracker: 13 | - Source code repository: 14 | 15 | ## Introduction 16 | 17 | Python implementation of the Empirical Mode 18 | Decomposition (EMD). The package contains multiple EMD variations and 19 | intends to deliver more in time. 20 | 21 | ### Recent changes 22 | 23 | - \[2025-11 v1.9\] Migrate to `uv` and `nox` for builds and tests 24 | - \[2025-11 v1.8\] Performance boost of 18% to the core EMD 25 | 26 | ### EMD variations 27 | 28 | - Ensemble EMD (EEMD), 29 | - "Complete Ensemble EMD" (CEEMDAN) 30 | - different settings and configurations of vanilla EMD. 31 | - Image decomposition (EMD2D & BEMD) (experimental, no support) 32 | - Just-in-time compiled EMD (JitEMD) 33 | 34 | *PyEMD* allows you to use different splines for envelopes, stopping criteria 35 | and extrema interpolations. 36 | 37 | ### Available splines 38 | 39 | - Natural cubic (**default**) 40 | - Pointwise cubic 41 | - Hermite cubic 42 | - Akima 43 | - PChip 44 | - Linear 45 | 46 | ### Available stopping criteria 47 | 48 | - Cauchy convergence (**default**) 49 | - Fixed number of iterations 50 | - Number of consecutive proto-imfs 51 | 52 | ### Extrema detection 53 | 54 | - Discrete extrema (**default**) 55 | - Parabolic interpolation 56 | 57 | ## Installation 58 | 59 | **Note**: Downloadable package is called `emd-signal`. 60 | 61 | ### PyPi (recommended) 62 | 63 | The quickest way to install package is through `pip`. 64 | 65 | ```sh 66 | pip install EMD-signal 67 | ``` 68 | 69 | or with [uv]() you can do 70 | 71 | ```sh 72 | uv add emd-signal 73 | #or 74 | # uv pip install EMD-signal 75 | ``` 76 | 77 | In this way you install the latest stable release of PyEMD hosted on [PyPi](https://pypi.org/project/emd/). 78 | 79 | ### Conda 80 | 81 | PyEMD (as `emd-signal`) is available for Conda via conda-forge channel 82 | 83 | ```sh 84 | conda install -c conda-forge emd-signal 85 | ``` 86 | 87 | Source: [https://anaconda.org/conda-forge/emd-signal](https://anaconda.org/conda-forge/emd-signal) 88 | 89 | ### From source 90 | 91 | In case, if you only want to *use* EMD and its variations, the best way to install PyEMD is through `pip`. 92 | However, if you want the latest version of PyEMD, anyhow you might want to download the code and build package yourself. 93 | The source is publicaly available and hosted on [GitHub](https://github.com/laszukdawid/PyEMD). 94 | To download the code you can either go to the source code page and click `Code -> Download ZIP`, or use **git** command line 95 | 96 | ```sh 97 | git clone https://github.com/laszukdawid/PyEMD 98 | ``` 99 | 100 | Installing package from source is done using command line: 101 | 102 | ```sh 103 | python3 -m pip install . 104 | ``` 105 | 106 | after entering the PyEM directory created by `git`. 107 | 108 | A quicker way to install PyEMD from source is done using `pip` and `git` in the same command: 109 | 110 | ```sh 111 | python3 -m pip install git+https://github.com/laszukdawid/PyEMD.git 112 | ``` 113 | 114 | **Note**, however, that this will install it in your current environment. If you are working on many projects, or sharing reources with others, we suggest using [virtual environments](https://docs.python.org/3/library/venv.html). 115 | If you want to make your installation editable use the `-e` flag for [pip](https://packaging.python.org/en/latest/tutorials/installing-packages/) 116 | 117 | ## Example 118 | 119 | More detailed examples are included in the 120 | [documentation](https://pyemd.readthedocs.io/en/latest/examples.html) or 121 | in the 122 | [PyEMD/examples](https://github.com/laszukdawid/PyEMD/tree/master/example). 123 | 124 | ### EMD 125 | 126 | In most cases default settings are enough. Simply import `EMD` and pass 127 | your signal to instance or to `emd()` method. 128 | 129 | ```python 130 | from PyEMD import EMD 131 | import numpy as np 132 | 133 | s = np.random.random(100) 134 | emd = EMD() 135 | IMFs = emd(s) 136 | ``` 137 | 138 | The Figure below was produced with input: 139 | $S(t) = cos(22 \pi t^2) + 6t^2$ 140 | 141 | ![simpleExample](https://github.com/laszukdawid/PyEMD/raw/master/example/simple_example.png?raw=true) 142 | 143 | ### EEMD 144 | 145 | Simplest case of using Ensemble EMD (EEMD) is by importing `EEMD` and 146 | passing your signal to the instance or `eemd()` method. 147 | 148 | **Windows**: Please don't skip the `if __name__ == "__main__"` section. 149 | 150 | ```python 151 | from PyEMD import EEMD 152 | import numpy as np 153 | 154 | if __name__ == "__main__": 155 | s = np.random.random(100) 156 | eemd = EEMD() 157 | eIMFs = eemd(s) 158 | ``` 159 | 160 | ### CEEMDAN 161 | 162 | As with previous methods, also there is a simple way to use `CEEMDAN`. 163 | 164 | **Windows**: Please don't skip the `if __name__ == "__main__"` section. 165 | 166 | ```python 167 | from PyEMD import CEEMDAN 168 | import numpy as np 169 | 170 | if __name__ == "__main__": 171 | s = np.random.random(100) 172 | ceemdan = CEEMDAN() 173 | cIMFs = ceemdan(s) 174 | ``` 175 | 176 | ### Visualisation 177 | 178 | The package contains a simple visualisation helper that can help, e.g., with time series and instantaneous frequencies. 179 | 180 | ```python 181 | import numpy as np 182 | from PyEMD import EMD, Visualisation 183 | 184 | t = np.arange(0, 3, 0.01) 185 | S = np.sin(13*t + 0.2*t**1.4) - np.cos(3*t) 186 | 187 | # Extract imfs and residue 188 | # In case of EMD 189 | emd = EMD() 190 | emd.emd(S) 191 | imfs, res = emd.get_imfs_and_residue() 192 | 193 | # In general: 194 | #components = EEMD()(S) 195 | #imfs, res = components[:-1], components[-1] 196 | 197 | vis = Visualisation() 198 | vis.plot_imfs(imfs=imfs, residue=res, t=t, include_residue=True) 199 | vis.plot_instant_freq(t, imfs=imfs) 200 | vis.show() 201 | ``` 202 | 203 | ## Experimental 204 | 205 | ### JitEMD 206 | 207 | Just-in-time (JIT) compiled EMD is a version of EMD which exceed on very large signals 208 | or reusing the same instance multiple times. It's strongly sugested to be used in 209 | Jupyter notebooks when experimenting by modifyig input rather than the method itself. 210 | 211 | The problem with JIT is that the compilation happens on the first execution and it can be 212 | quite costly. With small signals, or performing decomposition just once, the extra time 213 | for compilation will be significantly larger than the decomposition, making it less performant. 214 | 215 | Please see documentation for more information or [examples](./example/) for how to use the code. 216 | This is experimental as it's value is still questionable, and the author (me) isn't proficient 217 | in JIT optimization so mistakes could've been made. 218 | 219 | Any feedback is welcomed. Happy to improve if there's intrest. Please open tickets with questions 220 | and suggestions. 221 | 222 | To enable JIT in your PyEMD, please install with `jit` option, i.e. 223 | 224 | ```sh 225 | pip install EMD-signal[jit] 226 | ``` 227 | 228 | ### EMD2D/BEMD 229 | 230 | *Unfortunately, this is Experimental and we can't guarantee that the output is meaningful.* 231 | The simplest use is to pass image as monochromatic numpy 2D array. Sample as 232 | with the other modules one can use the default setting of an instance or, more explicitly, 233 | use the `emd2d()` method. 234 | 235 | ```python 236 | from PyEMD.EMD2d import EMD2D #, BEMD 237 | import numpy as np 238 | 239 | x, y = np.arange(128), np.arange(128).reshape((-1,1)) 240 | img = np.sin(0.1*x)*np.cos(0.2*y) 241 | emd2d = EMD2D() # BEMD() also works 242 | IMFs_2D = emd2d(img) 243 | ``` 244 | 245 | ## F.A.Q 246 | 247 | ### Why is EEMD/CEEMDAN so slow? 248 | 249 | Unfortunately, that's their nature. They execute EMD multiple times every time with slightly modified version. Added noise can cause a creation of many extrema which will decrease performance of the natural cubic spline. For some tweaks on how to deal with that please see [Speedup tricks](https://pyemd.readthedocs.io/en/latest/speedup.html) in the documentation. 250 | 251 | ## Contact 252 | 253 | Feel free to contact me with any questions, requests or simply to say *hi*. 254 | It's always nice to know that I've helped someone or made their work easier. 255 | Contributing to the project is also acceptable and warmly welcomed. 256 | 257 | ### Citation 258 | 259 | If you found this package useful and would like to cite it in your work 260 | please use the following structure: 261 | 262 | ```latex 263 | @misc{pyemd, 264 | author = {Laszuk, Dawid}, 265 | title = {Python implementation of Empirical Mode Decomposition algorithm}, 266 | year = {2017}, 267 | publisher = {GitHub}, 268 | journal = {GitHub Repository}, 269 | howpublished = {\url{https://github.com/laszukdawid/PyEMD}}, 270 | doi = {10.5281/zenodo.5459184} 271 | } 272 | ``` 273 | -------------------------------------------------------------------------------- /PyEMD/tests/test_emd2d.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Coding: UTF-8 3 | 4 | import unittest 5 | 6 | import numpy as np 7 | 8 | from PyEMD.EMD2d import EMD2D 9 | 10 | 11 | @unittest.skip("Not supported until supported") 12 | class ImageEMDTest(unittest.TestCase): 13 | def setUp(self) -> None: 14 | self.emd2d = EMD2D() 15 | 16 | @staticmethod 17 | def _generate_image(r=64, c=64): 18 | return np.random.random((r, c)) 19 | 20 | @staticmethod 21 | def _generate_linear_image(r=16, c=16): 22 | rows = np.arange(r) 23 | return np.repeat(rows, c).reshape(r, c) 24 | 25 | @staticmethod 26 | def _generate_Gauss(x, y, pos, std, amp=1): 27 | x_s = x - pos[0] 28 | y_s = y - pos[1] 29 | x2 = x_s * x_s 30 | y2 = y_s * y_s 31 | 32 | exp = np.exp(-(x2 + y2) / (2 * std * std)) 33 | # exp[exp<1e-6] = 0 34 | scale = amp / np.linalg.norm(exp) 35 | 36 | return scale * exp 37 | 38 | def test_default_call_EMD2d(self): 39 | x = np.arange(50) 40 | y = np.arange(50) 41 | xv, yv = np.meshgrid(x, y) 42 | pos = (10, 20) 43 | std = 5 44 | img = self._generate_Gauss(xv, yv, pos, std) 45 | 46 | max_imf = 2 47 | 48 | emd2d = EMD2D() 49 | emd2d(img, max_imf) 50 | 51 | def test_endCondition_perfectReconstruction(self): 52 | c1 = self._generate_image() 53 | c2 = self._generate_image() 54 | IMFs = np.stack((c1, c2)) 55 | org_img = np.sum(IMFs, axis=0) 56 | self.assertTrue(self.emd2d.end_condition(org_img, IMFs)) 57 | 58 | def test_findExtrema_singleMax(self): 59 | x = np.arange(50) 60 | y = np.arange(50) 61 | xv, yv = np.meshgrid(x, y) 62 | pos = (10, 20) 63 | std = 5 64 | img_max = self._generate_Gauss(xv, yv, pos, std) 65 | 66 | idx_min, idx_max = self.emd2d.find_extrema(img_max) 67 | x_min, y_min = xv[idx_min], yv[idx_min] 68 | x_max, y_max = xv[idx_max], yv[idx_max] 69 | 70 | self.assertTrue((x_max, y_max) == pos) 71 | self.assertTrue(len(x_min) == 0) 72 | self.assertTrue(len(y_min) == 0) 73 | 74 | def test_findExtrema_singleMin(self): 75 | x = np.arange(50) 76 | y = np.arange(50) 77 | xv, yv = np.meshgrid(x, y) 78 | pos = (10, 20) 79 | std = 5 80 | img_max = (-1) * self._generate_Gauss(xv, yv, pos, std, 10) 81 | 82 | idx_min, idx_max = self.emd2d.find_extrema(img_max) 83 | x_min, y_min = xv[idx_min], yv[idx_min] 84 | x_max, y_max = xv[idx_max], yv[idx_max] 85 | 86 | self.assertTrue((x_min, y_min) == pos) 87 | self.assertTrue(len(x_max) == 0) 88 | self.assertTrue(len(y_max) == 0) 89 | 90 | def test_findExtrema_general(self): 91 | x = np.arange(50) 92 | y = np.arange(50) 93 | xv, yv = np.meshgrid(x, y) 94 | 95 | min_peaks = [((5, 40), 4, -1), ((34, 10), 2, -3)] 96 | max_peaks = [((10, 20), 5, 2), ((25, 25), 3, 1), ((40, 5), 3, 3)] 97 | 98 | # Construct image with few Gausses 99 | img = np.zeros(xv.shape) 100 | for peak in max_peaks + min_peaks: 101 | img = img + self._generate_Gauss(xv, yv, peak[0], peak[1], peak[2]) 102 | 103 | # Extract extrema 104 | idx_min, idx_max = self.emd2d.find_extrema(img) 105 | x_min = xv[idx_min].tolist() 106 | y_min = yv[idx_min].tolist() 107 | x_max = xv[idx_max].tolist() 108 | y_max = yv[idx_max].tolist() 109 | 110 | # Confirm that all peaks found - number 111 | self.assertTrue(len(x_min) == len(min_peaks)) 112 | self.assertTrue(len(y_min) == len(min_peaks)) 113 | self.assertTrue(len(x_max) == len(max_peaks)) 114 | self.assertTrue(len(y_max) == len(max_peaks)) 115 | 116 | for peak in min_peaks: 117 | peak_pos = peak[0] 118 | x_min.remove(peak_pos[0]) 119 | y_min.remove(peak_pos[1]) 120 | 121 | for peak in max_peaks: 122 | peak_pos = peak[0] 123 | x_max.remove(peak_pos[0]) 124 | y_max.remove(peak_pos[1]) 125 | 126 | # Confirm that all peaks found - exact position 127 | self.assertTrue(len(x_min) == 0) 128 | self.assertTrue(len(y_min) == 0) 129 | self.assertTrue(len(x_max) == 0) 130 | self.assertTrue(len(y_max) == 0) 131 | 132 | def test_splinePoints_SBS_simpleGrid(self): 133 | # Test points - leave space inbetween for interpolation 134 | X = np.arange(5) * 2 135 | Y = np.arange(5) * 2 136 | xm, ym = np.meshgrid(X, Y) 137 | 138 | xmf = xm.flatten() 139 | ymf = ym.flatten() 140 | 141 | # Constant value image 142 | zf = np.ones(xmf.size) 143 | 144 | # Interpolation grid 145 | xi = np.arange(10) 146 | yi = np.arange(10) 147 | 148 | # interpolated_image == np.ones((5,5)) 149 | interpolated_image = self.emd2d.spline_points(xmf, ymf, zf, xi, yi) 150 | 151 | # It is expected, that interpolation on constant will produce 152 | # constant image 153 | self.assertTrue(np.allclose(interpolated_image, 1)) 154 | 155 | def test_splinePoints_SBS_linearGrid(self): 156 | X = np.arange(5) * 2 157 | Y = np.arange(5) * 2 158 | xm, ym = np.meshgrid(X, Y) 159 | 160 | xmf = xm.flatten() 161 | ymf = ym.flatten() 162 | 163 | # Linear value image 164 | z = np.repeat(np.arange(0, 10, 2)[None, :], 5, axis=0) 165 | zf = z.flatten() 166 | 167 | # Interpolation grid 168 | xi = np.arange(9) 169 | yi = np.arange(9) 170 | 171 | # interpolated_image[row] == row 172 | interpolated_image = self.emd2d.spline_points(xmf, ymf, zf, xi, yi) 173 | 174 | # Since interpolation is on linear function, each row should have 175 | # value equal to position, i.e. z[0]=(0...), ,,,, z[n] = (n...n). 176 | for n in range(xi.size): 177 | nth_row = interpolated_image[n] 178 | self.assertTrue(np.allclose(nth_row, n)) 179 | 180 | def test_emd2d_noExtrema(self): 181 | linear_image = self._generate_linear_image() 182 | 183 | IMFs = self.emd2d.emd(linear_image) 184 | 185 | self.assertTrue(np.all(linear_image == IMFs)) 186 | 187 | def test_emd2d_simpleIMF(self): 188 | rows, cols = 128, 128 189 | 190 | # Sinusoidal IMF 191 | X = np.arange(cols)[None, :].T 192 | Y = np.arange(rows) 193 | sin_1d = np.sin(Y * 0.3) 194 | cos_1d = np.cos(X * 0.4) 195 | comp_2d = 10 * cos_1d * sin_1d 196 | comp_2d -= np.mean(comp_2d) 197 | 198 | image = comp_2d 199 | IMFs = self.emd2d.emd(image) 200 | 201 | # Image = IMF + noise 202 | self.assertTrue(IMFs.shape[0] <= 2, "Depending on spline, there should be an IMF and possibly trend") 203 | 204 | self.assertTrue( 205 | np.allclose(IMFs[0], image, atol=0.5), "Output: \n" + str(IMFs[0]) + "\nInput: \n" + str(comp_2d) 206 | ) 207 | 208 | def test_emd2d_linearBackground_simpleIMF(self): 209 | rows, cols = 128, 128 210 | linear_background = 0.1 * self._generate_linear_image(rows, cols) 211 | 212 | # Sinusoidal IMF 213 | X = np.arange(cols)[None, :].T 214 | Y = np.arange(rows) 215 | x_comp_1d = np.sin(X * 0.5) 216 | y_comp_1d = np.sin(Y * 0.2) 217 | comp_2d = 5 * x_comp_1d * y_comp_1d 218 | 219 | image = linear_background + comp_2d 220 | IMFs = self.emd2d.emd(image) 221 | 222 | # Check that only two IMFs were extracted 223 | self.assertTrue(IMFs.shape == (2, rows, cols), "Shape is " + str(IMFs.shape)) 224 | 225 | # First IMF should be sin 226 | self.assertTrue( 227 | np.allclose(IMFs[0], comp_2d, atol=1.0), "Output: \n" + str(IMFs[0]) + "\nInput: \n" + str(comp_2d) 228 | ) 229 | 230 | # Second IMF should be linear trend 231 | self.assertTrue(np.allclose(IMFs[1], linear_background, atol=1.0)) 232 | 233 | def test_emd2d_linearBackground_simpleIMF_FIXE(self): 234 | rows, cols = 128, 128 235 | linear_background = 0.1 * self._generate_linear_image(rows, cols) 236 | 237 | # Sinusoidal IMF 238 | X = np.arange(cols)[None, :].T 239 | Y = np.arange(rows) 240 | x_comp_1d = np.sin(X * 0.5) 241 | y_comp_1d = np.sin(Y * 0.2) 242 | comp_2d = 5 * x_comp_1d * y_comp_1d 243 | 244 | image = linear_background + comp_2d 245 | emd2d = EMD2D() 246 | emd2d.FIXE = 10 247 | IMFs = emd2d.emd(image) 248 | 249 | # Check that only two IMFs were extracted 250 | self.assertTrue(IMFs.shape == (2, rows, cols), "Shape is " + str(IMFs.shape)) 251 | 252 | # First IMF should be sin 253 | self.assertTrue( 254 | np.allclose(IMFs[0], comp_2d, atol=1.0), "Output: \n" + str(IMFs[0]) + "\nInput: \n" + str(comp_2d) 255 | ) 256 | 257 | # Second IMF should be linear trend 258 | self.assertTrue(np.allclose(IMFs[1], linear_background, atol=1.0)) 259 | 260 | def test_emd2d_linearBackground_simpleIMF_FIXE_H(self): 261 | rows, cols = 128, 128 262 | linear_background = 0.1 * self._generate_linear_image(rows, cols) 263 | 264 | # Sinusoidal IMF 265 | X = np.arange(cols)[None, :].T 266 | Y = np.arange(rows) 267 | x_comp_1d = np.sin(X * 0.5) 268 | y_comp_1d = np.sin(Y * 0.2) 269 | comp_2d = 5 * x_comp_1d * y_comp_1d 270 | 271 | image = linear_background + comp_2d 272 | emd2D = EMD2D() 273 | emd2D.FIXE_H = 10 274 | IMFs = emd2D.emd(image) 275 | 276 | # Check that only two IMFs were extracted 277 | self.assertTrue(IMFs.shape == (2, rows, cols), "Shape is " + str(IMFs.shape)) 278 | 279 | # First IMF should be sin 280 | self.assertTrue( 281 | np.allclose(IMFs[0], comp_2d, atol=1.0), "Output: \n" + str(IMFs[0]) + "\nInput: \n" + str(comp_2d) 282 | ) 283 | 284 | # Second IMF should be linear trend 285 | self.assertTrue(np.allclose(IMFs[1], linear_background, atol=1.0)) 286 | 287 | def test_emd2d_passArgsViaDict(self): 288 | FIXE = 10 289 | params = {"FIXE": FIXE} 290 | emd2D = EMD2D(**params) 291 | 292 | self.assertTrue(emd2D.FIXE == FIXE, "Received {}, Expceted {}".format(emd2D.FIXE, FIXE)) 293 | 294 | def test_emd2d_limitImfNo(self): 295 | # Create image 296 | rows, cols = 128, 128 297 | linear_background = 0.2 * self._generate_linear_image(rows, cols) 298 | 299 | # Sinusoidal IMF 300 | X = np.arange(cols)[None, :].T 301 | Y = np.arange(rows) 302 | x_comp_1d = np.sin(X * 0.3) + np.cos(X * 2.9) ** 2 303 | y_comp_1d = np.sin(Y * 0.2) 304 | comp_2d = 10 * x_comp_1d * y_comp_1d 305 | comp_2d = comp_2d 306 | 307 | image = linear_background + comp_2d 308 | 309 | # Limit number of IMFs 310 | max_imf = 2 311 | 312 | # decompose image 313 | IMFs = self.emd2d.emd(image, max_imf=max_imf) 314 | 315 | # It should have no more than 2 (max_imf) 316 | self.assertTrue(IMFs.shape[0] == max_imf) 317 | 318 | 319 | if __name__ == "__main__": 320 | unittest.main() 321 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2017 Dawid Laszuk 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | -------------------------------------------------------------------------------- /PyEMD/tests/test_extrema.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Coding: UTF-8 3 | 4 | import unittest 5 | 6 | import numpy as np 7 | 8 | from PyEMD import EMD 9 | 10 | 11 | class ExtremaTest(unittest.TestCase): 12 | def test_incorrectExtremaDetectionSetup(self): 13 | extrema_detection = "bubble_gum" 14 | 15 | # Sanity check 16 | emd = EMD() 17 | self.assertFalse(emd.extrema_detection == extrema_detection) 18 | 19 | # Assign incorrect extrema_detection 20 | emd.extrema_detection = extrema_detection 21 | self.assertTrue(emd.extrema_detection == extrema_detection) 22 | 23 | T = np.arange(10) 24 | S = np.sin(T) 25 | max_pos, max_val = np.random.random((2, 3)) 26 | min_pos, min_val = np.random.random((2, 3)) 27 | 28 | # Check for Exception 29 | with self.assertRaises(ValueError): 30 | emd.prepare_points(T, S, max_pos, max_val, min_pos, min_val) 31 | 32 | def test_wrong_extrema_detection_type(self): 33 | emd = EMD() 34 | emd.extrema_detection = "very_complicated" 35 | 36 | t = np.arange(10) 37 | s = np.array([-1, 0, 1, 0, -1, 0, 3, 0, -9, 0]) 38 | 39 | with self.assertRaises(ValueError): 40 | emd.find_extrema(t, s) 41 | 42 | def test_find_extrema_simple(self): 43 | """Simple test for extrema.""" 44 | emd = EMD() 45 | emd.extrema_detection = "simple" 46 | 47 | t = np.arange(10) 48 | s = np.array([-1, 0, 1, 0, -1, 0, 3, 0, -9, 0]) 49 | expMaxPos = [2, 6] 50 | expMaxVal = [1, 3] 51 | expMinPos = [4, 8] 52 | expMinVal = [-1, -9] 53 | expZeros = t[s == 0] 54 | 55 | maxPos, maxVal, minPos, minVal, nz = emd.find_extrema(t, s) 56 | 57 | self.assertEqual(maxPos.tolist(), expMaxPos) 58 | self.assertEqual(maxVal.tolist(), expMaxVal) 59 | self.assertEqual(minPos.tolist(), expMinPos) 60 | self.assertEqual(minVal.tolist(), expMinVal) 61 | self.assertEqual(nz.tolist(), expZeros.tolist()) 62 | 63 | def test_find_extrema_simple_repeat(self): 64 | r""" 65 | Test what happens in /^^\ situation, i.e. 66 | when extremum is somewhere between two consecutive pts. 67 | """ 68 | emd = EMD() 69 | emd.extrema_detection = "simple" 70 | 71 | t = np.arange(2, 13) 72 | s = np.array([-1, 0, 1, 1, 0, -1, 0, 3, 0, -9, 0]) 73 | expMaxPos = [4, 9] 74 | expMaxVal = [1, 3] 75 | expMinPos = [7, 11] 76 | expMinVal = [-1, -9] 77 | 78 | maxPos, maxVal, minPos, minVal, _ = emd.find_extrema(t, s) 79 | 80 | self.assertEqual(maxPos.tolist(), expMaxPos) 81 | self.assertEqual(maxVal.tolist(), expMaxVal) 82 | self.assertEqual(minPos.tolist(), expMinPos) 83 | self.assertEqual(minVal.tolist(), expMinVal) 84 | 85 | def test_bound_extrapolation_simple(self): 86 | emd = EMD() 87 | emd.extrema_detection = "simple" 88 | emd.nbsym = 1 89 | emd.DTYPE = np.int64 90 | 91 | S = [0, -3, 1, 4, 3, 2, -2, 0, 1, 2, 1, 0, 1, 2, 5, 4, 0, -2, -1] 92 | S = np.array(S) 93 | T = np.arange(len(S)) 94 | 95 | pp = emd.prepare_points 96 | 97 | # There are 4 cases for both (L)eft and (R)ight ends. In case of left (L) bound: 98 | # L1) ,/ -- ext[0] is min, s[0] < ext[1] (1st max) 99 | # L2) / -- ext[0] is min, s[0] > ext[1] (1st max) 100 | # L3) ^. -- ext[0] is max, s[0] > ext[1] (1st min) 101 | # L4) \ -- ext[0] is max, s[0] < ext[1] (1st min) 102 | 103 | # CASE 1 104 | # L1, R1 -- no edge MIN & no edge MIN 105 | s = S.copy() 106 | t = T.copy() 107 | 108 | maxPos, maxVal, minPos, minVal, _ = emd.find_extrema(t, s) 109 | 110 | # Should extrapolate left and right bounds 111 | maxExtrema, minExtrema = pp(t, s, maxPos, maxVal, minPos, minVal) 112 | 113 | self.assertEqual([-1, 3, 9, 14, 20], maxExtrema[0].tolist()) 114 | self.assertEqual([4, 4, 2, 5, 5], maxExtrema[1].tolist()) 115 | self.assertEqual([-4, 1, 6, 11, 17, 23], minExtrema[0].tolist()) 116 | self.assertEqual([-2, -3, -2, 0, -2, 0], minExtrema[1].tolist()) 117 | 118 | # CASE 2 119 | # L2, R2 -- edge MIN, edge MIN 120 | s = S[1:-1].copy() 121 | t = np.arange(s.size) 122 | 123 | maxPos, maxVal, minPos, minVal, _ = emd.find_extrema(t, s) 124 | 125 | # Should extrapolate left and right bounds 126 | maxExtrema, minExtrema = pp(t, s, maxPos, maxVal, minPos, minVal) 127 | 128 | self.assertEqual([-2, 2, 8, 13, 19], maxExtrema[0].tolist()) 129 | self.assertEqual([4, 4, 2, 5, 5], maxExtrema[1].tolist()) 130 | self.assertEqual([0, 5, 10, 16], minExtrema[0].tolist()) 131 | self.assertEqual([-3, -2, 0, -2], minExtrema[1].tolist()) 132 | 133 | # CASE 3 134 | # L3, R3 -- no edge MAX & no edge MAX 135 | s = S[2:-3].copy() 136 | t = np.arange(s.size) 137 | 138 | maxPos, maxVal, minPos, minVal, _ = emd.find_extrema(t, s) 139 | 140 | # Should extrapolate left and right bounds 141 | maxExtrema, minExtrema = pp(t, s, maxPos, maxVal, minPos, minVal) 142 | 143 | self.assertEqual([-5, 1, 7, 12, 17], maxExtrema[0].tolist()) 144 | self.assertEqual([2, 4, 2, 5, 2], maxExtrema[1].tolist()) 145 | self.assertEqual([-2, 4, 9, 15], minExtrema[0].tolist()) 146 | self.assertEqual([-2, -2, 0, 0], minExtrema[1].tolist()) 147 | 148 | # CASE 4 149 | # L4, R4 -- edge MAX & edge MAX 150 | s = S[3:-4].copy() 151 | t = np.arange(s.size) 152 | 153 | maxPos, maxVal, minPos, minVal, _ = emd.find_extrema(t, s) 154 | 155 | # Should extrapolate left and right bounds 156 | maxExtrema, minExtrema = pp(t, s, maxPos, maxVal, minPos, minVal) 157 | 158 | self.assertEqual([0, 6, 11], maxExtrema[0].tolist()) 159 | self.assertEqual([4, 2, 5], maxExtrema[1].tolist()) 160 | self.assertEqual([-3, 3, 8, 14], minExtrema[0].tolist()) 161 | self.assertEqual([-2, -2, 0, 0], minExtrema[1].tolist()) 162 | 163 | def test_find_extrema_parabol(self): 164 | """ 165 | Simple test for extrema. 166 | """ 167 | emd = EMD() 168 | emd.extrema_detection = "parabol" 169 | 170 | t = np.arange(10) 171 | s = np.array([-1, 0, 1, 0, -1, 0, 3, 0, -9, 0]) 172 | expMaxPos = [2, 6] 173 | expMaxVal = [1, 3] 174 | expMinPos = [4, 8] 175 | expMinVal = [-1, -9] 176 | 177 | maxPos, maxVal, minPos, minVal, _ = emd.find_extrema(t, s) 178 | 179 | self.assertEqual(maxPos.tolist(), expMaxPos) 180 | self.assertEqual(maxVal.tolist(), expMaxVal) 181 | self.assertEqual(minPos.tolist(), expMinPos) 182 | self.assertEqual(minVal.tolist(), expMinVal) 183 | 184 | def test_find_extrema_parabol_repeat(self): 185 | r""" 186 | Test what happens in /^^\ situation, i.e. 187 | when extremum is somewhere between two consecutive pts. 188 | """ 189 | emd = EMD() 190 | emd.extrema_detection = "parabol" 191 | 192 | t = np.arange(2, 13) 193 | s = np.array([-1, 0, 1, 1, 0, -1, 0, 3, 0, -9, 0]) 194 | expMaxPos = [4.5, 9] 195 | expMaxVal = [1.125, 3] 196 | expMinPos = [7, 11] 197 | expMinVal = [-1, -9] 198 | 199 | maxPos, maxVal, minPos, minVal, _ = emd.find_extrema(t, s) 200 | 201 | self.assertEqual(maxPos.tolist(), expMaxPos) 202 | self.assertEqual(maxVal.tolist(), expMaxVal) 203 | self.assertEqual(minPos.tolist(), expMinPos) 204 | self.assertEqual(minVal.tolist(), expMinVal) 205 | 206 | def test_bound_extrapolation_parabol(self): 207 | emd = EMD() 208 | emd.extrema_detection = "parabol" 209 | emd.nbsym = 1 210 | emd.DTYPE = np.float64 211 | 212 | S = [0, -3, 1, 4, 3, 2, -2, 0, 1, 2, 1, 0, 1, 2, 5, 4, 0, -2, -1] 213 | S = np.array(S) 214 | T = np.arange(len(S)) 215 | 216 | pp = emd.prepare_points 217 | 218 | # There are 4 cases for both (L)eft and (R)ight ends. In case of left (L) bound: 219 | # L1) ,/ -- ext[0] is min, s[0] < ext[1] (1st max) 220 | # L2) / -- ext[0] is min, s[0] > ext[1] (1st max) 221 | # L3) ^. -- ext[0] is max, s[0] > ext[1] (1st min) 222 | # L4) \ -- ext[0] is max, s[0] < ext[1] (1st min) 223 | 224 | # CASE 1 225 | # L1, R1 -- no edge MIN & no edge MIN 226 | s = S.copy() 227 | t = T.copy() 228 | 229 | maxPos, maxVal, minPos, minVal, _ = emd.find_extrema(t, s) 230 | 231 | # Should extrapolate left and right bounds 232 | maxExtrema, minExtrema = pp(t, s, maxPos, maxVal, minPos, minVal) 233 | 234 | maxExtrema = np.round(maxExtrema, decimals=3) 235 | minExtrema = np.round(minExtrema, decimals=3) 236 | 237 | self.assertEqual([-1.393, 3.25, 9, 14.25, 20.083], maxExtrema[0].tolist()) 238 | self.assertEqual([4.125, 4.125, 2, 5.125, 5.125], maxExtrema[1].tolist()) 239 | self.assertEqual([-4.31, 0.929, 6.167, 11, 17.167, 23.333], minExtrema[0].tolist()) 240 | self.assertEqual([-2.083, -3.018, -2.083, 0, -2.042, 0], minExtrema[1].tolist()) 241 | 242 | # CASE 2 243 | # L2, R2 -- edge MIN, edge MIN 244 | s = S[1:-1].copy() 245 | t = T[1:-1].copy() 246 | 247 | maxPos, maxVal, minPos, minVal, _ = emd.find_extrema(t, s) 248 | 249 | # Should extrapolate left and right bounds 250 | maxExtrema, minExtrema = pp(t, s, maxPos, maxVal, minPos, minVal) 251 | 252 | maxExtrema = np.round(maxExtrema, decimals=3) 253 | minExtrema = np.round(minExtrema, decimals=3) 254 | 255 | self.assertEqual([-1.25, 3.25, 9, 14.25, 19.75], maxExtrema[0].tolist()) 256 | self.assertEqual([4.125, 4.125, 2, 5.125, 5.125], maxExtrema[1].tolist()) 257 | self.assertEqual([1, 6.167, 11, 17], minExtrema[0].tolist()) 258 | self.assertEqual([-3, -2.083, 0, -2], minExtrema[1].tolist()) 259 | 260 | # CASE 3 261 | # L3, R3 -- no edge MAX & no edge MAX 262 | s = S[2:-3].copy() 263 | t = T[2:-3].copy() 264 | 265 | maxPos, maxVal, minPos, minVal, _ = emd.find_extrema(t, s) 266 | 267 | # Should extrapolate left and right bounds 268 | maxExtrema, minExtrema = pp(t, s, maxPos, maxVal, minPos, minVal) 269 | 270 | maxExtrema = np.round(maxExtrema, decimals=3) 271 | minExtrema = np.round(minExtrema, decimals=3) 272 | 273 | self.assertEqual([-2.5, 3.25, 9, 14.25, 19.5], maxExtrema[0].tolist()) 274 | self.assertEqual([2, 4.125, 2, 5.125, 2], maxExtrema[1].tolist()) 275 | self.assertEqual([0.333, 6.167, 11, 17.5], minExtrema[0].tolist()) 276 | self.assertEqual([-2.083, -2.083, 0, 0], minExtrema[1].tolist()) 277 | 278 | # CASE 4 279 | # L4, R4 -- edge MAX & edge MAX 280 | s = S[3:-4].copy() 281 | t = T[3:-4].copy() 282 | 283 | maxPos, maxVal, minPos, minVal, _ = emd.find_extrema(t, s) 284 | 285 | # Should extrapolate left and right bounds 286 | maxExtrema, minExtrema = pp(t, s, maxPos, maxVal, minPos, minVal) 287 | 288 | maxExtrema = np.round(maxExtrema, decimals=3) 289 | minExtrema = np.round(minExtrema, decimals=3) 290 | 291 | self.assertEqual([3, 9, 14], maxExtrema[0].tolist()) 292 | self.assertEqual([4, 2, 5], maxExtrema[1].tolist()) 293 | self.assertEqual([-0.167, 6.167, 11, 17], minExtrema[0].tolist()) 294 | self.assertEqual([-2.083, -2.083, 0, 0], minExtrema[1].tolist()) 295 | 296 | # TODO: 297 | # - nbsym > 1 298 | 299 | 300 | if __name__ == "__main__": 301 | unittest.main() 302 | -------------------------------------------------------------------------------- /PyEMD/BEMD.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding: UTF-8 3 | # 4 | # Author: Dawid Laszuk 5 | # Contact: https://github.com/laszukdawid/PyEMD/issues 6 | # 7 | # Feel free to contact for any information. 8 | 9 | import logging 10 | 11 | import numpy as np 12 | from scipy.interpolate import Rbf 13 | 14 | try: 15 | from skimage.morphology import reconstruction 16 | except (ImportError, ModuleNotFoundError): 17 | raise ImportError( 18 | "EMD2D and BEMD are not supported. Feel free to play around and improve them. " 19 | + "Required dependencies are in `requirements-extra`." 20 | ) 21 | 22 | 23 | class BEMD: 24 | """ 25 | **Bidimensional Empirical Mode Decomposition** 26 | 27 | **Important**: This class intends to be undocumented until it's actually properly tested 28 | and proven to work. An attempt to replicate findings in the paper cited below has failed. 29 | This method is only included in the package because someone asked for it, and I'm hoping 30 | that one day someone else will come and *fix it*. Until then, USE AT YOUR OWN RISK. 31 | 32 | The guess why the decomposition doesn't work is that it's difficult to extrapolate image 33 | far away from extrema. Not even mirroring helps in this case. 34 | 35 | Method decomposition 2D arrays like gray-scale images into 2D representations of 36 | Intrinsic Mode Functions (IMFs). 37 | 38 | The algorithm is based on Nunes et. al. [Nunes2003]_ work. 39 | 40 | .. [Nunes2003] J.-C. Nunes, Y. Bouaoune, E. Delechelle, O. Niang, P. Bunel., 41 | "Image analysis by bidimensional empirical mode decomposition. Image and Vision Computing", 42 | Elsevier, 2003, 21 (12), pp.1019-1026. 43 | """ 44 | 45 | logger = logging.getLogger(__name__) 46 | 47 | def __init__(self): 48 | # ProtoIMF related 49 | self.mse_thr = 0.01 50 | self.mean_thr = 0.01 51 | 52 | self.FIXE = 1 # Single iteration by default, otherwise results are terrible 53 | self.FIXE_H = 0 54 | self.MAX_ITERATION = 5 55 | 56 | def __call__(self, image, max_imf=-1): 57 | return self.bemd(image, max_imf=max_imf) 58 | 59 | def extract_max_min_spline(self, image, min_peaks_pos, max_peaks_pos): 60 | """Calculates top and bottom envelopes for image. 61 | 62 | Parameters 63 | ---------- 64 | image : numpy 2D array 65 | 66 | Returns 67 | ------- 68 | min_env : numpy 2D array 69 | Bottom envelope in form of an image. 70 | max_env : numpy 2D array 71 | Top envelope in form of an image. 72 | """ 73 | xi, yi = np.meshgrid(np.arange(image.shape[0]), np.arange(image.shape[1])) 74 | min_val = np.array([image[x, y] for x, y in zip(*min_peaks_pos)]) 75 | max_val = np.array([image[x, y] for x, y in zip(*max_peaks_pos)]) 76 | min_env = self.spline_points(min_peaks_pos[0], min_peaks_pos[1], min_val, xi, yi) 77 | max_env = self.spline_points(max_peaks_pos[0], max_peaks_pos[1], max_val, xi, yi) 78 | return min_env, max_env 79 | 80 | @classmethod 81 | def spline_points(cls, X, Y, Z, xi, yi): 82 | """Creates a spline for given set of points. 83 | 84 | Uses Radial-basis function to extrapolate surfaces. It's not the best but gives something. 85 | Grid data algorithm didn't work. 86 | """ 87 | spline = Rbf(X, Y, Z, function="cubic") 88 | return spline(xi, yi) 89 | 90 | @classmethod 91 | def find_extrema_positions(cls, image): 92 | """ 93 | Finds extrema, both minima and maxima, based on morphological reconstruction. 94 | Returns extrema where the first and second elements are x and y positions, respectively. 95 | 96 | Parameters 97 | ---------- 98 | image : numpy 2D array 99 | Monochromatic image or any 2D array. 100 | 101 | Returns 102 | ------- 103 | min_peaks_pos : numpy array 104 | Minima positions. 105 | max_peaks_pos : numpy array 106 | Maxima positions. 107 | """ 108 | min_peaks_pos = BEMD.extract_minima_positions(image) 109 | max_peaks_pos = BEMD.extract_maxima_positions(image) 110 | return min_peaks_pos, max_peaks_pos 111 | 112 | @classmethod 113 | def extract_minima_positions(cls, image): 114 | return BEMD.extract_maxima_positions(-image) 115 | 116 | @classmethod 117 | def extract_maxima_positions(cls, image): 118 | seed_min = image - 1 119 | dilated = reconstruction(seed_min, image, method="dilation") 120 | cleaned_image = image - dilated 121 | return np.where(cleaned_image > 0)[::-1] 122 | 123 | @classmethod 124 | def end_condition(cls, image, IMFs): 125 | """Determines whether decomposition should be stopped. 126 | 127 | Parameters 128 | ---------- 129 | image : numpy 2D array 130 | Input image which is decomposed. 131 | IMFs : numpy 3D array 132 | Array for which first dimensions relates to respective IMF, 133 | i.e. (numIMFs, imageX, imageY). 134 | """ 135 | rec = np.sum(IMFs, axis=0) 136 | 137 | # If reconstruction is perfect, no need for more tests 138 | if np.allclose(image, rec): 139 | return True 140 | 141 | return False 142 | 143 | def check_proto_imf(self, proto_imf, proto_imf_prev, mean_env): 144 | """Check whether passed (proto) IMF is actual IMF. 145 | Current condition is solely based on checking whether the mean is below threshold. 146 | 147 | Parameters 148 | ---------- 149 | proto_imf : numpy 2D array 150 | Current iteration of proto IMF. 151 | proto_imf_prev : numpy 2D array 152 | Previous iteration of proto IMF. 153 | mean_env : numpy 2D array 154 | Local mean computed from top and bottom envelopes. 155 | 156 | Returns 157 | ------- 158 | boolean 159 | Whether current proto IMF is actual IMF. 160 | """ 161 | # TODO: Sifting is very sensitive and subtracting const val can often flip 162 | # maxima with minima in decomposition and thus repeating above/below 163 | # behaviour. For now, mean_env is checked whether close to zero excluding 164 | # its offset. 165 | if np.all(np.abs(mean_env - mean_env.mean()) < self.mean_thr): 166 | # if np.all(np.abs(mean_env) self.mse_thr: 180 | return False 181 | 182 | return False 183 | 184 | def bemd(self, image, max_imf=-1): 185 | """Performs bidimensional EMD (BEMD) on grey-scale image with specified parameters. 186 | 187 | Parameters 188 | ---------- 189 | image : numpy 2D array, 190 | Grey-scale image. 191 | max_imf : int, (default: -1) 192 | IMF number to which decomposition should be performed. 193 | Negative value means *all*. 194 | 195 | Returns 196 | ------- 197 | IMFs : numpy 3D array 198 | Set of IMFs in form of numpy array where the first dimension 199 | relates to IMF's ordinary number. 200 | """ 201 | image_s = image.copy() 202 | 203 | imf = np.zeros(image.shape) 204 | imf_old = imf.copy() 205 | 206 | imfNo = 0 207 | IMF = np.empty((imfNo,) + image.shape) 208 | notFinished = True 209 | 210 | while notFinished: 211 | self.logger.debug("IMF -- " + str(imfNo)) 212 | 213 | res = image_s - np.sum(IMF[:imfNo], axis=0) 214 | imf = res.copy() 215 | mean_env = np.zeros(image.shape) 216 | stop_sifting = False 217 | 218 | # Counters 219 | n = 0 # All iterations for current imf. 220 | n_h = 0 # counts when mean(proto_imf) < threshold 221 | 222 | while not stop_sifting and n < self.MAX_ITERATION: 223 | n += 1 224 | self.logger.debug("Iteration: %i", n) 225 | 226 | min_peaks_pos, max_peaks_pos = self.find_extrema_positions(imf) 227 | self.logger.debug( 228 | "min_peaks_pos = %i | max_peaks_pos = %i", len(min_peaks_pos[0]), len(max_peaks_pos[0]) 229 | ) 230 | if len(min_peaks_pos[0]) > 1 and len(max_peaks_pos[0]) > 1: 231 | min_env, max_env = self.extract_max_min_spline(imf, min_peaks_pos, max_peaks_pos) 232 | mean_env = 0.5 * (min_env + max_env) 233 | 234 | imf_old = imf.copy() 235 | imf = imf - mean_env 236 | 237 | # Fix number of iterations 238 | if self.FIXE: 239 | if n >= self.FIXE + 1: 240 | stop_sifting = True 241 | 242 | # Fix number of iterations after number of zero-crossings 243 | # and extrema differ at most by one. 244 | elif self.FIXE_H: 245 | if n == 1: 246 | continue 247 | if self.check_proto_imf(imf, imf_old, mean_env): 248 | n_h += 1 249 | else: 250 | n_h = 0 251 | 252 | # STOP if enough n_h 253 | if n_h >= self.FIXE_H: 254 | stop_sifting = True 255 | 256 | # Stops after default stopping criteria are met 257 | else: 258 | if self.check_proto_imf(imf, imf_old, mean_env): 259 | stop_sifting = True 260 | 261 | else: 262 | stop_sifting = True 263 | 264 | IMF = np.vstack((IMF, imf.copy()[None, :])) 265 | imfNo += 1 266 | 267 | if self.end_condition(image, IMF) or (max_imf > 0 and imfNo >= max_imf): 268 | notFinished = False 269 | break 270 | 271 | res = image_s - np.sum(IMF[:imfNo], axis=0) 272 | if not np.allclose(res, 0): 273 | IMF = np.vstack((IMF, res[None, :])) 274 | imfNo += 1 275 | 276 | return IMF 277 | 278 | 279 | if __name__ == "__main__": 280 | print("Running example on BEMD") 281 | PLOT = True 282 | 283 | logging.basicConfig(level=logging.DEBUG) 284 | 285 | # Generate image 286 | print("Generating image... ", end="") 287 | rows, cols = 256, 256 288 | row_scale, col_scale = 256, 256 289 | x = np.arange(rows) / float(row_scale) 290 | y = np.arange(cols).reshape((-1, 1)) / float(col_scale) 291 | 292 | pi2 = 2 * np.pi 293 | img = np.zeros((rows, cols)) 294 | # img = img + np.sin(2*pi2*x)*np.cos(y*4*pi2+4*x*pi2) 295 | img = img + 3 * np.sin(2 * pi2 * x) + 2 296 | # img = img + 5*x*y + 2*(y-0.2)*y 297 | print("Done") 298 | 299 | # Perform decomposition 300 | print("Performing decomposition... ", end="") 301 | bemd = BEMD() 302 | # bemd.FIXE_H = 5 303 | IMFs = bemd.bemd(img, max_imf=3) 304 | imfNo = IMFs.shape[0] 305 | print("Done") 306 | 307 | if PLOT: 308 | print("Plotting results... ", end="") 309 | import pylab as plt 310 | 311 | # Save image for preview 312 | plt.figure(figsize=(4, 4 * (imfNo + 1))) 313 | plt.subplot(imfNo + 1, 1, 1) 314 | plt.imshow(img) 315 | plt.colorbar() 316 | plt.title("Input image") 317 | 318 | # Save reconstruction 319 | for n, imf in enumerate(IMFs): 320 | plt.subplot(imfNo + 1, 1, n + 2) 321 | plt.imshow(imf) 322 | plt.colorbar() 323 | plt.title("IMF %i" % (n + 1)) 324 | 325 | plt.savefig("image_decomp") 326 | print("Done") 327 | -------------------------------------------------------------------------------- /PyEMD/EEMD.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding: UTF-8 3 | # 4 | # Author: Dawid Laszuk 5 | # Contact: https://github.com/laszukdawid/PyEMD/issues 6 | # 7 | # Feel free to contact for any information. 8 | """ 9 | .. currentmodule:: EEMD 10 | """ 11 | 12 | import logging 13 | import os 14 | from collections import defaultdict 15 | from multiprocessing import Pool 16 | from typing import Dict, List, Optional, Sequence, Tuple, Union 17 | 18 | import numpy as np 19 | from tqdm import tqdm 20 | 21 | from PyEMD.utils import get_timeline 22 | 23 | 24 | class EEMD: 25 | """ 26 | **Ensemble Empirical Mode Decomposition** 27 | 28 | Ensemble empirical mode decomposition (EEMD) [Wu2009]_ 29 | is noise-assisted technique, which is meant to be more robust 30 | than simple Empirical Mode Decomposition (EMD). The robustness is 31 | checked by performing many decompositions on signals slightly 32 | perturbed from their initial position. In the grand average over 33 | all IMF results the noise will cancel each other out and the result 34 | is pure decomposition. 35 | 36 | Parameters 37 | ---------- 38 | trials : int (default: 100) 39 | Number of trials or EMD performance with added noise. 40 | noise_width : float (default: 0.05) 41 | Standard deviation of Gaussian noise (:math:`\hat\sigma`). 42 | It's relative to absolute amplitude of the signal, i.e. 43 | :math:`\hat\sigma = \sigma\cdot|\max(S)-\min(S)|`, where 44 | :math:`\sigma` is noise_width. 45 | ext_EMD : EMD (default: None) 46 | One can pass EMD object defined outside, which will be 47 | used to compute IMF decompositions in each trial. If none 48 | is passed then EMD with default options is used. 49 | parallel : bool (default: True) 50 | Flag whether to use multiprocessing in EEMD execution. 51 | Since each EMD(s+noise) is independent this improves execution 52 | speed considerably. Enabled by default as EEMD's embarrassingly 53 | parallel nature makes it almost always beneficial. 54 | processes : int or None (default: None) 55 | Number of processes harness when executing in parallel mode. 56 | If None, defaults to the number of CPUs available on the system. 57 | The value should be between 1 and max that depends on your hardware. 58 | separate_trends : bool (default: False) 59 | Flag whether to isolate trends from each EMD decomposition into a separate component. 60 | If `true`, the resulting EEMD will contain ensemble only from IMFs and 61 | the mean residue will be stacked as the last element. 62 | 63 | Performance Notes 64 | ----------------- 65 | EEMD runs multiple independent EMD decompositions on noise-perturbed signals, 66 | making it embarrassingly parallel. With ``parallel=True`` (default), each trial 67 | runs in a separate process, typically achieving near-linear speedup with CPU count. 68 | 69 | For very short signals (< 100 samples) or few trials (< 4), the multiprocessing 70 | overhead may exceed the benefit. In such cases, set ``parallel=False``. 71 | 72 | References 73 | ---------- 74 | .. [Wu2009] Z. Wu and N. E. Huang, "Ensemble empirical mode decomposition: 75 | A noise-assisted data analysis method", Advances in Adaptive 76 | Data Analysis, Vol. 1, No. 1 (2009) 1-41. 77 | """ 78 | 79 | logger = logging.getLogger(__name__) 80 | 81 | noise_kinds_all = ["normal", "uniform"] 82 | 83 | def __init__(self, trials: int = 100, noise_width: float = 0.05, ext_EMD=None, parallel: bool = True, **kwargs): 84 | # Ensemble constants 85 | self.trials = trials 86 | self.noise_width = noise_width 87 | self.separate_trends = bool(kwargs.get("separate_trends", False)) 88 | 89 | self.random = np.random.RandomState() 90 | self.noise_kind = kwargs.get("noise_kind", "normal") 91 | self.parallel = parallel 92 | self.processes = kwargs.get("processes") # Optional[int] 93 | # Auto-set processes to CPU count if parallel is enabled and processes not specified 94 | if self.parallel and self.processes is None: 95 | self.processes = os.cpu_count() 96 | if self.processes is not None and not self.parallel: 97 | self.logger.warning("Passed value for process has no effect when `parallel` is False.") 98 | 99 | if ext_EMD is None: 100 | from PyEMD import EMD 101 | 102 | self.EMD = EMD(**kwargs) 103 | else: 104 | self.EMD = ext_EMD 105 | 106 | self.E_IMF = None # Optional[np.ndarray] 107 | self.residue = None # Optional[np.ndarray] 108 | self._all_imfs = {} 109 | 110 | def __call__( 111 | self, S: np.ndarray, T: Optional[np.ndarray] = None, max_imf: int = -1, progress: bool = False 112 | ) -> np.ndarray: 113 | return self.eemd(S, T=T, max_imf=max_imf, progress=progress) 114 | 115 | def __getstate__(self) -> Dict: 116 | self_dict = self.__dict__.copy() 117 | return self_dict 118 | 119 | def generate_noise(self, scale: float, size: Union[int, Sequence[int]]) -> np.ndarray: 120 | """ 121 | Generate noise with specified parameters. 122 | Currently supported distributions are: 123 | 124 | * *normal* with std equal scale. 125 | * *uniform* with range [-scale/2, scale/2]. 126 | 127 | Parameters 128 | ---------- 129 | scale : float 130 | Width for the distribution. 131 | size : int 132 | Number of generated samples. 133 | 134 | Returns 135 | ------- 136 | noise : numpy array 137 | Noise sampled from selected distribution. 138 | """ 139 | if self.noise_kind == "normal": 140 | noise = self.random.normal(loc=0, scale=scale, size=size) 141 | elif self.noise_kind == "uniform": 142 | noise = self.random.uniform(low=-scale / 2, high=scale / 2, size=size) 143 | else: 144 | raise ValueError( 145 | "Unsupported noise kind. Please assigned `noise_kind` to be one of these: {0}".format( 146 | str(self.noise_kinds_all) 147 | ) 148 | ) 149 | return noise 150 | 151 | def noise_seed(self, seed: int) -> None: 152 | """Set seed for noise generation.""" 153 | self.random.seed(seed) 154 | 155 | def eemd( 156 | self, S: np.ndarray, T: Optional[np.ndarray] = None, max_imf: int = -1, progress: bool = False 157 | ) -> np.ndarray: 158 | """ 159 | Performs EEMD on provided signal. 160 | 161 | For a large number of iterations defined by `trials` attr 162 | the method performs :py:meth:`emd` on a signal with added white noise. 163 | 164 | Parameters 165 | ---------- 166 | S : numpy array, 167 | Input signal on which EEMD is performed. 168 | T : numpy array or None, (default: None) 169 | If none passed samples are numerated. 170 | max_imf : int, (default: -1) 171 | Defines up to how many IMFs each decomposition should 172 | be performed. By default (negative value) it decomposes 173 | all IMFs. 174 | 175 | Returns 176 | ------- 177 | eIMF : numpy array 178 | Set of ensemble IMFs produced from input signal. In general, 179 | these do not have to be, and most likely will not be, same as IMFs 180 | produced using EMD. 181 | """ 182 | if T is None: 183 | T = get_timeline(len(S), S.dtype) 184 | 185 | scale = self.noise_width * np.abs(np.max(S) - np.min(S)) 186 | self._S = S 187 | self._T = T 188 | self._N = len(S) 189 | self._scale = scale 190 | self.max_imf = max_imf 191 | 192 | # For trial number of iterations perform EMD on a signal 193 | # with added white noise 194 | if self.parallel: 195 | pool = Pool(processes=self.processes) 196 | map_pool = pool.map 197 | else: 198 | map_pool = map 199 | all_IMFs = map_pool(self._trial_update, range(self.trials)) 200 | 201 | if self.parallel: 202 | pool.close() 203 | 204 | self._all_imfs = defaultdict(list) 205 | it = iter if not progress else lambda x: tqdm(x, desc="EEMD", total=self.trials) 206 | for imfs, trend in it(all_IMFs): 207 | # A bit of explanation here. 208 | # If the `trend` is not None, that means it was intentionally separated in the decomp process. 209 | # This might due to `separate_trends` flag which means that trends are summed up separately 210 | # and treated as the last component. Since `proto_eimfs` is a dict, that `-1` is treated literally 211 | # and **not** as the *last position*. We can then use that `-1` to always add it as the last pos 212 | # in the actual eIMF, which indicates the trend. 213 | if trend is not None: 214 | self._all_imfs[-1].append(trend) 215 | 216 | for imf_num, imf in enumerate(imfs): 217 | self._all_imfs[imf_num].append(imf) 218 | 219 | # Convert defaultdict back to dict and explicitly rename `-1` position to be {the last value} for consistency. 220 | self._all_imfs = dict(self._all_imfs) 221 | if -1 in self._all_imfs: 222 | self._all_imfs[len(self._all_imfs)] = self._all_imfs.pop(-1) 223 | 224 | for imf_num in self._all_imfs.keys(): 225 | self._all_imfs[imf_num] = np.array(self._all_imfs[imf_num]) 226 | 227 | self.E_IMF = self.ensemble_mean() 228 | self.residue = S - np.sum(self.E_IMF, axis=0) 229 | 230 | return self.E_IMF 231 | 232 | def _trial_update(self, trial) -> Tuple[np.ndarray, Optional[np.ndarray]]: 233 | """A single trial evaluation, i.e. EMD(signal + noise). 234 | 235 | *Note*: Although `trial` argument isn't used it's needed for the (multiprocessing) map method. 236 | """ 237 | noise = self.generate_noise(self._scale, self._N) 238 | imfs = self.emd(self._S + noise, self._T, self.max_imf) 239 | trend = None 240 | if self.separate_trends: 241 | imfs, trend = self.EMD.get_imfs_and_trend() 242 | 243 | return (imfs, trend) 244 | 245 | def emd(self, S: np.ndarray, T: np.ndarray, max_imf: int = -1) -> np.ndarray: 246 | """Vanilla EMD method. 247 | 248 | Provides emd evaluation from provided EMD class. 249 | For reference please see :class:`PyEMD.EMD`. 250 | """ 251 | return self.EMD.emd(S, T, max_imf=max_imf) 252 | 253 | def get_imfs_and_residue(self) -> Tuple[np.ndarray, np.ndarray]: 254 | """ 255 | Provides access to separated imfs and residue from recently analysed signal. 256 | 257 | Returns 258 | ------- 259 | (imfs, residue) : (np.ndarray, np.ndarray) 260 | Tuple that contains all imfs and a residue (if any). 261 | 262 | """ 263 | if self.E_IMF is None or self.residue is None: 264 | raise ValueError("No IMF found. Please, run EMD method or its variant first.") 265 | return self.E_IMF, self.residue 266 | 267 | @property 268 | def all_imfs(self): 269 | """A dictionary with all computed imfs per given order.""" 270 | return self._all_imfs 271 | 272 | def ensemble_count(self) -> List[int]: 273 | """Count of imfs observed for given order, e.g. 1st proto-imf, in the whole ensemble.""" 274 | return [len(imfs) for imfs in self._all_imfs.values()] 275 | 276 | def ensemble_mean(self) -> np.ndarray: 277 | """Pointwise mean over computed ensemble. Same as the output of `eemd()` method.""" 278 | return np.array([imfs.mean(axis=0) for imfs in self._all_imfs.values()]) 279 | 280 | def ensemble_std(self) -> np.ndarray: 281 | """Pointwise standard deviation over computed ensemble.""" 282 | return np.array([imfs.std(axis=0) for imfs in self._all_imfs.values()]) 283 | 284 | 285 | ################################################### 286 | # Beginning of program 287 | if __name__ == "__main__": 288 | import pylab as plt 289 | 290 | E_imfNo = np.zeros(50, dtype=np.int) 291 | 292 | # Logging options 293 | logging.basicConfig(level=logging.INFO) 294 | 295 | # EEMD options 296 | max_imf = -1 297 | 298 | # Signal options 299 | N = 500 300 | tMin, tMax = 0, 2 * np.pi 301 | T = np.linspace(tMin, tMax, N) 302 | 303 | S = 3 * np.sin(4 * T) + 4 * np.cos(9 * T) + np.sin(8.11 * T + 1.2) 304 | 305 | # Prepare and run EEMD 306 | eemd = EEMD(trials=50) 307 | eemd.noise_seed(12345) 308 | 309 | E_IMFs = eemd.eemd(S, T, max_imf) 310 | imfNo = E_IMFs.shape[0] 311 | 312 | # Plot results in a grid 313 | c = np.floor(np.sqrt(imfNo + 1)) 314 | r = np.ceil((imfNo + 1) / c) 315 | 316 | plt.ioff() 317 | plt.subplot(r, c, 1) 318 | plt.plot(T, S, "r") 319 | plt.xlim((tMin, tMax)) 320 | plt.title("Original signal") 321 | 322 | for num in range(imfNo): 323 | plt.subplot(r, c, num + 2) 324 | plt.plot(T, E_IMFs[num], "g") 325 | plt.xlim((tMin, tMax)) 326 | plt.title("Imf " + str(num + 1)) 327 | 328 | plt.show() 329 | -------------------------------------------------------------------------------- /PyEMD/EMD2d.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding: UTF-8 3 | # 4 | # Author: Dawid Laszuk 5 | # Contact: https://github.com/laszukdawid/PyEMD/issues 6 | # 7 | # Edited: 07/07/2017 8 | # 9 | # Feel free to contact for any information. 10 | 11 | import logging 12 | 13 | import numpy as np 14 | 15 | try: 16 | from scipy.interpolate import SmoothBivariateSpline as SBS 17 | from scipy.ndimage.filters import maximum_filter 18 | from scipy.ndimage.morphology import binary_erosion, generate_binary_structure 19 | except ImportError: 20 | raise ImportError( 21 | "EMD2D and BEMD are not supported. Feel free to play around and improve them. " 22 | + "Required depdenecies are in `requriements-extra`." 23 | ) 24 | 25 | 26 | class EMD2D: 27 | """ 28 | **Empirical Mode Decomposition** on images. 29 | 30 | **Important** This is an experimental module. 31 | Experiments performed using this module didn't provide acceptable results, 32 | either in actual output nor in computation performance. The author is not 33 | an expert in image processing so it's very likely that the code could 34 | have been improved. Take your best shot. 35 | 36 | Method decomposes images into 2D representations of loose Intrinsic Mode 37 | Functions (IMFs). 38 | 39 | The current version of the algorithm detects local extrema, separately 40 | minima and maxima, and then connects them to create envelopes. These 41 | are then used to create a mean trend and subtracted from the input. 42 | 43 | Threshold values that control goodness of the decomposition: 44 | * `mse_thr` --- proto-IMF check whether small mean square error. 45 | * `mean_thr` --- proto-IMF chekc whether small mean value. 46 | """ 47 | 48 | logger = logging.getLogger(__name__) 49 | 50 | def __init__(self, **config): 51 | # ProtoIMF related 52 | self.mse_thr = 0.01 53 | self.mean_thr = 0.01 54 | 55 | self.FIXE = 0 56 | self.FIXE_H = 0 57 | 58 | self.MAX_ITERATION = 1000 59 | 60 | # Update based on options 61 | for key in config.keys(): 62 | if key in self.__dict__.keys(): 63 | self.__dict__[key] = config[key] 64 | 65 | def __call__(self, image, max_imf=-1): 66 | return self.emd(image, max_imf=max_imf) 67 | 68 | def extract_max_min_spline(self, image): 69 | """Calculates top and bottom envelopes for image. 70 | 71 | Parameters 72 | ---------- 73 | image : numpy 2D array 74 | 75 | Returns 76 | ------- 77 | min_env : numpy 2D array 78 | Bottom envelope in form of an image. 79 | max_env : numpy 2D array 80 | Top envelope in form of an image. 81 | """ 82 | 83 | big_image = self.prepare_image(image) 84 | big_min_peaks, big_max_peaks = self.find_extrema(big_image) 85 | 86 | # Prepare grid for interpolation. Doesn't seem necessary. 87 | xi = np.arange(image.shape[0], image.shape[0] * 2) 88 | yi = np.arange(image.shape[1], image.shape[1] * 2) 89 | 90 | big_min_image_val = big_image[big_min_peaks] 91 | big_max_image_val = big_image[big_max_peaks] 92 | min_env = self.spline_points(big_min_peaks[0], big_min_peaks[1], big_min_image_val, xi, yi) 93 | max_env = self.spline_points(big_max_peaks[0], big_max_peaks[1], big_max_image_val, xi, yi) 94 | 95 | return min_env, max_env 96 | 97 | @classmethod 98 | def prepare_image(cls, image): 99 | """Prepares image for edge extrapolation. 100 | Method bloats image by mirroring it along all axes. This turns 101 | extrapolation on edges into interpolation within bigger image. 102 | 103 | Parameters 104 | ---------- 105 | image : numpy 2D array 106 | Image for which interpolation is required, 107 | 108 | Returns 109 | ------- 110 | image : numpy 2D array 111 | Big image based on the input. Grid 3x3 where the center block is input and 112 | neighbouring panels are respective mirror images. 113 | """ 114 | 115 | # TODO: This is nasty. Instead of bloating whole image and then trying to 116 | # find all extrema, it's better to deal directly with indices. 117 | shape = image.shape 118 | big_image = np.zeros((shape[0] * 3, shape[1] * 3)) 119 | 120 | image_lr = np.fliplr(image) 121 | image_ud = np.flipud(image) 122 | image_ud_lr = np.flipud(image_lr) 123 | image_lr_ud = np.fliplr(image_ud) 124 | 125 | # Fill center with default image 126 | big_image[shape[0] : 2 * shape[0], shape[1] : 2 * shape[1]] = image 127 | 128 | # Fill left center 129 | big_image[shape[0] : 2 * shape[0], : shape[1]] = image_lr 130 | 131 | # Fill right center 132 | big_image[shape[0] : 2 * shape[0], 2 * shape[1] :] = image_lr 133 | 134 | # Fill center top 135 | big_image[: shape[0], shape[1] : shape[1] * 2] = image_ud 136 | 137 | # Fill center bottom 138 | big_image[2 * shape[0] :, shape[1] : 2 * shape[1]] = image_ud 139 | 140 | # Fill left top 141 | big_image[: shape[0], : shape[1]] = image_ud_lr 142 | 143 | # Fill left bottom 144 | big_image[2 * shape[0] :, : shape[1]] = image_ud_lr 145 | 146 | # Fill right top 147 | big_image[: shape[0], 2 * shape[1] :] = image_lr_ud 148 | 149 | # Fill right bottom 150 | big_image[2 * shape[0] :, 2 * shape[1] :] = image_lr_ud 151 | 152 | return big_image 153 | 154 | @classmethod 155 | def spline_points(cls, X, Y, Z, xi, yi): 156 | """Interpolates for given set of points""" 157 | 158 | # SBS requires at least m=(kx+1)*(ky+1) points, 159 | # where kx=ky=3 (default) is the degree of bivariate spline. 160 | # Thus, if less than 16=(3+1)*(3+1) points, adjust kx & ky. 161 | spline = SBS(X, Y, Z) 162 | 163 | return spline(xi, yi) 164 | 165 | @classmethod 166 | def find_extrema(cls, image): 167 | """ 168 | Finds extrema, both mininma and maxima, based on local maximum filter. 169 | Returns extrema in form of two rows, where the first and second are 170 | positions of x and y, respectively. 171 | 172 | Parameters 173 | ---------- 174 | image : numpy 2D array 175 | Monochromatic image or any 2D array. 176 | 177 | Returns 178 | ------- 179 | min_peaks : numpy array 180 | Minima positions. 181 | max_peaks : numpy array 182 | Maxima positions. 183 | """ 184 | 185 | # define an 3x3 neighborhood 186 | neighborhood = generate_binary_structure(2, 2) 187 | 188 | # apply the local maximum filter; all pixel of maximal value 189 | # in their neighborhood are set to 1 190 | local_min = maximum_filter(-image, footprint=neighborhood) == -image 191 | local_max = maximum_filter(image, footprint=neighborhood) == image 192 | 193 | # can't distinguish between background zero and filter zero 194 | background = image == 0 195 | 196 | # appear along the bg border (artifact of the local max filter) 197 | eroded_background = binary_erosion(background, structure=neighborhood, border_value=1) 198 | 199 | # we obtain the final mask, containing only peaks, 200 | # by removing the background from the local_max mask (xor operation) 201 | min_peaks = local_min ^ eroded_background 202 | max_peaks = local_max ^ eroded_background 203 | 204 | min_peaks = local_min 205 | max_peaks = local_max 206 | min_peaks[[0, -1], :] = False 207 | min_peaks[:, [0, -1]] = False 208 | max_peaks[[0, -1], :] = False 209 | max_peaks[:, [0, -1]] = False 210 | 211 | min_peaks = np.nonzero(min_peaks) 212 | max_peaks = np.nonzero(max_peaks) 213 | 214 | return min_peaks, max_peaks 215 | 216 | @classmethod 217 | def end_condition(cls, image, IMFs): 218 | """Determins whether decomposition should be stopped. 219 | 220 | Parameters 221 | ---------- 222 | image : numpy 2D array 223 | Input image which is decomposed. 224 | IMFs : numpy 3D array 225 | Array for which first dimensions relates to respective IMF, 226 | i.e. (numIMFs, imageX, imageY). 227 | """ 228 | rec = np.sum(IMFs, axis=0) 229 | 230 | # If reconstruction is perfect, no need for more tests 231 | if np.allclose(image, rec): 232 | return True 233 | 234 | return False 235 | 236 | def check_proto_imf(self, proto_imf, proto_imf_prev, mean_env): 237 | """Check whether passed (proto) IMF is actual IMF. 238 | Current condition is solely based on checking whether the mean is below threshold. 239 | 240 | Parameters 241 | ---------- 242 | proto_imf : numpy 2D array 243 | Current iteration of proto IMF. 244 | proto_imf_prev : numpy 2D array 245 | Previous iteration of proto IMF. 246 | mean_env : numpy 2D array 247 | Local mean computed from top and bottom envelopes. 248 | 249 | Returns 250 | ------- 251 | boolean 252 | Whether current proto IMF is actual IMF. 253 | """ 254 | 255 | # TODO: Sifiting is very sensitive and subtracting const val can often flip 256 | # maxima with minima in decompoisition and thus repeating above/below 257 | # behaviour. For now, mean_env is checked whether close to zero excluding 258 | # its offset. 259 | if np.all(np.abs(mean_env - mean_env.mean()) < self.mean_thr): 260 | # if np.all(np.abs(mean_env) 4 and len(max_peaks[0]) > 4: 328 | imf_old = imf.copy() 329 | imf = imf - mean_env 330 | 331 | min_env, max_env = self.extract_max_min_spline(imf) 332 | 333 | mean_env = 0.5 * (min_env + max_env) 334 | 335 | imf_old = imf.copy() 336 | imf = imf - mean_env 337 | 338 | # Fix number of iterations 339 | if self.FIXE: 340 | if n >= self.FIXE + 1: 341 | stop_sifting = True 342 | 343 | # Fix number of iterations after number of zero-crossings 344 | # and extrema differ at most by one. 345 | elif self.FIXE_H: 346 | if n == 1: 347 | continue 348 | if self.check_proto_imf(imf, imf_old, mean_env): 349 | n_h += 1 350 | else: 351 | n_h = 0 352 | 353 | # STOP if enough n_h 354 | if n_h >= self.FIXE_H: 355 | stop_sifting = True 356 | 357 | # Stops after default stopping criteria are met 358 | else: 359 | if self.check_proto_imf(imf, imf_old, mean_env): 360 | stop_sifting = True 361 | 362 | else: 363 | notFinished = False 364 | stop_sifting = True 365 | 366 | IMF = np.vstack((IMF, imf.copy()[None, :])) 367 | imfNo += 1 368 | 369 | if self.end_condition(image, IMF) or (max_imf > 0 and imfNo >= max_imf): 370 | notFinished = False 371 | break 372 | 373 | res = image_s - np.sum(IMF[:imfNo], axis=0) 374 | if not np.allclose(res, 0): 375 | IMF = np.vstack((IMF, res[None, :])) 376 | imfNo += 1 377 | 378 | IMF = IMF * scale 379 | IMF[-1] += offset 380 | return IMF 381 | 382 | 383 | ######################################## 384 | if __name__ == "__main__": 385 | print("Running example on EMD2D") 386 | PLOT = True 387 | 388 | logging.basicConfig(level=logging.DEBUG) 389 | 390 | # Generate image 391 | print("Generating image... ", end="") 392 | rows, cols = 1024, 1024 393 | row_scale, col_scale = 256, 256 394 | x = np.arange(rows) / float(row_scale) 395 | y = np.arange(cols).reshape((-1, 1)) / float(col_scale) 396 | 397 | pi2 = 2 * np.pi 398 | img = np.zeros((rows, cols)) 399 | img = img + np.sin(2 * pi2 * x) * np.cos(y * 4 * pi2 + 4 * x * pi2) 400 | img = img + 3 * np.sin(2 * pi2 * x) + 2 401 | img = img + 5 * x * y + 2 * (y - 0.2) * y 402 | print("Done") 403 | 404 | # Perform decomposition 405 | print("Performing decomposition... ", end="") 406 | emd2d = EMD2D() 407 | # emd2d.FIXE_H = 5 408 | IMFs = emd2d.emd(img, max_imf=4) 409 | imfNo = IMFs.shape[0] 410 | print("Done") 411 | 412 | if PLOT: 413 | print("Plotting results... ", end="") 414 | import pylab as plt 415 | 416 | # Save image for preview 417 | plt.figure(figsize=(4, 4 * (imfNo + 1))) 418 | plt.subplot(imfNo + 1, 1, 1) 419 | plt.imshow(img) 420 | plt.colorbar() 421 | plt.title("Input image") 422 | 423 | # Save reconstruction 424 | for n, imf in enumerate(IMFs): 425 | plt.subplot(imfNo + 1, 1, n + 2) 426 | plt.imshow(imf) 427 | plt.colorbar() 428 | plt.title("IMF %i" % (n + 1)) 429 | 430 | plt.savefig("image_decomp") 431 | print("Done") 432 | --------------------------------------------------------------------------------