├── pyampd ├── __init__.py └── ampd.py ├── ass_ampd.png ├── MANIFEST.in ├── .travis.yml ├── Pipfile ├── Makefile ├── LICENSE ├── setup.cfg ├── setup.py ├── .gitignore ├── tests └── test_ampd.py ├── README.md └── Pipfile.lock /pyampd/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ass_ampd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ig248/pyampd/HEAD/ass_ampd.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.py 2 | recursive-exclude * __pycache__ 3 | recursive-exclude * *.py[co] 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | cache: pip 3 | language: python 4 | python: 5 | - 3.6 6 | 7 | install: 8 | - pip install pipenv 9 | - make dev-install 10 | - pip install codecov 11 | 12 | script: 13 | - make lint 14 | - make test 15 | 16 | branches: 17 | only: 18 | - master 19 | 20 | after_success: 21 | - codecov -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [requires] 7 | python_version = "3.6" 8 | 9 | [packages] 10 | pyampd = {editable = true, path = "."} 11 | 12 | [dev-packages] 13 | mccabe = '*' 14 | pep8-naming = '*' 15 | flake8 = '*' 16 | pydocstyle = '*' 17 | mypy = '*' 18 | typeshed = '*' 19 | yapf = '*' 20 | isort = '*' 21 | pytest = '*' 22 | pytest-cov = '*' 23 | pytest-benchmark = '*' 24 | glob2 = '*' -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | pipenv install 3 | 4 | dev-install: 5 | pipenv install --dev 6 | 7 | yapf: 8 | pipenv run yapf -vv -ir . 9 | pipenv run isort -y 10 | 11 | lint: 12 | pipenv run flake8 . 13 | pipenv run pydocstyle . 14 | pipenv run mypy . 15 | 16 | clean: 17 | find . | grep -E '(__pycache__|\.pyc|\.pyo$$)' | xargs rm -rf 18 | 19 | test: 20 | pipenv run pytest --cov=. 21 | 22 | test-cov: 23 | pipenv run pytest --cov=. --cov-report html --cov-report term 24 | 25 | release: 26 | python setup.py sdist bdist_wheel 27 | twine upload dist/* 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Igor Gotlibovych 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | addopts = -x -s -v 3 | norecursedirs = .git src ai_platform .tox 4 | 5 | [flake8] 6 | exclude = .git,__pycache__,legacy,build,dist,.tox,models 7 | max-complexity = 10 8 | application-import-names=pyampd,tests 9 | ignore = T484,T499,D100,D101,D102,D103,D104,D105,D107,D200,N803,N806 10 | 11 | [mypy] 12 | warn_incomplete_stub = False 13 | incremental = True 14 | check_untyped_defs = False 15 | ignore_missing_imports = True 16 | 17 | [pydocstyle] 18 | add_ignore = D100,D101,D102,D103,D104,D105,D107,D200 19 | match_dir = (?!(legacy|tmp|docs|ja_docs|tests|\.)).* 20 | 21 | [yapf] 22 | based_on_style = pep8 23 | spaces_before_comment = 2 24 | split_before_logical_operator = true 25 | indent_width = 4 26 | split_complex_comprehension = true 27 | column_limit = 79 28 | dedent_closing_brackets = true 29 | spaces_around_power_operator = true 30 | no_spaces_around_selected_binary_operators = false 31 | split_penalty_import_names = 500 32 | join_multiple_lines = true 33 | 34 | [coverage:run] 35 | omit = 36 | tests/* 37 | src/* 38 | setup.py 39 | .tox/* 40 | dist/* 41 | **/__init__.py 42 | 43 | [bdist_wheel] 44 | universal = 1 45 | 46 | [zest.releaser] 47 | create-wheel = yes 48 | release = yes 49 | register = yes 50 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -* 3 | 4 | import os 5 | 6 | from setuptools import find_packages, setup 7 | 8 | install_requires = ['numpy', 'scipy'] 9 | 10 | VERSION = '0.0.1' 11 | 12 | # read the contents of your README file 13 | this_directory = os.path.abspath(os.path.dirname(__file__)) 14 | with open(os.path.join(this_directory, 'README.md')) as f: 15 | long_description = f.read() 16 | 17 | setup( 18 | name='pyampd', 19 | version=VERSION, 20 | description='Peak detection using AMPD and ASS-AMPD algorithms', 21 | url='https://github.com/ig248/pyampd', 22 | long_description=long_description, 23 | long_description_content_type='text/markdown', 24 | packages=find_packages(exclude=('tests', )), 25 | entry_points={'console_scripts': []}, 26 | include_package_data=True, 27 | author='Igor Gotlibovych', 28 | author_email='igor.gotlibovych@gmail.com', 29 | license='MIT', 30 | install_requires=install_requires, 31 | extras_require={}, 32 | classifiers=[ 33 | 'Intended Audience :: Developers', 34 | 'Operating System :: OS Independent', 35 | 'Programming Language :: Python', 36 | 'Programming Language :: Python :: 3', 37 | 'Programming Language :: Python :: 3.6', 38 | ], 39 | ) 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # https://github.com/github/gitignore/blob/master/Python.gitignore 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | .static_storage/ 59 | .media/ 60 | local_settings.py 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # PyCharm 97 | .idea 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | 112 | \.DS_Store 113 | -------------------------------------------------------------------------------- /tests/test_ampd.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import numpy.testing as npt 3 | import pytest 4 | 5 | from pyampd.ampd import find_peaks, find_peaks_adaptive 6 | 7 | 8 | def _gen_gaussian_peaks(len=100, locs=[50], sigma=1): 9 | """timeseries with `len` samples and peaks of width `sigma` at `locs`""" 10 | t = np.arange(len) 11 | x = np.zeros((len, )) 12 | for loc in locs: 13 | peak = np.exp(-(t - loc) ** 2 / (2 * sigma ** 2)) 14 | x += peak 15 | return x 16 | 17 | 18 | def signal_simple(): 19 | len = 101 20 | delta = 10 21 | sigma = 2 22 | locs = np.arange(delta, len - 1, delta) 23 | x = _gen_gaussian_peaks(len, locs, sigma) 24 | peaks = locs 25 | return x, peaks 26 | 27 | 28 | def signal_with_endpoints(): 29 | len = 101 30 | delta = 10 31 | sigma = 2 32 | locs = np.arange(0, len + 1, delta) 33 | x = _gen_gaussian_peaks(len, locs, sigma) 34 | peaks = locs 35 | return x, peaks 36 | 37 | 38 | def signal_multiscale(): 39 | locs = np.hstack([ 40 | np.arange(0, 100, 10), 41 | np.arange(100, 400, 30), 42 | ]) 43 | len = locs[-1] + 1 44 | x = _gen_gaussian_peaks(len, locs, sigma=2) 45 | peaks = locs 46 | return x, peaks 47 | 48 | 49 | @pytest.mark.parametrize('scale', [None, 100]) 50 | @pytest.mark.parametrize('signal', [signal_simple, signal_with_endpoints]) 51 | def test_ampd(signal, scale): 52 | x, known_peaks = signal() 53 | peaks = find_peaks(x, scale=scale) 54 | npt.assert_array_equal(peaks, known_peaks) 55 | 56 | 57 | @pytest.mark.parametrize( 58 | 'signal, window', [ 59 | (signal_simple, None), (signal_with_endpoints, None), 60 | (signal_simple, 100), (signal_with_endpoints, 100), 61 | (signal_multiscale, 100) 62 | ] 63 | ) 64 | def test_ass_ampd(signal, window): 65 | x, known_peaks = signal() 66 | peaks = find_peaks_adaptive(x, window=window) 67 | npt.assert_array_equal(peaks, known_peaks) 68 | 69 | 70 | if __name__ == '__main__': 71 | pytest.main() 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI version](https://badge.fury.io/py/pyampd.svg)](https://badge.fury.io/py/pyampd) 2 | [![Build Status](https://travis-ci.com/ig248/pyampd.svg?branch=master)](https://travis-ci.com/ig248/pyampd) 3 | [![Coverage Status](https://codecov.io/gh/ig248/pyampd/branch/master/graph/badge.svg)](https://codecov.io/gh/ig248/pyampd) 4 | 5 | # AMPD algorithm in Python 6 | Implements a function `find_peaks` based on the Automatic Multi-scale 7 | Peak Detection algorithm proposed by Felix Scholkmann et al. in 8 | "An Efficient Algorithm for Automatic Peak Detection in 9 | Noisy Periodic and Quasi-Periodic Signals", Algorithms 2012, 10 | 5, 588-603 11 | 12 | ![Peak finding](https://raw.githubusercontent.com/ig248/pyampd/master/ass_ampd.png) 13 | 14 | ## Usage 15 | Install from PyPI: 16 | 17 | ``` 18 | pip install pyampd 19 | ``` 20 | 21 | Or install from source: 22 | 23 | ``` 24 | pip install git+https://github.com/ig248/pyampd 25 | ``` 26 | 27 | Import function: 28 | 29 | ```python 30 | from pyampd.ampd import find_peaks 31 | ``` 32 | 33 | See `notebooks/ampd.ipynb` for usage examples. 34 | 35 | ### Specifying maximum scale 36 | To improve run-time on large time-series, it is possible to specify the maximum scale to consider: 37 | ```python 38 | peaks = find_peaks(x, scale=100) 39 | ``` 40 | will only consider windows up to +-100 point either side of peak candidates. 41 | 42 | ### Adaptive Scale Selection 43 | If the characteristic scale of the signal changes over time, a new algorithm called 44 | Adaptive Scale Selection can track the changes in optimal scales and detect peaks accordingly: 45 | ```python 46 | peaks = find_peaks_adaptive(x, window=200) 47 | ``` 48 | will select the optimal scale at each point using a 200-point running window. 49 | 50 | 51 | ### Original implementation 52 | `find_peaks` is not identical to the algorithm proposed in the original paper (especially near start and end of time series). 53 | A performance-optimized version of the original implementation is provided in `find_peaks_original`. 54 | 55 | 56 | ## Tests 57 | Run 58 | ```bash 59 | pytest 60 | ``` 61 | 62 | ## Other implementations 63 | - R: https://cran.r-project.org/web/packages/ampd/index.html 64 | - MATLAB: https://github.com/mathouse/AMPD-algorithm 65 | - Python: https://github.com/LucaCerina/ampdLib 66 | 67 | ## Improvements 68 | This Python implementation provides significant speed-ups in two areas: 69 | 1. Efficient tracking of local minima without using random numbers 70 | 2. Introduction of maximum window size, reducing algorithm run-time from 71 | quadratic to linear in the number of samples. 72 | 3. Better handling of peaks near start/end of the series 73 | 4. Addition of new Adaptive Scale Selection 74 | 75 | ## ToDo 76 | - It may be possible to avoid repeated comparisons, and reduce worst-case 77 | runtime from `O(n^2)` to `O(n log(n))`. 78 | - `find_peaks_adaptive` could benefit from specifying both `window` and `max_scale` 79 | 80 | ## References 81 | Original paper: https://doi.org/10.1109/ICRERA.2016.7884365 82 | -------------------------------------------------------------------------------- /pyampd/ampd.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.ndimage import uniform_filter1d 3 | from scipy.signal import detrend 4 | 5 | 6 | def find_peaks_original(x, scale=None, debug=False): 7 | """Find peaks in quasi-periodic noisy signals using AMPD algorithm. 8 | 9 | Automatic Multi-Scale Peak Detection originally proposed in 10 | "An Efficient Algorithm for Automatic Peak Detection in 11 | Noisy Periodic and Quasi-Periodic Signals", Algorithms 2012, 5, 588-603 12 | https://doi.org/10.1109/ICRERA.2016.7884365 13 | 14 | Optimized implementation by Igor Gotlibovych, 2018 15 | 16 | 17 | Parameters 18 | ---------- 19 | x : ndarray 20 | 1-D array on which to find peaks 21 | scale : int, optional 22 | specify maximum scale window size of (2 * scale + 1) 23 | debug : bool, optional 24 | if set to True, return the Local Scalogram Matrix, `LSM`, 25 | and scale with most local maxima, `l`, 26 | together with peak locations 27 | 28 | Returns 29 | ------- 30 | pks: ndarray 31 | The ordered array of peak indices found in `x` 32 | 33 | """ 34 | x = detrend(x) 35 | N = len(x) 36 | L = N // 2 37 | if scale: 38 | L = min(scale, L) 39 | 40 | # create LSM matix 41 | LSM = np.zeros((L, N), dtype=bool) 42 | for k in np.arange(1, L): 43 | LSM[k - 1, k:N - k] = ( 44 | (x[0:N - 2 * k] < x[k:N - k]) & (x[k:N - k] > x[2 * k:N]) 45 | ) 46 | 47 | # Find scale with most maxima 48 | G = LSM.sum(axis=1) 49 | l_scale = np.argmax(G) 50 | 51 | # find peaks that persist on all scales up to l 52 | pks_logical = np.min(LSM[0:l_scale, :], axis=0) 53 | pks = np.flatnonzero(pks_logical) 54 | if debug: 55 | return pks, LSM, l_scale 56 | return pks 57 | 58 | 59 | def find_peaks(x, scale=None, debug=False): 60 | """Find peaks in quasi-periodic noisy signals using AMPD algorithm. 61 | 62 | Extended implementation handles peaks near start/end of the signal. 63 | 64 | Optimized implementation by Igor Gotlibovych, 2018 65 | 66 | 67 | Parameters 68 | ---------- 69 | x : ndarray 70 | 1-D array on which to find peaks 71 | scale : int, optional 72 | specify maximum scale window size of (2 * scale + 1) 73 | debug : bool, optional 74 | if set to True, return the Local Scalogram Matrix, `LSM`, 75 | weigted number of maxima, 'G', 76 | and scale at which G is maximized, `l`, 77 | together with peak locations 78 | 79 | Returns 80 | ------- 81 | pks: ndarray 82 | The ordered array of peak indices found in `x` 83 | 84 | """ 85 | x = detrend(x) 86 | N = len(x) 87 | L = N // 2 88 | if scale: 89 | L = min(scale, L) 90 | 91 | # create LSM matix 92 | LSM = np.ones((L, N), dtype=bool) 93 | for k in np.arange(1, L + 1): 94 | LSM[k - 1, 0:N - k] &= (x[0:N - k] > x[k:N] 95 | ) # compare to right neighbours 96 | LSM[k - 1, k:N] &= (x[k:N] > x[0:N - k]) # compare to left neighbours 97 | 98 | # Find scale with most maxima 99 | G = LSM.sum(axis=1) 100 | G = G * np.arange( 101 | N // 2, N // 2 - L, -1 102 | ) # normalize to adjust for new edge regions 103 | l_scale = np.argmax(G) 104 | 105 | # find peaks that persist on all scales up to l 106 | pks_logical = np.min(LSM[0:l_scale, :], axis=0) 107 | pks = np.flatnonzero(pks_logical) 108 | if debug: 109 | return pks, LSM, G, l_scale 110 | return pks 111 | 112 | 113 | def find_peaks_adaptive(x, window=None, debug=False): 114 | """Find peaks in quasi-periodic noisy signals using ASS-AMPD algorithm. 115 | 116 | Adaptive Scale Selection Automatic Multi-Scale Peak Detection, 117 | an extension of AMPD - 118 | "An Efficient Algorithm for Automatic Peak Detection in 119 | Noisy Periodic and Quasi-Periodic Signals", Algorithms 2012, 5, 588-603 120 | https://doi.org/10.1109/ICRERA.2016.7884365 121 | 122 | Optimized implementation by Igor Gotlibovych, 2018 123 | 124 | 125 | Parameters 126 | ---------- 127 | x : ndarray 128 | 1-D array on which to find peaks 129 | window : int, optional 130 | sliding window size for adaptive scale selection 131 | debug : bool, optional 132 | if set to True, return the Local Scalogram Matrix, `LSM`, 133 | and `adaptive_scale`, 134 | together with peak locations 135 | 136 | Returns 137 | ------- 138 | pks: ndarray 139 | The ordered array of peak indices found in `x` 140 | 141 | """ 142 | x = detrend(x) 143 | N = len(x) 144 | if not window: 145 | window = N 146 | if window > N: 147 | window = N 148 | L = window // 2 149 | 150 | # create LSM matix 151 | LSM = np.ones((L, N), dtype=bool) 152 | for k in np.arange(1, L + 1): 153 | LSM[k - 1, 0:N - k] &= (x[0:N - k] > x[k:N] 154 | ) # compare to right neighbours 155 | LSM[k - 1, k:N] &= (x[k:N] > x[0:N - k]) # compare to left neighbours 156 | 157 | # Create continuos adaptive LSM 158 | ass_LSM = uniform_filter1d(LSM * window, window, axis=1, mode='nearest') 159 | normalization = np.arange(L, 0, -1) # scale normalization weight 160 | ass_LSM = ass_LSM * normalization.reshape(-1, 1) 161 | 162 | # Find adaptive scale at each point 163 | adaptive_scale = ass_LSM.argmax(axis=0) 164 | 165 | # construct reduced LSM 166 | LSM_reduced = LSM[:adaptive_scale.max(), :] 167 | mask = (np.indices(LSM_reduced.shape)[0] > adaptive_scale 168 | ) # these elements are outside scale of interest 169 | LSM_reduced[mask] = 1 170 | 171 | # find peaks that persist on all scales up to l 172 | pks_logical = np.min(LSM_reduced, axis=0) 173 | pks = np.flatnonzero(pks_logical) 174 | if debug: 175 | return pks, ass_LSM, adaptive_scale 176 | return pks 177 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "232be254122e4fc9ec776445b29eefa032859221e58e70758059e4731230f9d8" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "numpy": { 20 | "hashes": [ 21 | "sha256:0df89ca13c25eaa1621a3f09af4c8ba20da849692dcae184cb55e80952c453fb", 22 | "sha256:154c35f195fd3e1fad2569930ca51907057ae35e03938f89a8aedae91dd1b7c7", 23 | "sha256:18e84323cdb8de3325e741a7a8dd4a82db74fde363dce32b625324c7b32aa6d7", 24 | "sha256:1e8956c37fc138d65ded2d96ab3949bd49038cc6e8a4494b1515b0ba88c91565", 25 | "sha256:23557bdbca3ccbde3abaa12a6e82299bc92d2b9139011f8c16ca1bb8c75d1e95", 26 | "sha256:24fd645a5e5d224aa6e39d93e4a722fafa9160154f296fd5ef9580191c755053", 27 | "sha256:36e36b6868e4440760d4b9b44587ea1dc1f06532858d10abba98e851e154ca70", 28 | "sha256:3d734559db35aa3697dadcea492a423118c5c55d176da2f3be9c98d4803fc2a7", 29 | "sha256:416a2070acf3a2b5d586f9a6507bb97e33574df5bd7508ea970bbf4fc563fa52", 30 | "sha256:4a22dc3f5221a644dfe4a63bf990052cc674ef12a157b1056969079985c92816", 31 | "sha256:4d8d3e5aa6087490912c14a3c10fbdd380b40b421c13920ff468163bc50e016f", 32 | "sha256:4f41fd159fba1245e1958a99d349df49c616b133636e0cf668f169bce2aeac2d", 33 | "sha256:561ef098c50f91fbac2cc9305b68c915e9eb915a74d9038ecf8af274d748f76f", 34 | "sha256:56994e14b386b5c0a9b875a76d22d707b315fa037affc7819cda08b6d0489756", 35 | "sha256:73a1f2a529604c50c262179fcca59c87a05ff4614fe8a15c186934d84d09d9a5", 36 | "sha256:7da99445fd890206bfcc7419f79871ba8e73d9d9e6b82fe09980bc5bb4efc35f", 37 | "sha256:99d59e0bcadac4aa3280616591fb7bcd560e2218f5e31d5223a2e12a1425d495", 38 | "sha256:a4cc09489843c70b22e8373ca3dfa52b3fab778b57cf81462f1203b0852e95e3", 39 | "sha256:a61dc29cfca9831a03442a21d4b5fd77e3067beca4b5f81f1a89a04a71cf93fa", 40 | "sha256:b1853df739b32fa913cc59ad9137caa9cc3d97ff871e2bbd89c2a2a1d4a69451", 41 | "sha256:b1f44c335532c0581b77491b7715a871d0dd72e97487ac0f57337ccf3ab3469b", 42 | "sha256:b261e0cb0d6faa8fd6863af26d30351fd2ffdb15b82e51e81e96b9e9e2e7ba16", 43 | "sha256:c857ae5dba375ea26a6228f98c195fec0898a0fd91bcf0e8a0cae6d9faf3eca7", 44 | "sha256:cf5bb4a7d53a71bb6a0144d31df784a973b36d8687d615ef6a7e9b1809917a9b", 45 | "sha256:db9814ff0457b46f2e1d494c1efa4111ca089e08c8b983635ebffb9c1573361f", 46 | "sha256:df04f4bad8a359daa2ff74f8108ea051670cafbca533bb2636c58b16e962989e", 47 | "sha256:ecf81720934a0e18526177e645cbd6a8a21bb0ddc887ff9738de07a1df5c6b61", 48 | "sha256:edfa6fba9157e0e3be0f40168eb142511012683ac3dc82420bee4a3f3981b30e" 49 | ], 50 | "version": "==1.15.4" 51 | }, 52 | "pyampd": { 53 | "editable": true, 54 | "path": "." 55 | }, 56 | "scipy": { 57 | "hashes": [ 58 | "sha256:02cb79ea38114dc480e9b08d6b87095728e8fb39b9a49b449ee443d678001611", 59 | "sha256:03c827cdbc584e935264040b958e5fa0570a16095bee23a013482ba3f0e963a2", 60 | "sha256:04f2b23258139c109d0524f111597dd095a505d9cb2c71e381d688d653877fa3", 61 | "sha256:3132a9fab3f3545c8b0ba15688d11857efdd4a58d388d3785dc474f56fea7138", 62 | "sha256:4b1f0883cb9d8ee963cf0a31c87341e9e758abb2cf1e5bcc0d7b066ef6b17573", 63 | "sha256:4cce25c6e7ff7399c67dfe1b5423c36c391cf9b4b2be390c1675ab410f1ef503", 64 | "sha256:51a2424c8ed80e60bdb9a896806e7adaf24a58253b326fbad10f80a6d06f2214", 65 | "sha256:5706b785ca289fdfd91aa05066619e51d140613b613e35932601f2315f5d8470", 66 | "sha256:58f0435f052cb60f1472c77f52a8f6642f8877b70559e5e0b9a1744f33f5cbe5", 67 | "sha256:63e1d5ca9e5e1984f1a275276991b036e25d39d37dd7edbb3f4f6165c2da7dbb", 68 | "sha256:64b2c35824da3ef6bb1e722216e4ef28802af6413c7586136500e343d34ba179", 69 | "sha256:6f791987899532305126309578727c0197bddbafab9596bafe3e7bfab6e1ce13", 70 | "sha256:72bd766f753fd32f072d30d7bc2ad492d56dbcbf3e13764e27635d5c41079339", 71 | "sha256:7413080b381766a22d52814edb65631f0e323a7cea945c70021a672f38acc73f", 72 | "sha256:78a67ee4845440e81cfbfabde20537ca12051d0eeac951fe4c6d8751feac3103", 73 | "sha256:7994c044bf659b0a24ad7673ec7db85c2fadb87e4980a379a9fd5b086fe3649a", 74 | "sha256:7dc4002f0a0a688774ef04878afe769ecd1ac21fe9b4b1d7125e2cf16170afd3", 75 | "sha256:854bd87cc23824d5db4983956bc30f3790e1c7448f1a9e6a8fb7bff7601aef87", 76 | "sha256:8608316d0cc01f8b25111c8adfe6efbc959cbba037a62c784551568d7ffbf280", 77 | "sha256:8f5fcc87b48fc3dd6d901669c89af4feeb856dffb6f671539a238b7e8af1799c", 78 | "sha256:937147086e8b4338bf139ca8fa98da650e3a46bf2ca617f8e9dd68c3971ec420", 79 | "sha256:bc294841f6c822714af362095b181a853f47ed5ce757354bd2e4776d579ff3a4", 80 | "sha256:bc6a88b0009a1b60eab5c22ac3a006f6968d6328de10c6a64ebb0d64a05548d3", 81 | "sha256:c5eae911cf26b3c7eda889ec98d3c226f312c587acfaaf02602473f98b4c16d6", 82 | "sha256:cca33a01a5987c650b87a1a910aa311ffa44e67cca1ff502ef9efdae5d9a8624", 83 | "sha256:d1ae77b79fd5e27a10ba7c4e7c3a62927b9d29a4dccf28f6905c25d983aaf183", 84 | "sha256:fb36064047e6bf87b6320a9b6eb7f525ef6863c7a4aef1a84a4bbfb043612617", 85 | "sha256:fc1a19d95649439dbd50baca676bceb29bbfcd600aed2c5bd71d9bad82a87cfe" 86 | ], 87 | "version": "==1.2.0" 88 | } 89 | }, 90 | "develop": { 91 | "atomicwrites": { 92 | "hashes": [ 93 | "sha256:0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", 94 | "sha256:ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee" 95 | ], 96 | "version": "==1.2.1" 97 | }, 98 | "attrs": { 99 | "hashes": [ 100 | "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", 101 | "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" 102 | ], 103 | "version": "==18.2.0" 104 | }, 105 | "coverage": { 106 | "hashes": [ 107 | "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f", 108 | "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe", 109 | "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d", 110 | "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0", 111 | "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607", 112 | "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d", 113 | "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b", 114 | "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3", 115 | "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e", 116 | "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815", 117 | "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36", 118 | "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1", 119 | "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14", 120 | "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c", 121 | "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794", 122 | "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b", 123 | "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840", 124 | "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd", 125 | "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82", 126 | "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952", 127 | "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389", 128 | "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f", 129 | "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4", 130 | "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da", 131 | "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647", 132 | "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d", 133 | "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42", 134 | "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478", 135 | "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b", 136 | "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb", 137 | "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9" 138 | ], 139 | "version": "==4.5.2" 140 | }, 141 | "flake8": { 142 | "hashes": [ 143 | "sha256:6a35f5b8761f45c5513e3405f110a86bea57982c3b75b766ce7b65217abe1670", 144 | "sha256:c01f8a3963b3571a8e6bd7a4063359aff90749e160778e03817cd9b71c9e07d2" 145 | ], 146 | "index": "pypi", 147 | "version": "==3.6.0" 148 | }, 149 | "flake8-polyfill": { 150 | "hashes": [ 151 | "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9", 152 | "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda" 153 | ], 154 | "version": "==1.0.2" 155 | }, 156 | "glob2": { 157 | "hashes": [ 158 | "sha256:f5b0a686ff21f820c4d3f0c4edd216704cea59d79d00fa337e244a2f2ff83ed6" 159 | ], 160 | "index": "pypi", 161 | "version": "==0.6" 162 | }, 163 | "isort": { 164 | "hashes": [ 165 | "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", 166 | "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", 167 | "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" 168 | ], 169 | "index": "pypi", 170 | "version": "==4.3.4" 171 | }, 172 | "mccabe": { 173 | "hashes": [ 174 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 175 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 176 | ], 177 | "index": "pypi", 178 | "version": "==0.6.1" 179 | }, 180 | "more-itertools": { 181 | "hashes": [ 182 | "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092", 183 | "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e", 184 | "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d" 185 | ], 186 | "version": "==4.3.0" 187 | }, 188 | "mypy": { 189 | "hashes": [ 190 | "sha256:12d965c9c4e8a625673aec493162cf390e66de12ef176b1f4821ac00d55f3ab3", 191 | "sha256:38d5b5f835a81817dcc0af8d155bce4e9aefa03794fe32ed154d6612e83feafa" 192 | ], 193 | "index": "pypi", 194 | "version": "==0.650" 195 | }, 196 | "mypy-extensions": { 197 | "hashes": [ 198 | "sha256:37e0e956f41369209a3d5f34580150bcacfabaa57b33a15c0b25f4b5725e0812", 199 | "sha256:b16cabe759f55e3409a7d231ebd2841378fb0c27a5d1994719e340e4f429ac3e" 200 | ], 201 | "version": "==0.4.1" 202 | }, 203 | "pep8-naming": { 204 | "hashes": [ 205 | "sha256:360308d2c5d2fff8031c1b284820fbdb27a63274c0c1a8ce884d631836da4bdd", 206 | "sha256:624258e0dd06ef32a9daf3c36cc925ff7314da7233209c5b01f7e5cdd3c34826" 207 | ], 208 | "index": "pypi", 209 | "version": "==0.7.0" 210 | }, 211 | "pluggy": { 212 | "hashes": [ 213 | "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095", 214 | "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f" 215 | ], 216 | "version": "==0.8.0" 217 | }, 218 | "py": { 219 | "hashes": [ 220 | "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694", 221 | "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6" 222 | ], 223 | "version": "==1.7.0" 224 | }, 225 | "py-cpuinfo": { 226 | "hashes": [ 227 | "sha256:6615d4527118d4ea1db4d86dac4340725b3906aa04bf36b7902f7af4425fb25f" 228 | ], 229 | "version": "==4.0.0" 230 | }, 231 | "pycodestyle": { 232 | "hashes": [ 233 | "sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83", 234 | "sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a" 235 | ], 236 | "version": "==2.4.0" 237 | }, 238 | "pydocstyle": { 239 | "hashes": [ 240 | "sha256:2258f9b0df68b97bf3a6c29003edc5238ff8879f1efb6f1999988d934e432bd8", 241 | "sha256:5741c85e408f9e0ddf873611085e819b809fca90b619f5fd7f34bd4959da3dd4", 242 | "sha256:ed79d4ec5e92655eccc21eb0c6cf512e69512b4a97d215ace46d17e4990f2039" 243 | ], 244 | "index": "pypi", 245 | "version": "==3.0.0" 246 | }, 247 | "pyflakes": { 248 | "hashes": [ 249 | "sha256:9a7662ec724d0120012f6e29d6248ae3727d821bba522a0e6b356eff19126a49", 250 | "sha256:f661252913bc1dbe7fcfcbf0af0db3f42ab65aabd1a6ca68fe5d466bace94dae" 251 | ], 252 | "version": "==2.0.0" 253 | }, 254 | "pytest": { 255 | "hashes": [ 256 | "sha256:f689bf2fc18c4585403348dd56f47d87780bf217c53ed9ae7a3e2d7faa45f8e9", 257 | "sha256:f812ea39a0153566be53d88f8de94839db1e8a05352ed8a49525d7d7f37861e9" 258 | ], 259 | "index": "pypi", 260 | "version": "==4.0.2" 261 | }, 262 | "pytest-benchmark": { 263 | "hashes": [ 264 | "sha256:185526b10b7cf1804cb0f32ac0653561ef2f233c6e50a9b3d8066a9757e36480", 265 | "sha256:3549545f1a051a789d956a4a9b176583cd6b847e621b788471e6c04b7d8d0e3c" 266 | ], 267 | "index": "pypi", 268 | "version": "==3.1.1" 269 | }, 270 | "pytest-cov": { 271 | "hashes": [ 272 | "sha256:513c425e931a0344944f84ea47f3956be0e416d95acbd897a44970c8d926d5d7", 273 | "sha256:e360f048b7dae3f2f2a9a4d067b2dd6b6a015d384d1577c994a43f3f7cbad762" 274 | ], 275 | "index": "pypi", 276 | "version": "==2.6.0" 277 | }, 278 | "six": { 279 | "hashes": [ 280 | "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", 281 | "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" 282 | ], 283 | "version": "==1.12.0" 284 | }, 285 | "snowballstemmer": { 286 | "hashes": [ 287 | "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128", 288 | "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89" 289 | ], 290 | "version": "==1.2.1" 291 | }, 292 | "typed-ast": { 293 | "hashes": [ 294 | "sha256:0555eca1671ebe09eb5f2176723826f6f44cca5060502fea259de9b0e893ab53", 295 | "sha256:0ca96128ea66163aea13911c9b4b661cb345eb729a20be15c034271360fc7474", 296 | "sha256:16ccd06d614cf81b96de42a37679af12526ea25a208bce3da2d9226f44563868", 297 | "sha256:1e21ae7b49a3f744958ffad1737dfbdb43e1137503ccc59f4e32c4ac33b0bd1c", 298 | "sha256:37670c6fd857b5eb68aa5d193e14098354783b5138de482afa401cc2644f5a7f", 299 | "sha256:46d84c8e3806619ece595aaf4f37743083f9454c9ea68a517f1daa05126daf1d", 300 | "sha256:5b972bbb3819ece283a67358103cc6671da3646397b06e7acea558444daf54b2", 301 | "sha256:6306ffa64922a7b58ee2e8d6f207813460ca5a90213b4a400c2e730375049246", 302 | "sha256:6cb25dc95078931ecbd6cbcc4178d1b8ae8f2b513ae9c3bd0b7f81c2191db4c6", 303 | "sha256:7e19d439fee23620dea6468d85bfe529b873dace39b7e5b0c82c7099681f8a22", 304 | "sha256:7f5cd83af6b3ca9757e1127d852f497d11c7b09b4716c355acfbebf783d028da", 305 | "sha256:81e885a713e06faeef37223a5b1167615db87f947ecc73f815b9d1bbd6b585be", 306 | "sha256:94af325c9fe354019a29f9016277c547ad5d8a2d98a02806f27a7436b2da6735", 307 | "sha256:b1e5445c6075f509d5764b84ce641a1535748801253b97f3b7ea9d948a22853a", 308 | "sha256:cb061a959fec9a514d243831c514b51ccb940b58a5ce572a4e209810f2507dcf", 309 | "sha256:cc8d0b703d573cbabe0d51c9d68ab68df42a81409e4ed6af45a04a95484b96a5", 310 | "sha256:da0afa955865920edb146926455ec49da20965389982f91e926389666f5cf86a", 311 | "sha256:dc76738331d61818ce0b90647aedde17bbba3d3f9e969d83c1d9087b4f978862", 312 | "sha256:e7ec9a1445d27dbd0446568035f7106fa899a36f55e52ade28020f7b3845180d", 313 | "sha256:f741ba03feb480061ab91a465d1a3ed2d40b52822ada5b4017770dfcb88f839f", 314 | "sha256:fe800a58547dd424cd286b7270b967b5b3316b993d86453ede184a17b5a6b17d" 315 | ], 316 | "version": "==1.1.1" 317 | }, 318 | "typeshed": { 319 | "hashes": [ 320 | "sha256:097c3f643fb754d38b0538c1d9d2b49f04e68ab6ea53196171c8663d9c473211", 321 | "sha256:5a9253eb9e9beaa54ee5aa4e41f1ba1af15ffcd647d0a27e22239b699626d07d" 322 | ], 323 | "index": "pypi", 324 | "version": "==0.0.1" 325 | }, 326 | "yapf": { 327 | "hashes": [ 328 | "sha256:8aa7f9abdb97b4da4d3227306b88477982daafef0a96cc41639754ca31f46d55", 329 | "sha256:f2df5891481f94ddadfbf8ae8ae499080752cfb06005a31bbb102f3012f8b944" 330 | ], 331 | "index": "pypi", 332 | "version": "==0.25.0" 333 | } 334 | } 335 | } 336 | --------------------------------------------------------------------------------