├── .gitignore ├── img ├── multidico.png └── audio4spikegram.png ├── .github ├── dependabot.yml └── workflows │ └── test_lint.yml ├── setup.cfg ├── .coveragerc ├── mdla ├── __init__.py └── dict_metrics.py ├── CITATION.cff ├── pyproject.toml ├── .pre-commit-config.yaml ├── README.md ├── tests ├── test_dict_metrics.py └── test_mdla.py ├── experiments ├── experiment_bci_competition.py ├── experiment_dictionary_recovering.py └── experiment_multivariate_recovering.py ├── examples ├── plot_bci_dict.py ├── example_benchmark_performance.py ├── example_multivariate.py ├── example_sparse_decomposition.py └── example_univariate.py ├── requirements.txt └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_STORE 2 | **/__pycache__ 3 | -------------------------------------------------------------------------------- /img/multidico.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sylvchev/mdla/HEAD/img/multidico.png -------------------------------------------------------------------------------- /img/audio4spikegram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sylvchev/mdla/HEAD/img/audio4spikegram.png -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203, E501, W503, W605, B950, C901 3 | max-line-length = 90 4 | max-complexity = 12 5 | select = B, C, E, F, W, B9 6 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = 4 | mdla 5 | dict_metrics 6 | omit = 7 | */tests/*.py 8 | disable_warnings = 9 | module-not-imported 10 | no-data-collected 11 | -------------------------------------------------------------------------------- /mdla/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from .mdla import ( 3 | MiniBatchMultivariateDictLearning, 4 | MultivariateDictLearning, 5 | SparseMultivariateCoder, 6 | multivariate_sparse_encode, 7 | reconstruct_from_code, 8 | ) 9 | -------------------------------------------------------------------------------- /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: "Sylvain" 5 | given-names: "Chevallier" 6 | orcid: "https://orcid.org/0000-0003-3027-8241" 7 | title: "Multivariate Dictionary Learning Algorithm" 8 | version: 1.0.3 9 | doi: 10.5281/zenodo.8434917 10 | date-released: 2023-10-12 11 | url: "https://github.com/sylvchev/mdla" 12 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 90 3 | target-version = ["py39"] 4 | 5 | [tool.isort] 6 | src_paths = ["mdla"] 7 | profile = "black" 8 | line_length = 90 9 | lines_after_imports = 2 10 | 11 | [tool.poetry] 12 | name = "mdla" 13 | version = "1.0.3" 14 | description = "Multivariate Dictionary Learning Algorithm" 15 | authors = ["Sylvain Chevallier "] 16 | readme = "README.md" 17 | repository = "https://github.com/sylvchev/mdla" 18 | documentation = "http://github.com/sylvchev/mdla" 19 | keywords = ["sparse decomposition", "dictionary learning", "multivariate signal", "eeg"] 20 | license = "BSD-3-Clause" 21 | classifiers = [ 22 | "Intended Audience :: Developers", 23 | "Intended Audience :: Science/Research", 24 | "Topic :: Scientific/Engineering :: Artificial Intelligence", 25 | ] 26 | 27 | [tool.poetry.dependencies] 28 | python = ">=3.9,<3.13" 29 | numpy = "^1.26.0" 30 | scipy = "^1.11.3" 31 | scikit-learn = "^1.3.1" 32 | matplotlib = "^3.8.0" 33 | cvxopt = "^1.3.2" 34 | 35 | [tool.poetry.group.dev.dependencies] 36 | pre-commit = "^3.4.0" 37 | pytest = "^7.4.2" 38 | pytest-cov = "^4.1.0" 39 | 40 | [build-system] 41 | requires = ["poetry-core>=1.0.0"] 42 | build-backend = "poetry.core.masonry.api" 43 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v3.2.0 4 | hooks: 5 | - id: check-yaml 6 | - id: check-added-large-files 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | - id: check-case-conflict 10 | - id: mixed-line-ending 11 | 12 | - repo: https://github.com/psf/black 13 | rev: 23.9.1 14 | hooks: 15 | - id: black 16 | language_version: python3.9 17 | 18 | - repo: https://github.com/PyCQA/isort 19 | rev: 5.12.0 20 | hooks: 21 | - id: isort 22 | 23 | - repo: https://github.com/PyCQA/flake8 24 | rev: 6.1.0 25 | hooks: 26 | - id: flake8 27 | additional_dependencies: 28 | [ 29 | "flake8-blind-except", 30 | "flake8-docstrings", 31 | "flake8-bugbear", 32 | "flake8-comprehensions", 33 | "flake8-docstrings", 34 | "flake8-implicit-str-concat", 35 | "pydocstyle>=5.0.0", 36 | ] 37 | exclude: ^docs/ | ^setup\.py$ | 38 | 39 | - repo: https://github.com/pre-commit/mirrors-prettier 40 | rev: v2.2.1 41 | hooks: 42 | - id: prettier 43 | args: [--print-width=90, --prose-wrap=always] 44 | -------------------------------------------------------------------------------- /.github/workflows/test_lint.yml: -------------------------------------------------------------------------------- 1 | name: Test-and-Lint 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test-and-lint: 11 | name: test-py-${{ matrix.python_version }} 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: true 15 | matrix: 16 | python_version: [3.9] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: "Setup Python" 22 | uses: actions/setup-python@v4 23 | with: 24 | python-version: ${{ matrix.python_version }} 25 | 26 | - uses: pre-commit/action@v3.0.0 27 | 28 | - name: Install Poetry 29 | uses: snok/install-poetry@v1 30 | with: 31 | virtualenvs-create: true 32 | virtualenvs-in-project: true 33 | 34 | - name: Install MDLA 35 | run: poetry install --no-interaction --with dev 36 | 37 | - name: "Run tests" 38 | shell: bash 39 | run: | 40 | poetry run pytest --cov=./ --cov-report=xml 41 | 42 | - name: "Coverage" 43 | uses: codecov/codecov-action@v3 44 | with: 45 | files: ./coverage.xml 46 | flags: unittests 47 | name: codecov-umbrella 48 | verbose: true 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MDLA - Multivariate Dictionary Learning Algorithm 2 | 3 | [![Build Status](https://github.com/sylvchev/mdla/workflows/Test-and-Lint/badge.svg)](https://github.com/sylvchev/mdla/actions?query=branch%3Amaster) 4 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 5 | [![codecov](https://codecov.io/gh/sylvchev/mdla/branch/master/graph/badge.svg?token=Vba6g9c5pM)](https://codecov.io/gh/sylvchev/mdla) 6 | 7 | ## Dictionary Learning for the multivariate dataset 8 | 9 | This dictionary learning variant is tailored for dealing with multivariate datasets and 10 | especially timeseries, where samples are matrices and the dataset is seen as a tensor. 11 | Dictionary Learning Algorithm (DLA) decompose input vector on a dictionary matrix with a 12 | sparse coefficient vector, see (a) on figure below. To handle multivariate data, a first 13 | approach called **multichannel DLA**, see (b) on figure below, is to decompose the matrix 14 | vector on a dictionary matrix but with sparse coefficient matrices, assuming that a 15 | multivariate sample could be seen as a collection of channels explained by the same 16 | dictionary. Nonetheless, multichannel DLA breaks the "spatial" coherence of multivariate 17 | samples, discarding the column-wise relationship existing in the samples. **Multivariate 18 | DLA**, (c), on figure below, decompose the matrix input on a tensor dictionary, where each 19 | atom is a matrix, with sparse coefficient vectors. In this case, the spatial relationship 20 | are directly encoded in the dictionary, as each atoms has the same dimension than an input 21 | samples. 22 | 23 | ![dictionaries](https://github.com/sylvchev/mdla/raw/master/img/multidico.png) 24 | 25 | (figure from [Chevallier et al., 2014](#biblio) ) 26 | 27 | To handle timeseries, two major modifications are brought to DLA: 28 | 29 | 1. extension to **multivariate** samples 30 | 2. **shift-invariant** approach, The first point is explained above. To implement the 31 | second one, there is two possibility, either slicing the input timeseries into small 32 | overlapping samples or to have atoms smaller than input samples, leading to a 33 | decomposition with sparse coefficients and offsets. In the latter case, the 34 | decomposition could be seen as sequence of kernels occuring at different time steps. 35 | 36 | ![shift invariance](https://github.com/sylvchev/mdla/raw/master/img/audio4spikegram.png) 37 | 38 | (figure from [Smith & Lewicki, 2005](#biblio)) 39 | 40 | The proposed implementation is an adaptation of the work of the following authors: 41 | 42 | - Q. Barthélemy, A. Larue, A. Mayoue, D. Mercier, and J.I. Mars. _Shift & 2D rotation 43 | invariant sparse coding for multi- variate signal_. IEEE Trans. Signal Processing, 44 | 60:1597–1611, 2012. 45 | - Q. Barthélemy, A. Larue, and J.I. Mars. _Decomposition and dictionary learning for 3D 46 | trajectories_. Signal Process., 98:423–437, 2014. 47 | - Q. Barthélemy, C. Gouy-Pailler, Y. Isaac, A. Souloumiac, A. Larue, and J.I. Mars. 48 | _Multivariate temporal dictionary learning for EEG_. Journal of Neuroscience Methods, 49 | 215:19–28, 2013. 50 | 51 | ## Dependencies 52 | 53 | The only dependencies are scikit-learn, matplotlib, numpy and scipy. 54 | 55 | No installation is required. 56 | 57 | ## Example 58 | 59 | A straightforward example is: 60 | 61 | ```python 62 | import numpy as np 63 | from mdla import MultivariateDictLearning 64 | from mdla import multivariate_sparse_encode 65 | from numpy.linalg import norm 66 | 67 | rng_global = np.random.RandomState(0) 68 | n_samples, n_features, n_dims = 10, 5, 3 69 | X = rng_global.randn(n_samples, n_features, n_dims) 70 | 71 | n_kernels = 8 72 | dico = MultivariateDictLearning(n_kernels=n_kernels, max_iter=10).fit(X) 73 | residual, code = multivariate_sparse_encode(X, dico) 74 | print ('Objective error for each samples is:') 75 | for i in range(len(residual)): 76 | print ('Sample', i, ':', norm(residual[i], 'fro') + len(code[i])) 77 | ``` 78 | 79 | ## Bibliography 80 | 81 | - Chevallier, S., Barthelemy, Q., & Atif, J. (2014). [_Subspace metrics for multivariate 82 | dictionaries and application to EEG_][1]. In Acoustics, Speech and Signal Processing 83 | (ICASSP), IEEE International Conference on (pp. 7178-7182). 84 | - Smith, E., & Lewicki, M. S. (2005). [_Efficient coding of time-relative structure using 85 | spikes_][2]. Neural Computation, 17(1), 19-45 86 | - Chevallier, S., Barthélemy, Q., & Atif, J. (2014). [_On the need for metrics in 87 | dictionary learning assessment_][3]. In European Signal Processing Conference (EUSIPCO), 88 | pp. 1427-1431. 89 | 90 | [1]: http://dx.doi.org/10.1109/ICASSP.2014.6854993 "Chevallier et al., 2014" 91 | [2]: http://dl.acm.org/citation.cfm?id=1119614 "Smith and Lewicki, 2005" 92 | [3]: https://hal-uvsq.archives-ouvertes.fr/hal-01352054/document 93 | -------------------------------------------------------------------------------- /tests/test_dict_metrics.py: -------------------------------------------------------------------------------- 1 | from numpy import NaN, allclose, arange, concatenate, ones, zeros 2 | from numpy.linalg import norm 3 | from numpy.random import randn 4 | from numpy.testing import assert_almost_equal, assert_equal, assert_raises 5 | 6 | from mdla.dict_metrics import ( 7 | beta_dist, 8 | detection_rate, 9 | emd, 10 | hausdorff, 11 | precision_recall, 12 | precision_recall_points, 13 | ) 14 | 15 | 16 | n_kernels, n_features, n_dims = 10, 5, 3 17 | dm = [randn(n_features, n_dims) for i in range(n_kernels)] 18 | for i in range(len(dm)): 19 | dm[i] /= norm(dm[i], "fro") 20 | du = [randn(n_features, 1) for i in range(n_kernels)] 21 | for i in range(len(du)): 22 | du[i] /= norm(du[i]) 23 | 24 | gdm = [ 25 | "chordal", 26 | "chordal_principal_angles", 27 | "fubinistudy", 28 | "binetcauchy", 29 | "geodesic", 30 | "frobenius", 31 | ] 32 | gdu = ["abs_euclidean", "euclidean"] 33 | dist = [ 34 | hausdorff, 35 | emd, 36 | detection_rate, 37 | precision_recall, 38 | precision_recall_points, 39 | beta_dist, 40 | ] 41 | 42 | 43 | def test_scale(): 44 | for m in [hausdorff, emd]: 45 | for g in gdm: 46 | print("for", g, ":") 47 | assert_almost_equal(0.0, m(dm, dm, g, scale=True)) 48 | for g in gdu: 49 | print("for", g, ":") 50 | assert_almost_equal(0.0, m(du, du, g, scale=True)) 51 | 52 | 53 | def test_kernel_registration(): 54 | dm2 = [randn(int(n_features + i / 2), n_dims) for i in range(n_kernels)] 55 | for i in range(len(dm2)): 56 | dm2[i] /= norm(dm2[i], "fro") 57 | 58 | for m in [hausdorff, emd]: 59 | assert 0.0 != m(dm, dm2, "chordal") 60 | assert 0.0 != m(dm2, dm, "chordal") 61 | 62 | dm3 = [] 63 | for i in range(len(dm)): 64 | dm3.append(concatenate((zeros((4, 3)), dm[i]), axis=0)) 65 | 66 | for m in [hausdorff, emd]: 67 | assert_almost_equal(0.0, m(dm, dm3, "chordal")) 68 | assert_almost_equal(0.0, m(dm3, dm, "chordal")) 69 | 70 | # max(dm3) > max(dm4), min(dm4) > min(dm3) 71 | # dm4 = [] 72 | # for i in range(len(dm)): 73 | # k_l = dm[i].shape[0] 74 | # dm4.append(concatenate((zeros((i/2+1, 3)), dm[i]), axis=0)) 75 | # dm5 = [] 76 | # for i in range(len(dm)): 77 | # k_l = dm[i].shape[0] 78 | # dm5.append(concatenate((zeros((3, 3)), dm[i]), axis=0)) 79 | # for m in [hausdorff, emd]: 80 | # assert_almost_equal(0., m(dm4, dm5, 'chordal')) 81 | # assert_almost_equal(0., m(dm5, dm4, 'chordal')) 82 | 83 | du2 = [randn(int(n_features + i / 2), 1) for i in range(n_kernels)] 84 | for i in range(len(du2)): 85 | du2[i] /= norm(du2[i]) 86 | du3 = [] 87 | for i in range(len(du)): 88 | du3.append(concatenate((zeros((4, 1)), du[i]), axis=0)) 89 | for g, m in zip(gdu, [hausdorff, emd]): 90 | assert 0.0 != m(du, du2, g) 91 | assert 0.0 != m(du2, du, g) 92 | assert_almost_equal(0.0, m(du, du3, g)) 93 | assert_almost_equal(0.0, m(du3, du, g)) 94 | 95 | 96 | def test_unknown_metric(): 97 | for m in [hausdorff, emd]: 98 | assert m(dm, dm, "inexistant_metric") is NaN 99 | 100 | 101 | def test_inhomogeneous_dims(): 102 | idx = arange(n_dims) 103 | for g in ["chordal_principal_angles", "binetcauchy", "geodesic"]: 104 | for i in range(n_dims, 0, -1): 105 | assert_almost_equal(0.0, emd(dm, [a[:, idx[:i]] for a in dm], g, scale=True)) 106 | assert_almost_equal( 107 | 0.0, hausdorff(dm, [a[:, idx[:i]] for a in dm], g, scale=True) 108 | ) 109 | for g in ["chordal", "fubinistudy", "frobenius"]: 110 | assert_raises(ValueError, emd, dm, [a[:, :-1] for a in dm], g) 111 | assert_raises(ValueError, hausdorff, dm, [a[:, :-1] for a in dm], g) 112 | 113 | 114 | def test_univariate(): 115 | for m in [hausdorff, emd]: 116 | for g in gdu: 117 | assert_raises(ValueError, m, dm, dm, g) 118 | 119 | 120 | def test_correlation(): 121 | du2 = [ 122 | randn( 123 | n_features, 124 | ) 125 | for i in range(n_kernels) 126 | ] 127 | for i in range(len(du2)): 128 | du2[i] /= norm(du2[i]) 129 | dm2 = [randn(n_features, n_dims) for i in range(n_kernels)] 130 | for i in range(len(dm2)): 131 | dm2[i] /= norm(dm2[i]) 132 | 133 | assert_equal(100.0, detection_rate(du, du, 0.97)) 134 | assert 100.0 != detection_rate(du, du2, 0.99) 135 | assert_equal(100.0, detection_rate(dm, dm, 0.97)) 136 | assert 100.0 != detection_rate(dm, dm2, 0.99) 137 | assert_equal((100.0, 100.0), precision_recall(du, du, 0.97)) 138 | assert_equal((0.0, 0.0), precision_recall(du, du2, 0.99)) 139 | assert allclose(precision_recall_points(du, du), (ones(len(du)), ones(len(du)))) 140 | assert not allclose(precision_recall_points(du, du2), (ones(len(du)), ones(len(du2)))) 141 | 142 | 143 | def test_beta_dist(): 144 | du2 = [randn(n_features, 1) for i in range(n_kernels)] 145 | for i in range(len(du2)): 146 | du2[i] /= norm(du2[i]) 147 | 148 | assert_equal(0.0, beta_dist(du, du)) 149 | assert 0.0 != beta_dist(du, du2) 150 | 151 | du2 = [randn(n_features + 2, 1) for i in range(n_kernels)] 152 | for i in range(len(du2)): 153 | du2[i] /= norm(du2[i]) 154 | assert_raises(ValueError, beta_dist, du, du2) 155 | 156 | 157 | def test_beta_dict_length(): 158 | du2 = [randn(n_features, 1) for i in range(n_kernels + 2)] 159 | for i in range(len(du2)): 160 | du2[i] /= norm(du2[i]) 161 | 162 | assert 0.0 != beta_dist(du, du2) 163 | -------------------------------------------------------------------------------- /experiments/experiment_bci_competition.py: -------------------------------------------------------------------------------- 1 | """Learning dictionary on BCI Competition IV-2a dataset""" 2 | import pickle 3 | from os import listdir 4 | from os.path import exists 5 | 6 | from numpy import array, exp, int, nan_to_num, pi, poly, real, zeros_like 7 | from numpy.random import RandomState 8 | from plot_bci_dict import plot_atom_usage, plot_objective_func 9 | from scipy.io import loadmat 10 | from scipy.signal import butter, decimate, filtfilt 11 | 12 | from mdla import MiniBatchMultivariateDictLearning 13 | 14 | 15 | def notch(Wn, notchWidth): 16 | # Compute zeros 17 | nzeros = array([exp(1j * pi * Wn), exp(-1j * pi * Wn)]) 18 | # Compute poles 19 | poles = (1 - notchWidth) * nzeros 20 | b = poly(nzeros) # Get moving average filter coefficients 21 | a = poly(poles) # Get autoregressive filter coefficients 22 | return b, a 23 | 24 | 25 | def read_BCI_signals(): 26 | kppath = "../../datasets/BCIcompetition4/" 27 | lkp = listdir(kppath) 28 | sujets = list() 29 | classes = list() 30 | signals = list() 31 | 32 | preprocessing = True 33 | decimation = True 34 | 35 | if preprocessing: 36 | # Notch filtering 37 | f0 = 50.0 # notch frequency 38 | notchWidth = 0.1 # width of the notch 39 | # Bandpass filtering 40 | order = 8 41 | fc = array([8.0, 30.0]) # [0.1, 100] 42 | sr = 250 # sampling rate, o['SampleRate'][0,0] from loadmat 43 | Wn = f0 / (sr / 2.0) # ratio of notch freq. to Nyquist freq. 44 | [bn, an] = notch(Wn, notchWidth) 45 | [bb, ab] = butter(order, fc / (sr / 2.0), "bandpass") 46 | else: 47 | f0 = -1.0 48 | notchWidth = 0.0 49 | order = 0 50 | fc = 0 51 | 52 | if decimation: 53 | dfactor = 2.0 54 | else: 55 | dfactor = 1.0 56 | 57 | fn = ( 58 | "bcicompdata" 59 | + str( 60 | hash( 61 | str(preprocessing) 62 | + str(f0) 63 | + str(notchWidth) 64 | + str(order) 65 | + str(fc) 66 | + str(decimation) 67 | + str(dfactor) 68 | ) 69 | ) 70 | + ".pickle" 71 | ) 72 | 73 | if exists(fn): 74 | with open(fn, "rb") as f: 75 | o = pickle.load(f) 76 | signals = o["signals"] 77 | classes = o["classes"] 78 | print("Previous preprocessing of BCI dataset found, reusing it") 79 | else: 80 | for item in lkp: 81 | if item[-8:] == "-EOG.mat": 82 | print("loading", item) 83 | o = loadmat(kppath + item, struct_as_record=True) 84 | s = nan_to_num(o["s"]) 85 | # sample_rate = o['SampleRate'] 86 | event_type = o["EVENTTYP"] 87 | event_pos = o["EVENTPOS"] 88 | class_label = o["Classlabel"] 89 | if preprocessing: 90 | # Use a Notch filter to remove 50Hz power line 91 | ns = zeros_like(s) 92 | for e in range(s.shape[1]): 93 | ns[:, e] = filtfilt(bn, an, s[:, e]) 94 | # Apply a bandpass filter 95 | fs = zeros_like(s) 96 | for e in range(s.shape[1]): 97 | fs[:, e] = filtfilt(real(bb), real(ab), ns[:, e]) 98 | # decimate the signal 99 | if decimation: 100 | fs = decimate(fs, int(dfactor), axis=0, zero_phase=True) 101 | 102 | # Event Type 103 | trial_begin = 768 104 | 105 | start = 3 * sr / dfactor # 2s fixation, 1s after cue 106 | stop = 6 * sr / dfactor # 4s after cue, 3s of EEG 107 | 108 | trials = event_pos[event_type == trial_begin] 109 | for i, t in enumerate(trials): 110 | tmpfs = fs[int(t / dfactor + start) : int(t / dfactor + stop), 0:22] 111 | signals.append((tmpfs - tmpfs.mean(axis=0))) # center data 112 | sujets.append(item[2:3]) 113 | classes.append(class_label[i]) 114 | 115 | with open(fn, "wb") as f: 116 | o = {"signals": signals, "classes": classes} 117 | pickle.dump(o, f) 118 | return signals, classes 119 | 120 | 121 | X, classes = read_BCI_signals() 122 | 123 | rng_global = RandomState(1) 124 | n_samples = len(X) 125 | n_dims = X[0].shape[0] # 22 electrodes 126 | n_features = X[0].shape[1] # 375, 3s of decimated signal at 125Hz 127 | kernel_init_len = 80 # kernel size is 50 128 | n_kernels = 60 129 | n_nonzero_coefs = 2 130 | learning_rate = 5.0 131 | n_iter = 40 # 100 132 | n_jobs, batch_size = -1, None # n_cpu, 5*n_cpu 133 | figname = "-60ker-K3-klen80-lr5.0-emm-all" 134 | 135 | d = MiniBatchMultivariateDictLearning( 136 | n_kernels=n_kernels, 137 | batch_size=batch_size, 138 | n_iter=n_iter, 139 | n_nonzero_coefs=n_nonzero_coefs, 140 | n_jobs=n_jobs, 141 | learning_rate=learning_rate, 142 | kernel_init_len=kernel_init_len, 143 | verbose=1, 144 | random_state=rng_global, 145 | ) 146 | d = d.fit(X) 147 | 148 | plot_objective_func(d.error_, n_iter, figname) 149 | 150 | n_jobs = 4 151 | plot_atom_usage(X, d.kernels_, n_nonzero_coefs, n_jobs, figname) 152 | 153 | with open("EEG-savedico" + figname + ".pkl", "wb") as f: 154 | o = { 155 | "kernels": d.kernels_, 156 | "error": d.error_, 157 | "kernel_init_len": d.kernel_init_len, 158 | "learning_rate": d.learning_rate, 159 | "n_iter": d.n_iter, 160 | "n_jobs": d.n_jobs, 161 | "n_kernels": d.n_kernels, 162 | "n_nonzero_coefs": d.n_nonzero_coefs, 163 | } 164 | pickle.dump(o, f) 165 | -------------------------------------------------------------------------------- /examples/plot_bci_dict.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | 3 | import matplotlib 4 | import matplotlib.pyplot as plt 5 | from numpy import abs, arange, argsort, histogram, hstack, percentile, zeros 6 | from numpy.linalg import norm 7 | 8 | from mdla import multivariate_sparse_encode, reconstruct_from_code 9 | 10 | 11 | matplotlib.use("Agg") 12 | 13 | # TODO: use sum of decomposition weight instead of number of atom usage in 14 | # plot_atom_usage 15 | # - subplot(3,n*2, ) = X, residual, reconst for n best/worst reconstruction. 16 | # 17 | 18 | 19 | def plot_kernels( 20 | kernels, 21 | n_kernels, 22 | col=5, 23 | row=-1, 24 | order=None, 25 | amp=None, 26 | figname="allkernels", 27 | label=None, 28 | ): 29 | n_display = idx = 0 30 | if n_kernels == row * col: 31 | pass 32 | elif row == -1: 33 | row = n_kernels / int(col) 34 | if n_kernels % int(col) != 0: 35 | row += 1 36 | elif col == -1: 37 | col = n_kernels / int(row) 38 | if n_kernels % int(row) != 0: 39 | col += 1 40 | n_display = row * col 41 | n_figure = int(n_kernels / n_display) 42 | if n_kernels % int(n_display) != 0: 43 | n_figure += 1 44 | if order is None: 45 | order = range(n_kernels) 46 | if label is None: 47 | label = range(n_kernels) 48 | if amp is None: 49 | amp = range(n_kernels) 50 | 51 | for j in range(int(n_figure)): 52 | fig = plt.figure(figsize=(15, 10)) 53 | for i in range(1, n_display + 1): 54 | if idx + i > n_kernels: 55 | break 56 | k = fig.add_subplot(row, col, i) 57 | k.plot(kernels[order[-(idx + i)]]) 58 | k.set_xticklabels([]) 59 | k.set_yticklabels([]) 60 | k.set_title( 61 | "k %d: %d-%g" 62 | % (order[-(idx + i)], label[order[-(idx + i)]], amp[order[-(idx + i)]]) 63 | ) 64 | idx += n_display 65 | plt.tight_layout(0.5) 66 | plt.savefig(figname + "-part" + str(j) + ".png") 67 | 68 | 69 | def plot_reconstruction_samples(X, r, code, kernels, n, figname): 70 | n_features = X[0].shape[0] 71 | energy_residual = zeros(len(r)) 72 | for i in range(len(r)): 73 | energy_residual[i] = norm(r[i], "fro") 74 | energy_sample = zeros(len(X)) 75 | for i in range(len(X)): 76 | energy_sample[i] = norm(X[i], "fro") 77 | 78 | energy_explained = energy_residual / energy_sample 79 | index = argsort(energy_explained) # 0 =worse, end=best 80 | fig = plt.figure(figsize=(15, 9)) 81 | k = fig.add_subplot(3, 2 * n, 1) 82 | k.set_xticklabels([]) 83 | k.set_yticklabels([]) 84 | for i in range(n): 85 | if i != 0: 86 | ka = fig.add_subplot(3, 2 * n, i + 1, sharex=k, sharey=k) 87 | else: 88 | ka = k 89 | ka.plot(X[index[i]]) 90 | ka.set_title("s%d: %.1f%%" % (index[i], 100.0 * (1 - energy_explained[index[i]]))) 91 | ka = fig.add_subplot(3, 2 * n, 2 * n + i + 1, sharex=k, sharey=k) 92 | ka.plot(r[index[i]]) 93 | ka = fig.add_subplot(3, 2 * n, 4 * n + i + 1, sharex=k, sharey=k) 94 | s = reconstruct_from_code([code[index[i]]], kernels, n_features) 95 | ka.plot(s[0, :, :]) 96 | for j, i in zip(range(n, 2 * n), range(n, 0, -1)): 97 | ka = fig.add_subplot(3, 2 * n, j + 1, sharex=k, sharey=k) 98 | ka.plot(X[index[-i]]) 99 | ka.set_title( 100 | "s%d: %.1f%%" % (index[-i], 100.0 * (1 - energy_explained[index[-i]])) 101 | ) 102 | ka = fig.add_subplot(3, 2 * n, 2 * n + j + 1, sharex=k, sharey=k) 103 | ka.plot(r[index[-i]]) 104 | ka = fig.add_subplot(3, 2 * n, 4 * n + j + 1, sharex=k, sharey=k) 105 | s = reconstruct_from_code([code[index[-i]]], kernels, n_features) 106 | ka.plot(s[0, :, :]) 107 | plt.tight_layout(0.5) 108 | plt.savefig("EEG-reconstruction" + figname + ".png") 109 | 110 | 111 | def plot_objective_func_box(error, n_iter, figname): 112 | fig = plt.figure() 113 | objf = fig.add_subplot(1, 1, 1) 114 | ofun = objf.boxplot(error.T) 115 | medianof = [median.get_ydata()[0] for n, median in enumerate(ofun["medians"])] 116 | _ = objf.plot(arange(1, n_iter + 1), medianof, linewidth=1) 117 | plt.savefig("EEG-decomposition-error" + figname + ".png") 118 | 119 | 120 | def plot_objective_func(error, n_iter, figname): 121 | fig = plt.figure() 122 | objf = fig.add_subplot(1, 1, 1) 123 | p0, p25, med, p75, p100 = percentile(error, (0, 25, 50, 75, 100), axis=1) 124 | objf.fill_between( 125 | arange(1, n_iter + 1), p0, p100, facecolor="blue", alpha=0.1, interpolate=True 126 | ) 127 | objf.fill_between( 128 | arange(1, n_iter + 1), p25, p75, facecolor="blue", alpha=0.3, interpolate=True 129 | ) 130 | objf.plot(arange(1, n_iter + 1), med, linewidth=2.5, color="blue") 131 | objf.set_xlabel("Iterations") 132 | objf.set_ylabel("Objective function") 133 | plt.tight_layout(0.5) 134 | plt.savefig("EEG-decomposition-error" + figname + ".png") 135 | 136 | 137 | def plot_coef_hist(decomposition_weight, figname, width=1): 138 | correlation = sorted(Counter(decomposition_weight).items()) 139 | labels, values = zip(*correlation) 140 | indexes = arange(len(correlation)) 141 | plt.figure() 142 | plt.bar(indexes, values, width, linewidth=0) 143 | plt.savefig("EEG-coeff_hist_sorted" + figname + ".png") 144 | 145 | 146 | def plot_weight_hist(amplitudes, figname, width=1): 147 | amplitudes.sort() 148 | indexes = arange(len(amplitudes)) 149 | plt.figure() 150 | width = 1 151 | plt.bar(indexes, amplitudes, width, linewidth=0) 152 | plt.savefig("EEG-weight_sorted" + figname + ".png") 153 | 154 | 155 | def plot_atom_usage(X, kernels, n_nonzero_coefs, n_jobs, figname): 156 | r, code = multivariate_sparse_encode( 157 | X, kernels, n_nonzero_coefs=n_nonzero_coefs, n_jobs=n_jobs, verbose=2 158 | ) 159 | n_kernels = len(kernels) 160 | amplitudes = zeros(n_kernels) 161 | for i in range(len(code)): 162 | for s in range(n_nonzero_coefs): 163 | amplitudes[int(code[i][s, 2])] += abs(code[i][s, 0]) 164 | 165 | decomposition_weight = hstack([code[i][:, 2] for i in range(len(code))]) 166 | decomposition_weight.sort() 167 | weight, _ = histogram(decomposition_weight, len(kernels), normed=False) 168 | order = weight.argsort() 169 | plot_kernels( 170 | kernels, 171 | len(kernels), 172 | order=order, 173 | label=weight, 174 | amp=amplitudes, 175 | figname="EEG-kernels" + figname, 176 | row=6, 177 | ) 178 | plot_coef_hist(decomposition_weight, figname) 179 | plot_weight_hist(amplitudes, figname) 180 | plot_reconstruction_samples(X, r, code, kernels, 3, figname) 181 | -------------------------------------------------------------------------------- /examples/example_benchmark_performance.py: -------------------------------------------------------------------------------- 1 | """Benchmarking dictionary learning algorithms on random dataset""" 2 | 3 | from multiprocessing import cpu_count 4 | from time import time 5 | 6 | import matplotlib.pyplot as plt 7 | import numpy as np 8 | from numpy import array 9 | from numpy.linalg import norm 10 | from numpy.random import permutation, rand, randint, randn 11 | 12 | from mdla import MiniBatchMultivariateDictLearning, MultivariateDictLearning 13 | 14 | 15 | # TODO: 16 | # investigate perf break from pydico 17 | 18 | 19 | def benchmarking_plot(figname, pst, plot_sep, minibatchRange, mprocessRange): 20 | _ = plt.figure(figsize=(15, 10)) 21 | bar_width = 0.35 22 | _ = plt.bar( 23 | np.array([0]), 24 | pst[0], 25 | bar_width, 26 | color="b", 27 | label="Online, no multiprocessing (baseline)", 28 | ) 29 | index = [0] 30 | for i in range(1, plot_sep[1]): 31 | if i == 1: 32 | _ = plt.bar( 33 | np.array([i + 1]), 34 | pst[i], 35 | bar_width, 36 | color="r", 37 | label="Online with minibatch", 38 | ) 39 | else: 40 | _ = plt.bar(np.array([i + 1]), pst[i], bar_width, color="r") 41 | index.append(i + 1) 42 | for _ in range(plot_sep[1], plot_sep[2]): 43 | if i == plot_sep[1]: 44 | _ = plt.bar( 45 | np.array([i + 2]), 46 | pst[i], 47 | bar_width, 48 | label="Batch with multiprocessing", 49 | color="magenta", 50 | ) 51 | else: 52 | _ = plt.bar(np.array([i + 2]), pst[i], bar_width, color="magenta") 53 | index.append(i + 2) 54 | 55 | plt.ylabel("Time per iteration (s)") 56 | plt.title("Processing time for online and batch processing") 57 | tick = [""] 58 | tick.extend(map(str, minibatchRange)) 59 | tick.extend(map(str, mprocessRange)) 60 | plt.xticks(index, tuple(tick)) 61 | plt.legend() 62 | plt.savefig(figname + ".png") 63 | 64 | 65 | def _generate_testbed( 66 | kernel_init_len, 67 | n_nonzero_coefs, 68 | n_kernels, 69 | n_samples=10, 70 | n_features=5, 71 | n_dims=3, 72 | snr=1000, 73 | ): 74 | """Generate a dataset from a random dictionary 75 | 76 | Generate a random dictionary and a dataset, where samples are combination of 77 | n_nonzero_coefs dictionary atoms. Noise is added, based on SNR value, with 78 | 1000 indicated that no noise should be added. 79 | Return the dictionary, the dataset and an array indicated how atoms are combined 80 | to obtain each sample 81 | """ 82 | print("Dictionary sampled from uniform distribution") 83 | dico = [rand(kernel_init_len, n_dims) for i in range(n_kernels)] 84 | for i in range(len(dico)): 85 | dico[i] /= norm(dico[i], "fro") 86 | 87 | signals = list() 88 | decomposition = list() 89 | for _ in range(n_samples): 90 | s = np.zeros(shape=(n_features, n_dims)) 91 | d = np.zeros(shape=(n_nonzero_coefs, 3)) 92 | rk = permutation(range(n_kernels)) 93 | for j in range(n_nonzero_coefs): 94 | k_idx = rk[j] 95 | k_amplitude = 3.0 * rand() + 1.0 96 | k_offset = randint(n_features - kernel_init_len + 1) 97 | s[k_offset : k_offset + kernel_init_len, :] += k_amplitude * dico[k_idx] 98 | d[j, :] = array([k_amplitude, k_offset, k_idx]) 99 | decomposition.append(d) 100 | noise = randn(n_features, n_dims) 101 | if snr == 1000: 102 | alpha = 0 103 | else: 104 | ps = norm(s, "fro") 105 | pn = norm(noise, "fro") 106 | alpha = ps / (pn * 10 ** (snr / 20.0)) 107 | signals.append(s + alpha * noise) 108 | signals = np.array(signals) 109 | 110 | return dico, signals, decomposition 111 | 112 | 113 | rng_global = np.random.RandomState(1) 114 | n_samples, n_dims = 1500, 1 115 | n_features = kernel_init_len = 5 116 | n_nonzero_coefs = 3 117 | n_kernels, max_iter, learning_rate = 50, 10, 1.5 118 | n_jobs, batch_size = -1, None 119 | 120 | iter_time, plot_separator, it_separator = list(), list(), 0 121 | 122 | generating_dict, X, code = _generate_testbed( 123 | kernel_init_len, n_nonzero_coefs, n_kernels, n_samples, n_features, n_dims 124 | ) 125 | 126 | # Online without mini-batch 127 | print( 128 | "Processing ", 129 | max_iter, 130 | "iterations in online mode, " "without multiprocessing:", 131 | end="", 132 | ) 133 | batch_size, n_jobs = n_samples, 1 134 | learned_dict = MiniBatchMultivariateDictLearning( 135 | n_kernels=n_kernels, 136 | batch_size=batch_size, 137 | n_iter=max_iter, 138 | n_nonzero_coefs=n_nonzero_coefs, 139 | n_jobs=n_jobs, 140 | learning_rate=learning_rate, 141 | kernel_init_len=kernel_init_len, 142 | verbose=1, 143 | dict_init=None, 144 | random_state=rng_global, 145 | ) 146 | ts = time() 147 | learned_dict = learned_dict.fit(X) 148 | iter_time.append((time() - ts) / max_iter) 149 | it_separator += 1 150 | plot_separator.append(it_separator) 151 | 152 | # Online with mini-batch 153 | minibatch_range = [cpu_count()] 154 | minibatch_range.extend([cpu_count() * i for i in range(3, 10, 2)]) 155 | n_jobs = -1 156 | for mb in minibatch_range: 157 | print( 158 | "\nProcessing ", 159 | max_iter, 160 | "iterations in online mode, with ", 161 | "minibatch size", 162 | mb, 163 | "and", 164 | cpu_count(), 165 | "processes:", 166 | end="", 167 | ) 168 | batch_size = mb 169 | learned_dict = MiniBatchMultivariateDictLearning( 170 | n_kernels=n_kernels, 171 | batch_size=batch_size, 172 | n_iter=max_iter, 173 | n_nonzero_coefs=n_nonzero_coefs, 174 | n_jobs=n_jobs, 175 | learning_rate=learning_rate, 176 | kernel_init_len=kernel_init_len, 177 | verbose=1, 178 | dict_init=None, 179 | random_state=rng_global, 180 | ) 181 | ts = time() 182 | learned_dict = learned_dict.fit(X) 183 | iter_time.append((time() - ts) / max_iter) 184 | it_separator += 1 185 | plot_separator.append(it_separator) 186 | 187 | # Batch learning 188 | mp_range = range(1, cpu_count() + 1) 189 | for p in mp_range: 190 | print( 191 | "\nProcessing ", 192 | max_iter, 193 | "iterations in batch mode, with", 194 | p, 195 | "processes:", 196 | end="", 197 | ) 198 | n_jobs = p 199 | learned_dict = MultivariateDictLearning( 200 | n_kernels=n_kernels, 201 | max_iter=max_iter, 202 | verbose=1, 203 | n_nonzero_coefs=n_nonzero_coefs, 204 | n_jobs=n_jobs, 205 | learning_rate=learning_rate, 206 | kernel_init_len=kernel_init_len, 207 | dict_init=None, 208 | random_state=rng_global, 209 | ) 210 | ts = time() 211 | learned_dict = learned_dict.fit(X) 212 | iter_time.append((time() - ts) / max_iter) 213 | it_separator += 1 214 | plot_separator.append(it_separator) 215 | print("Done benchmarking") 216 | 217 | figname = "minibatch-performance" 218 | print("Plotting results in", figname) 219 | benchmarking_plot(figname, iter_time, plot_separator, minibatch_range, mp_range) 220 | 221 | print("Exiting.") 222 | -------------------------------------------------------------------------------- /examples/example_multivariate.py: -------------------------------------------------------------------------------- 1 | """Dictionary recovering experiment for multivariate random dataset""" 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | from numpy import arange, array, max, min 5 | from numpy.linalg import norm 6 | from numpy.random import RandomState, permutation, rand, randint, randn 7 | 8 | from dict_metrics import detection_rate, emd 9 | from mdla import MiniBatchMultivariateDictLearning 10 | 11 | 12 | def plot_multivariate(objective_error, detection_rate, wasserstein, n_iter, figname): 13 | fig = plt.figure(figsize=(15, 5)) 14 | step = n_iter 15 | 16 | # plotting data from objective error 17 | objerr = fig.add_subplot(1, 3, 1) 18 | _ = objerr.plot( 19 | step * arange(1, len(objective_error) + 1), 20 | objective_error, 21 | color="green", 22 | label=r"Objective error", 23 | ) 24 | objerr.axis([0, len(objective_error) - 1, min(objective_error), max(objective_error)]) 25 | objerr.set_xticks(arange(0, step * len(objective_error) + 1, step)) 26 | objerr.set_xlabel("Iteration") 27 | objerr.set_ylabel(r"Error (no unit)") 28 | objerr.legend(loc="upper right") 29 | 30 | # plotting data from detection rate 0.99 31 | detection = fig.add_subplot(1, 3, 2) 32 | _ = detection.plot( 33 | step * arange(1, len(detection_rate) + 1), 34 | detection_rate, 35 | color="magenta", 36 | label=r"Detection rate 0.99", 37 | ) 38 | detection.axis([0, len(detection_rate), 0, 100]) 39 | detection.set_xticks(arange(0, step * len(detection_rate) + 1, step)) 40 | detection.set_xlabel("Iteration") 41 | detection.set_ylabel(r"Recovery rate (in %)") 42 | detection.legend(loc="upper left") 43 | 44 | # plotting data from our metric 45 | met = fig.add_subplot(1, 3, 3) 46 | _ = met.plot( 47 | step * arange(1, len(wasserstein) + 1), 48 | 1 - wasserstein, 49 | label=r"$d_W$", 50 | color="red", 51 | ) 52 | met.axis([0, len(wasserstein), 0, 1]) 53 | met.set_xticks(arange(0, step * len(wasserstein) + 1, step)) 54 | met.set_xlabel("Iteration") 55 | met.set_ylabel(r"Recovery distance") 56 | met.legend(loc="upper left") 57 | 58 | plt.tight_layout(0.5) 59 | plt.savefig(figname + ".png") 60 | 61 | 62 | def _generate_testbed( 63 | kernel_init_len, 64 | n_nonzero_coefs, 65 | n_kernels, 66 | n_samples=10, 67 | n_features=5, 68 | n_dims=3, 69 | snr=1000, 70 | ): 71 | """Generate a dataset from a random dictionary 72 | 73 | Generate a random dictionary and a dataset, where samples are combination of 74 | n_nonzero_coefs dictionary atoms. Noise is added, based on SNR value, with 75 | 1000 indicated that no noise should be added. 76 | Return the dictionary, the dataset and an array indicated how atoms are combined 77 | to obtain each sample 78 | """ 79 | dico = [randn(kernel_init_len, n_dims) for i in range(n_kernels)] 80 | for i in range(len(dico)): 81 | dico[i] /= norm(dico[i], "fro") 82 | 83 | signals = list() 84 | decomposition = list() 85 | for _ in range(n_samples): 86 | s = np.zeros(shape=(n_features, n_dims)) 87 | d = np.zeros(shape=(n_nonzero_coefs, 3)) 88 | rk = permutation(range(n_kernels)) 89 | for j in range(n_nonzero_coefs): 90 | k_idx = rk[j] 91 | k_amplitude = 3.0 * rand() + 1.0 92 | k_offset = randint(n_features - kernel_init_len + 1) 93 | s[k_offset : k_offset + kernel_init_len, :] += k_amplitude * dico[k_idx] 94 | d[j, :] = array([k_amplitude, k_offset, k_idx]) 95 | decomposition.append(d) 96 | noise = randn(n_features, n_dims) 97 | if snr == 1000: 98 | alpha = 0 99 | else: 100 | ps = norm(s, "fro") 101 | pn = norm(noise, "fro") 102 | alpha = ps / (pn * 10 ** (snr / 20.0)) 103 | signals.append(s + alpha * noise) 104 | signals = np.array(signals) 105 | 106 | return dico, signals, decomposition 107 | 108 | 109 | rng_global = RandomState(1) 110 | n_samples, n_dims = 1500, 3 111 | n_features = kernel_init_len = 20 112 | n_nonzero_coefs = 3 113 | n_kernels, max_iter, n_iter, learning_rate = 50, 10, 1, 1.5 114 | n_jobs, batch_size = -1, 10 115 | detect_rate, wasserstein, objective_error = list(), list(), list() 116 | 117 | generating_dict, X, code = _generate_testbed( 118 | kernel_init_len, n_nonzero_coefs, n_kernels, n_samples, n_features, n_dims 119 | ) 120 | 121 | # # Create a dictionary 122 | # dict_init = [rand(kernel_init_len, n_dims) for i in range(n_kernels)] 123 | # for i in range(len(dict_init)): 124 | # dict_init[i] /= norm(dict_init[i], 'fro') 125 | dict_init = None 126 | 127 | learned_dict = MiniBatchMultivariateDictLearning( 128 | n_kernels=n_kernels, 129 | batch_size=batch_size, 130 | n_iter=n_iter, 131 | n_nonzero_coefs=n_nonzero_coefs, 132 | n_jobs=n_jobs, 133 | learning_rate=learning_rate, 134 | kernel_init_len=kernel_init_len, 135 | verbose=1, 136 | dict_init=dict_init, 137 | random_state=rng_global, 138 | ) 139 | 140 | # Update learned dictionary at each iteration and compute a distance 141 | # with the generating dictionary 142 | for _ in range(max_iter): 143 | learned_dict = learned_dict.partial_fit(X) 144 | # Compute the detection rate 145 | detect_rate.append(detection_rate(learned_dict.kernels_, generating_dict, 0.99)) 146 | # Compute the Wasserstein distance 147 | wasserstein.append(emd(learned_dict.kernels_, generating_dict, "chordal", scale=True)) 148 | # Get the objective error 149 | objective_error.append(learned_dict.error_.sum()) 150 | 151 | plot_multivariate( 152 | array(objective_error), 153 | array(detect_rate), 154 | 100.0 - array(wasserstein), 155 | n_iter, 156 | "multivariate-case", 157 | ) 158 | 159 | 160 | # Another possibility is to rely on a callback function such as 161 | def callback_distance(loc): 162 | ii, iter_offset = loc["ii"], loc["iter_offset"] 163 | n_batches = loc["n_batches"] 164 | if np.mod((ii - iter_offset) / int(n_batches), n_iter) == 0: 165 | # Compute distance only every 5 iterations, as in previous case 166 | d = loc["dict_obj"] 167 | d.wasserstein.append( 168 | emd(loc["dictionary"], d.generating_dict, "chordal", scale=True) 169 | ) 170 | d.detect_rate.append(detection_rate(loc["dictionary"], d.generating_dict, 0.99)) 171 | d.objective_error.append(loc["current_cost"]) 172 | 173 | 174 | # reinitializing the random generator 175 | learned_dict2 = MiniBatchMultivariateDictLearning( 176 | n_kernels=n_kernels, 177 | batch_size=batch_size, 178 | n_iter=max_iter * n_iter, 179 | n_nonzero_coefs=n_nonzero_coefs, 180 | callback=callback_distance, 181 | n_jobs=n_jobs, 182 | learning_rate=learning_rate, 183 | kernel_init_len=kernel_init_len, 184 | verbose=1, 185 | dict_init=dict_init, 186 | random_state=rng_global, 187 | ) 188 | learned_dict2.generating_dict = list(generating_dict) 189 | learned_dict2.wasserstein = list() 190 | learned_dict2.detect_rate = list() 191 | learned_dict2.objective_error = list() 192 | 193 | learned_dict2 = learned_dict2.fit(X) 194 | 195 | plot_multivariate( 196 | array(learned_dict2.objective_error), 197 | array(learned_dict2.detect_rate), 198 | array(learned_dict2.wasserstein), 199 | n_iter=1, 200 | figname="multivariate-case-callback", 201 | ) 202 | -------------------------------------------------------------------------------- /examples/example_sparse_decomposition.py: -------------------------------------------------------------------------------- 1 | """Example of sparse decomposition with MOMP on random dataset""" 2 | 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | from numpy import array, zeros 6 | from numpy.linalg import norm 7 | from sklearn.utils import check_random_state 8 | 9 | from mdla import multivariate_sparse_encode 10 | 11 | 12 | def _generate_testbed( 13 | kernel_init_len, 14 | n_nonzero_coefs, 15 | n_kernels, 16 | rng=None, 17 | n_samples=10, 18 | n_features=5, 19 | n_dims=3, 20 | snr=1000, 21 | Gaussian=False, 22 | ): 23 | """Generate a dataset from a random dictionary 24 | 25 | Generate a random dictionary and a dataset, where samples are combination of 26 | n_nonzero_coefs dictionary atoms. Noise is added, based on SNR value, with 27 | 1000 indicated that no noise should be added. 28 | Return the dictionary, the dataset and an array indicated how atoms are 29 | combined to obtain each sample 30 | """ 31 | rng = check_random_state(rng) 32 | 33 | if Gaussian: 34 | print( 35 | "Dictionary of size (", 36 | kernel_init_len, 37 | ",", 38 | n_dims, 39 | ") sampled from Gaussian distribution", 40 | ) 41 | dico = [rng.randn(kernel_init_len, n_dims) for i in range(n_kernels)] 42 | else: 43 | print( 44 | "Dictionary of size (", 45 | kernel_init_len, 46 | ",", 47 | n_dims, 48 | ") sampled from uniform distribution", 49 | ) 50 | dico = [rng.rand(kernel_init_len, n_dims) for i in range(n_kernels)] 51 | 52 | for i in range(len(dico)): 53 | dico[i] /= norm(dico[i], "fro") 54 | 55 | signals = list() 56 | decomposition = list() 57 | for _ in range(n_samples): 58 | s = np.zeros(shape=(n_features, n_dims)) 59 | d = np.zeros(shape=(n_nonzero_coefs, 3)) 60 | rk = rng.permutation(range(n_kernels)) 61 | for j in range(n_nonzero_coefs): 62 | k_idx = rk[j] 63 | k_amplitude = 3.0 * rng.rand() + 1.0 64 | k_offset = rng.randint(n_features - kernel_init_len + 1) 65 | s[k_offset : k_offset + kernel_init_len, :] += k_amplitude * dico[k_idx] 66 | d[j, :] = array([k_amplitude, k_offset, k_idx]) 67 | decomposition.append(d) 68 | noise = rng.randn(n_features, n_dims) 69 | if snr == 1000: 70 | alpha = 0 71 | else: 72 | ps = norm(s, "fro") 73 | pn = norm(noise, "fro") 74 | alpha = ps / (pn * 10 ** (snr / 20.0)) 75 | signals.append(s + alpha * noise) 76 | 77 | return dico, signals, decomposition 78 | 79 | 80 | rng_global = np.random.RandomState(1) 81 | n_nonzero_coefs = 15 82 | 83 | 84 | def decomposition_random_dictionary(Gaussian=True, rng=None, n_features=65, n_dims=1): 85 | """Generate a dataset from a random dictionary and compute decomposition 86 | 87 | A dataset of n_samples examples is generated from a random dictionary, 88 | each sample containing a random mixture of n_nonzero_coef atoms and has 89 | a dimension of n_features by n_dims. All the examples are decomposed with 90 | sparse multivariate OMP, written as: 91 | (Eq. 1) min_a ||x - Da ||^2 s.t. ||a||_0 <= k 92 | with x in R^(n_features x n_dims), D in R^(n_features x n_kernels) and 93 | a in R^n_kernels. 94 | 95 | Returns a ndarray of (n_nonzero_coefs, n_samples) containing all the 96 | root mean square error (RMSE) computed as the residual of the decomposition 97 | for all samples for sparsity constraint values of (Eq. 1) going from 1 98 | to n_nonzero_coefs. 99 | """ 100 | n_samples = 100 101 | kernel_init_len = n_features 102 | n_kernels = 50 103 | n_jobs = 1 104 | 105 | dictionary, X, code = _generate_testbed( 106 | kernel_init_len=kernel_init_len, 107 | n_nonzero_coefs=n_nonzero_coefs, 108 | n_kernels=n_kernels, 109 | n_samples=n_samples, 110 | n_features=n_features, 111 | n_dims=n_dims, 112 | rng=rng_global, 113 | Gaussian=Gaussian, 114 | ) 115 | rmse = zeros(shape=(n_nonzero_coefs, n_samples)) 116 | for k in range(n_nonzero_coefs): 117 | for idx, s in enumerate(X): 118 | r, _ = multivariate_sparse_encode( 119 | array(s, ndmin=3), 120 | dictionary, 121 | n_nonzero_coefs=k + 1, 122 | n_jobs=n_jobs, 123 | verbose=1, 124 | ) 125 | rmse[k, idx] = norm(r[0], "fro") / norm(s, "fro") * 100 126 | return rmse 127 | 128 | 129 | rmse_gaussian1 = decomposition_random_dictionary( 130 | Gaussian=True, rng=rng_global, n_features=65, n_dims=1 131 | ) 132 | rmse_uniform1 = decomposition_random_dictionary( 133 | Gaussian=False, rng=rng_global, n_features=65, n_dims=1 134 | ) 135 | rmse_gaussian2 = decomposition_random_dictionary( 136 | Gaussian=True, rng=rng_global, n_features=65, n_dims=3 137 | ) 138 | rmse_uniform2 = decomposition_random_dictionary( 139 | Gaussian=False, rng=rng_global, n_features=65, n_dims=3 140 | ) 141 | rmse_gaussian3 = decomposition_random_dictionary( 142 | Gaussian=True, rng=rng_global, n_features=65, n_dims=5 143 | ) 144 | rmse_uniform3 = decomposition_random_dictionary( 145 | Gaussian=False, rng=rng_global, n_features=65, n_dims=5 146 | ) 147 | 148 | fig = plt.figure(figsize=(15, 5)) 149 | uni = fig.add_subplot(1, 3, 1) 150 | uni.set_title(r"$n$=1") 151 | uni.errorbar( 152 | range(1, n_nonzero_coefs + 1), 153 | rmse_uniform1.mean(1), 154 | yerr=rmse_uniform1.std(1), 155 | label="Uniform", 156 | ) 157 | uni.errorbar( 158 | range(1, n_nonzero_coefs + 1), 159 | rmse_gaussian1.mean(1), 160 | yerr=rmse_gaussian1.std(1), 161 | color="r", 162 | label="Gaussian", 163 | ) 164 | uni.plot(range(n_nonzero_coefs + 2), np.zeros(n_nonzero_coefs + 2), "k") 165 | uni.axis([0, n_nonzero_coefs + 1, 0, 100]) 166 | uni.set_xticks(range(0, n_nonzero_coefs + 2, 5)) 167 | uni.set_ylabel("rRMSE (%)") 168 | uni.legend(loc="upper right") 169 | mul1 = fig.add_subplot(1, 3, 2) 170 | mul1.set_title(r"Random multivariate dictionary, $n$=3") 171 | mul1.errorbar( 172 | range(1, n_nonzero_coefs + 1), 173 | rmse_uniform2.mean(1), 174 | yerr=rmse_uniform2.std(1), 175 | label="Uniform", 176 | ) 177 | mul1.errorbar( 178 | range(1, n_nonzero_coefs + 1), 179 | rmse_gaussian2.mean(1), 180 | yerr=rmse_gaussian2.std(1), 181 | color="r", 182 | label="Gaussian", 183 | ) 184 | mul1.plot(range(n_nonzero_coefs + 2), np.zeros(n_nonzero_coefs + 2), "k") 185 | mul1.axis([0, n_nonzero_coefs + 1, 0, 100]) 186 | mul1.set_xticks(range(0, n_nonzero_coefs + 2, 5)) 187 | mul1.set_xlabel("k") 188 | mul1.legend(loc="upper right") 189 | mul2 = fig.add_subplot(1, 3, 3) 190 | mul2.set_title(r"$n$=5") 191 | mul2.errorbar( 192 | range(1, n_nonzero_coefs + 1), 193 | rmse_uniform3.mean(1), 194 | yerr=rmse_uniform3.std(1), 195 | label="Uniform", 196 | ) 197 | mul2.errorbar( 198 | range(1, n_nonzero_coefs + 1), 199 | rmse_gaussian3.mean(1), 200 | yerr=rmse_gaussian3.std(1), 201 | color="r", 202 | label="Gaussian", 203 | ) 204 | mul2.plot(range(n_nonzero_coefs + 2), np.zeros(n_nonzero_coefs + 2), "k") 205 | mul2.axis([0, n_nonzero_coefs + 1, 0, 100]) 206 | mul2.set_xticks(range(0, n_nonzero_coefs + 2, 5)) 207 | mul2.legend(loc="upper right") 208 | plt.tight_layout(0.5) 209 | plt.savefig("sparse_decomposition_multivariate.png") 210 | -------------------------------------------------------------------------------- /examples/example_univariate.py: -------------------------------------------------------------------------------- 1 | """Dictionary recovering experiment for univariate random dataset""" 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | from numpy import arange, array, max, min 5 | from numpy.linalg import norm 6 | from numpy.random import RandomState, permutation, rand, randint, randn 7 | 8 | from dict_metrics import detection_rate, emd 9 | from mdla import MiniBatchMultivariateDictLearning 10 | 11 | 12 | def plot_univariate(objective_error, detect_rate, wasserstein, n_iter, figname): 13 | fig = plt.figure(figsize=(15, 5)) 14 | if n_iter == 1: 15 | step = 5 16 | else: 17 | step = n_iter 18 | 19 | # plotting data from objective error 20 | objerr = fig.add_subplot(1, 3, 1) 21 | # ofun = objerr.boxplot(objective_error) 22 | # medianof = [median.get_ydata()[0] 23 | # for n, median in enumerate(ofun['medians'])] 24 | # _ = objerr.plot(arange(1, n_iter+1), medianof, linewidth=1) 25 | 26 | _ = objerr.plot( 27 | step * arange(1, len(objective_error) + 1), 28 | objective_error, 29 | color="green", 30 | label=r"Objective error", 31 | ) 32 | objerr.axis([0, len(objective_error) - 1, min(objective_error), max(objective_error)]) 33 | objerr.set_xticks(arange(0, step * len(objective_error) + 1, step)) 34 | objerr.set_xlabel("Iteration") 35 | objerr.set_ylabel(r"Error (no unit)") 36 | objerr.legend(loc="upper right") 37 | 38 | # plotting data from detection rate 0.99 39 | detection = fig.add_subplot(1, 3, 2) 40 | _ = detection.plot( 41 | step * arange(1, len(detect_rate) + 1), 42 | detect_rate, 43 | color="magenta", 44 | label=r"Detection rate 0.99", 45 | ) 46 | detection.axis([0, len(detect_rate), 0, 100]) 47 | detection.set_xticks(arange(0, step * len(detect_rate) + 1, step)) 48 | detection.set_xlabel("Iteration") 49 | detection.set_ylabel(r"Recovery rate (in %)") 50 | detection.legend(loc="upper left") 51 | 52 | # plotting data from our metric 53 | met = fig.add_subplot(1, 3, 3) 54 | _ = met.plot( 55 | step * arange(1, len(wasserstein) + 1), 56 | 100 * (1 - wasserstein), 57 | label=r"$d_W$", 58 | color="red", 59 | ) 60 | met.axis([0, len(wasserstein), 0, 100]) 61 | met.set_xticks(arange(0, step * len(wasserstein) + 1, step)) 62 | met.set_xlabel("Iteration") 63 | met.set_ylabel(r"Recovery rate (in %)") 64 | met.legend(loc="upper left") 65 | 66 | plt.tight_layout(0.5) 67 | plt.savefig(figname + ".png") 68 | 69 | 70 | def _generate_testbed( 71 | kernel_init_len, 72 | n_nonzero_coefs, 73 | n_kernels, 74 | n_samples=10, 75 | n_features=5, 76 | n_dims=3, 77 | snr=1000, 78 | ): 79 | """Generate a dataset from a random dictionary 80 | 81 | Generate a random dictionary and a dataset, where samples are combination of 82 | n_nonzero_coefs dictionary atoms. Noise is added, based on SNR value, with 83 | 1000 indicated that no noise should be added. 84 | Return the dictionary, the dataset and an array indicated how atoms are combined 85 | to obtain each sample 86 | """ 87 | dico = [randn(kernel_init_len, n_dims) for i in range(n_kernels)] 88 | for i in range(len(dico)): 89 | dico[i] /= norm(dico[i], "fro") 90 | 91 | signals = list() 92 | decomposition = list() 93 | for _ in range(n_samples): 94 | s = np.zeros(shape=(n_features, n_dims)) 95 | d = np.zeros(shape=(n_nonzero_coefs, 3)) 96 | rk = permutation(range(n_kernels)) 97 | for j in range(n_nonzero_coefs): 98 | k_idx = rk[j] 99 | k_amplitude = 3.0 * rand() + 1.0 100 | k_offset = randint(n_features - kernel_init_len + 1) 101 | s[k_offset : k_offset + kernel_init_len, :] += k_amplitude * dico[k_idx] 102 | d[j, :] = array([k_amplitude, k_offset, k_idx]) 103 | decomposition.append(d) 104 | noise = randn(n_features, n_dims) 105 | if snr == 1000: 106 | alpha = 0 107 | else: 108 | ps = norm(s, "fro") 109 | pn = norm(noise, "fro") 110 | alpha = ps / (pn * 10 ** (snr / 20.0)) 111 | signals.append(s + alpha * noise) 112 | signals = np.array(signals) 113 | 114 | return dico, signals, decomposition 115 | 116 | 117 | rng_global = RandomState(1) 118 | n_samples, n_dims = 1500, 1 119 | n_features = kernel_init_len = 20 120 | n_nonzero_coefs = 3 121 | n_kernels, max_iter, n_iter, learning_rate = 50, 10, 3, 1.5 122 | n_jobs, batch_size = -1, 10 123 | detect_rate, wasserstein, objective_error = list(), list(), list() 124 | 125 | generating_dict, X, code = _generate_testbed( 126 | kernel_init_len, n_nonzero_coefs, n_kernels, n_samples, n_features, n_dims 127 | ) 128 | 129 | # # Create a dictionary 130 | # dict_init = [rand(kernel_init_len, n_dims) for i in range(n_kernels)] 131 | # for i in range(len(dict_init)): 132 | # dict_init[i] /= norm(dict_init[i], 'fro') 133 | dict_init = None 134 | 135 | learned_dict = MiniBatchMultivariateDictLearning( 136 | n_kernels=n_kernels, 137 | batch_size=batch_size, 138 | n_iter=n_iter, 139 | n_nonzero_coefs=n_nonzero_coefs, 140 | n_jobs=n_jobs, 141 | learning_rate=learning_rate, 142 | kernel_init_len=kernel_init_len, 143 | verbose=1, 144 | dict_init=dict_init, 145 | random_state=rng_global, 146 | ) 147 | 148 | # Update learned dictionary at each iteration and compute a distance 149 | # with the generating dictionary 150 | for _ in range(max_iter): 151 | learned_dict = learned_dict.partial_fit(X) 152 | # Compute the detection rate 153 | detect_rate.append(detection_rate(learned_dict.kernels_, generating_dict, 0.99)) 154 | # Compute the Wasserstein distance 155 | wasserstein.append(emd(learned_dict.kernels_, generating_dict, "chordal", scale=True)) 156 | # Get the objective error 157 | objective_error.append(learned_dict.error_.sum()) 158 | 159 | plot_univariate( 160 | array(objective_error), 161 | array(detect_rate), 162 | array(wasserstein), 163 | n_iter, 164 | "univariate-case", 165 | ) 166 | 167 | 168 | # Another possibility is to rely on a callback function such as 169 | def callback_distance(loc): 170 | ii, iter_offset = loc["ii"], loc["iter_offset"] 171 | n_batches = loc["n_batches"] 172 | if np.mod((ii - iter_offset) / int(n_batches), n_iter) == 0: 173 | # Compute distance only every 5 iterations, as in previous case 174 | d = loc["dict_obj"] 175 | d.wasserstein.append( 176 | emd(loc["dictionary"], d.generating_dict, "chordal", scale=True) 177 | ) 178 | d.detect_rate.append(detection_rate(loc["dictionary"], d.generating_dict, 0.99)) 179 | d.objective_error.append(loc["current_cost"]) 180 | 181 | 182 | # reinitializing the random generator 183 | learned_dict2 = MiniBatchMultivariateDictLearning( 184 | n_kernels=n_kernels, 185 | batch_size=batch_size, 186 | n_iter=max_iter * n_iter, 187 | n_nonzero_coefs=n_nonzero_coefs, 188 | callback=callback_distance, 189 | n_jobs=n_jobs, 190 | learning_rate=learning_rate, 191 | kernel_init_len=kernel_init_len, 192 | verbose=1, 193 | dict_init=dict_init, 194 | random_state=rng_global, 195 | ) 196 | learned_dict2.generating_dict = list(generating_dict) 197 | learned_dict2.wasserstein = list() 198 | learned_dict2.detect_rate = list() 199 | learned_dict2.objective_error = list() 200 | 201 | learned_dict2 = learned_dict2.fit(X) 202 | 203 | plot_univariate( 204 | array(learned_dict2.objective_error), 205 | array(learned_dict2.detect_rate), 206 | array(learned_dict2.wasserstein), 207 | n_iter=1, 208 | figname="univariate-case-callback", 209 | ) 210 | -------------------------------------------------------------------------------- /experiments/experiment_dictionary_recovering.py: -------------------------------------------------------------------------------- 1 | """Dictionary recovering experiment for univariate random dataset""" 2 | import pickle 3 | from os.path import exists 4 | 5 | import matplotlib.pyplot as plt 6 | from numpy import arange, array, zeros 7 | from numpy.linalg import norm 8 | from numpy.random import RandomState, permutation, rand, randint, randn 9 | 10 | from dict_metrics import beta_dist, detection_rate, emd, hausdorff 11 | from mdla import MiniBatchMultivariateDictLearning 12 | 13 | 14 | def _generate_testbed( 15 | kernel_init_len, 16 | n_nonzero_coefs, 17 | n_kernels, 18 | n_samples=10, 19 | n_features=5, 20 | n_dims=3, 21 | snr=1000, 22 | ): 23 | """Generate a dataset from a random dictionary 24 | 25 | Generate a random dictionary and a dataset, where samples are combination 26 | of n_nonzero_coefs dictionary atoms. Noise is added, based on SNR value, 27 | with 1000 indicated that no noise should be added. 28 | Return the dictionary, the dataset and an array indicated how atoms are 29 | combined to obtain each sample 30 | """ 31 | dico = [randn(kernel_init_len, n_dims) for i in range(n_kernels)] 32 | for i in range(len(dico)): 33 | dico[i] /= norm(dico[i], "fro") 34 | 35 | signals = list() 36 | decomposition = list() 37 | for _ in range(n_samples): 38 | s = zeros(shape=(n_features, n_dims)) 39 | d = zeros(shape=(n_nonzero_coefs, 3)) 40 | rk = permutation(range(n_kernels)) 41 | for j in range(n_nonzero_coefs): 42 | k_idx = rk[j] 43 | k_amplitude = 3.0 * rand() + 1.0 44 | k_offset = randint(n_features - kernel_init_len + 1) 45 | s[k_offset : k_offset + kernel_init_len, :] += k_amplitude * dico[k_idx] 46 | d[j, :] = array([k_amplitude, k_offset, k_idx]) 47 | decomposition.append(d) 48 | noise = randn(n_features, n_dims) 49 | if snr == 1000: 50 | alpha = 0 51 | else: 52 | ps = norm(s, "fro") 53 | pn = norm(noise, "fro") 54 | alpha = ps / (pn * 10 ** (snr / 20.0)) 55 | signals.append(s + alpha * noise) 56 | signals = array(signals) 57 | 58 | return dico, signals, decomposition 59 | 60 | 61 | def plot_recov(wc, wfs, hc, hfs, bd, dr99, dr97, n_iter, figname): 62 | snr = ["30", "20", "10"] 63 | fig = plt.figure(figsize=(18, 10)) 64 | for i, s in enumerate(snr): 65 | # plotting data from detection rate 66 | detection = fig.add_subplot(3, 4, i * 4 + 1) 67 | det99 = detection.boxplot(dr99[i, :, :] / 100.0) 68 | plt.setp(det99["medians"], color="green") 69 | plt.setp(det99["caps"], color="green") 70 | plt.setp(det99["boxes"], color="green") 71 | plt.setp(det99["fliers"], color="green") 72 | plt.setp(det99["whiskers"], color="green") 73 | medianlt99 = [median.get_ydata()[0] for n, median in enumerate(det99["medians"])] 74 | _ = detection.plot( 75 | arange(1, n_iter + 1), 76 | medianlt99, 77 | linewidth=1, 78 | color="green", 79 | label=r"$c_\operatorname{99}$", 80 | ) 81 | det97 = detection.boxplot(dr97[i, :, :] / 100.0) 82 | plt.setp(det97["medians"], color="magenta") 83 | plt.setp(det97["caps"], color="magenta") 84 | plt.setp(det97["boxes"], color="magenta") 85 | plt.setp(det97["fliers"], color="magenta") 86 | plt.setp(det97["whiskers"], color="magenta") 87 | medianlt97 = [median.get_ydata()[0] for n, median in enumerate(det97["medians"])] 88 | _ = detection.plot( 89 | arange(1, n_iter + 1), 90 | medianlt97, 91 | linewidth=1, 92 | color="magenta", 93 | label=r"$c_\operatorname{97}$", 94 | ) 95 | detection.axis([0, n_iter, 0, 1]) 96 | detection.set_xticks(arange(0, n_iter + 1, 10)) 97 | detection.set_xticklabels([]) 98 | detection.legend(loc="lower right") 99 | 100 | # plotting data from hausdorff metric 101 | methaus = fig.add_subplot(3, 4, i * 4 + 2) 102 | hausch = methaus.boxplot(1 - hc[i, :, :]) 103 | plt.setp(hausch["medians"], color="cyan") 104 | plt.setp(hausch["caps"], color="cyan") 105 | plt.setp(hausch["boxes"], color="cyan") 106 | plt.setp(hausch["fliers"], color="cyan") 107 | plt.setp(hausch["whiskers"], color="cyan") 108 | medianhc = [median.get_ydata()[0] for n, median in enumerate(hausch["medians"])] 109 | _ = methaus.plot( 110 | arange(1, n_iter + 1), medianhc, linewidth=1, label=r"$1-d_H^c$", color="cyan" 111 | ) 112 | hausfs = methaus.boxplot(1 - hfs[i, :, :]) 113 | plt.setp(hausfs["medians"], color="yellow") 114 | plt.setp(hausfs["caps"], color="yellow") 115 | plt.setp(hausfs["boxes"], color="yellow") 116 | plt.setp(hausfs["fliers"], color="yellow") 117 | plt.setp(hausfs["whiskers"], color="yellow") 118 | medianhfs = [median.get_ydata()[0] for n, median in enumerate(hausfs["medians"])] 119 | _ = methaus.plot( 120 | arange(1, n_iter + 1), 121 | medianhfs, 122 | linewidth=1, 123 | label=r"$1-d_H^{fs}$", 124 | color="yellow", 125 | ) 126 | methaus.axis([0, n_iter, 0, 1]) 127 | methaus.set_xticks(arange(0, n_iter + 1, 10)) 128 | methaus.set_xticklabels([]) 129 | methaus.set_yticklabels([]) 130 | methaus.legend(loc="lower right") 131 | 132 | # plotting data from wasserstein metric 133 | metwass = fig.add_subplot(3, 4, i * 4 + 3) 134 | wassch = metwass.boxplot(1 - wc[i, :, :]) 135 | plt.setp(wassch["medians"], color="red") 136 | plt.setp(wassch["caps"], color="red") 137 | plt.setp(wassch["boxes"], color="red") 138 | plt.setp(wassch["fliers"], color="red") 139 | plt.setp(wassch["whiskers"], color="red") 140 | medianwc = [median.get_ydata()[0] for n, median in enumerate(wassch["medians"])] 141 | _ = metwass.plot( 142 | arange(1, n_iter + 1), medianwc, linewidth=1, label=r"$1-d_W^c$", color="red" 143 | ) 144 | wassfs = metwass.boxplot(1 - wfs[i, :, :]) 145 | plt.setp(wassfs["medians"], color="blue") 146 | plt.setp(wassfs["caps"], color="blue") 147 | plt.setp(wassfs["boxes"], color="blue") 148 | plt.setp(wassfs["fliers"], color="blue") 149 | plt.setp(wassfs["whiskers"], color="blue") 150 | medianwfs = [median.get_ydata()[0] for n, median in enumerate(wassfs["medians"])] 151 | _ = metwass.plot( 152 | arange(1, n_iter + 1), 153 | medianwfs, 154 | linewidth=1, 155 | label=r"$1-d_W^{fs}$", 156 | color="blue", 157 | ) 158 | metwass.axis([0, n_iter, 0, 1]) 159 | metwass.set_xticks(arange(0, n_iter + 1, 10)) 160 | metwass.set_xticklabels([]) 161 | metwass.set_yticklabels([]) 162 | metwass.legend(loc="lower right") 163 | metwass.set_title(" ") 164 | 165 | # plotting data from Beta 166 | metbeta = fig.add_subplot(3, 4, i * 4 + 4) 167 | betad = metbeta.boxplot(1 - bd[i, :, :]) 168 | plt.setp(betad["medians"], color="black") 169 | plt.setp(betad["caps"], color="black") 170 | plt.setp(betad["boxes"], color="black") 171 | plt.setp(betad["fliers"], color="black") 172 | plt.setp(betad["whiskers"], color="black") 173 | medianbd = [median.get_ydata()[0] for n, median in enumerate(betad["medians"])] 174 | _ = metbeta.plot( 175 | arange(1, n_iter + 1), 176 | medianbd, 177 | linewidth=1, 178 | label=r"$1-d_\beta$", 179 | color="black", 180 | ) 181 | metbeta.axis([0, n_iter, 0, 1]) 182 | metbeta.set_xticks(arange(0, n_iter + 1, 10)) 183 | metbeta.set_xticklabels([]) 184 | metbeta.set_yticklabels([]) 185 | metbeta.legend(loc="lower right") 186 | 187 | metbeta.annotate( 188 | "SNR " + s, 189 | xy=(0.51, 1.0 - i * 1.0 / 3.0 + i * 0.01 - 0.001), 190 | xycoords="figure fraction", 191 | horizontalalignment="center", 192 | verticalalignment="top", 193 | fontsize="large", 194 | ) 195 | 196 | detection.set_xticks(arange(0, n_iter + 1, 10)) 197 | detection.set_xticklabels(arange(0, n_iter + 1, 10)) 198 | methaus.set_xticks(arange(0, n_iter + 1, 10)) 199 | methaus.set_xticklabels(arange(0, n_iter + 1, 10)) 200 | metwass.set_xticks(arange(0, n_iter + 1, 10)) 201 | metwass.set_xticklabels(arange(0, n_iter + 1, 10)) 202 | metbeta.set_xticks(arange(0, n_iter + 1, 10)) 203 | metbeta.set_xticklabels(arange(0, n_iter + 1, 10)) 204 | plt.tight_layout(1.2) 205 | plt.savefig(figname + ".png") 206 | 207 | 208 | def callback_recovery(loc): 209 | d = loc["dict_obj"] 210 | d.wc.append(emd(loc["dictionary"], d.generating_dict, "chordal", scale=True)) 211 | d.wfs.append(emd(loc["dictionary"], d.generating_dict, "fubinistudy", scale=True)) 212 | d.hc.append(hausdorff(loc["dictionary"], d.generating_dict, "chordal", scale=True)) 213 | d.hfs.append( 214 | hausdorff(loc["dictionary"], d.generating_dict, "fubinistudy", scale=True) 215 | ) 216 | d.bd.append(beta_dist(d.generating_dict, loc["dictionary"])) 217 | d.dr99.append(detection_rate(loc["dictionary"], d.generating_dict, 0.99)) 218 | d.dr97.append(detection_rate(loc["dictionary"], d.generating_dict, 0.97)) 219 | 220 | 221 | rng_global = RandomState(1) 222 | n_samples, n_dims, n_kernels = 1500, 1, 50 223 | n_features = kernel_init_len = 20 224 | n_nonzero_coefs, learning_rate = 3, 1.5 225 | n_experiments, n_iter = 15, 25 226 | snr = [30, 20, 10] 227 | n_snr = len(snr) 228 | n_jobs, batch_size = -1, 60 229 | 230 | if exists("expe_reco.pck"): 231 | with open("expe_reco.pck", "r") as f: 232 | o = pickle.load(f) 233 | wc, wfs, hc, hfs = o["wc"], o["wfs"], o["hc"], o["hfs"] 234 | bd, dr99, dr97 = o["bd"], o["dr99"], o["dr97"] 235 | plot_recov(wc, wfs, hc, hfs, bd, dr99, dr97, n_iter, "univariate_recov") 236 | else: 237 | wc = zeros((n_snr, n_experiments, n_iter)) 238 | wfs = zeros((n_snr, n_experiments, n_iter)) 239 | hc = zeros((n_snr, n_experiments, n_iter)) 240 | hfs = zeros((n_snr, n_experiments, n_iter)) 241 | bd = zeros((n_snr, n_experiments, n_iter)) 242 | dr99 = zeros((n_snr, n_experiments, n_iter)) 243 | dr97 = zeros((n_snr, n_experiments, n_iter)) 244 | 245 | for i, s in enumerate(snr): 246 | for e in range(n_experiments): 247 | g, X, code = _generate_testbed( 248 | kernel_init_len, 249 | n_nonzero_coefs, 250 | n_kernels, 251 | n_samples, 252 | n_features, 253 | n_dims, 254 | s, 255 | ) 256 | d = MiniBatchMultivariateDictLearning( 257 | n_kernels=n_kernels, 258 | batch_size=batch_size, 259 | n_iter=n_iter, 260 | n_nonzero_coefs=n_nonzero_coefs, 261 | callback=callback_recovery, 262 | n_jobs=n_jobs, 263 | learning_rate=learning_rate, 264 | kernel_init_len=kernel_init_len, 265 | verbose=1, 266 | random_state=rng_global, 267 | ) 268 | d.generating_dict = list(g) 269 | d.wc, d.wfs, d.hc, d.hfs = list(), list(), list(), list() 270 | d.bd, d.dr99, d.dr97 = list(), list(), list() 271 | print("\nExperiment", e + 1, "on", n_experiments) 272 | d = d.fit(X) 273 | wc[i, e, :] = array(d.wc) 274 | wfs[i, e, :] = array(d.wfs) 275 | hc[i, e, :] = array(d.hc) 276 | hfs[i, e, :] = array(d.hfs) 277 | dr99[i, e, :] = array(d.dr99) 278 | dr97[i, e, :] = array(d.dr97) 279 | bd[i, e, :] = array(d.bd) 280 | with open("expe_reco.pck", "w") as f: 281 | o = { 282 | "wc": wc, 283 | "wfs": wfs, 284 | "hc": hc, 285 | "hfs": hfs, 286 | "bd": bd, 287 | "dr99": dr99, 288 | "dr97": dr97, 289 | } 290 | pickle.dump(o, f) 291 | plot_recov(wc, wfs, hc, hfs, bd, dr99, dr97, n_iter, "univariate_recov") 292 | -------------------------------------------------------------------------------- /experiments/experiment_multivariate_recovering.py: -------------------------------------------------------------------------------- 1 | """Dictionary recovering experiment for multivariate random dataset""" 2 | import os 3 | import pickle 4 | from os.path import exists 5 | 6 | import matplotlib.pyplot as plt 7 | from numpy import arange, array, zeros 8 | from numpy.linalg import norm 9 | from numpy.random import RandomState, permutation, rand, randint, randn 10 | 11 | from dict_metrics import detection_rate, emd, hausdorff 12 | from mdla import MiniBatchMultivariateDictLearning 13 | 14 | 15 | display = os.environ.get("DISPLAY") 16 | if display is None: 17 | # if launched from a screen 18 | import matplotlib 19 | 20 | matplotlib.use("Agg") 21 | 22 | 23 | def _generate_testbed( 24 | kernel_init_len, 25 | n_nonzero_coefs, 26 | n_kernels, 27 | n_samples=10, 28 | n_features=5, 29 | n_dims=3, 30 | snr=1000, 31 | ): 32 | """Generate a dataset from a random dictionary 33 | 34 | Generate a random dictionary and a dataset, where samples are combination 35 | of n_nonzero_coefs dictionary atoms. Noise is added, based on SNR value, 36 | with 1000 indicated that no noise should be added. 37 | Return the dictionary, the dataset and an array indicated how atoms are 38 | combined to obtain each sample 39 | """ 40 | dico = [randn(kernel_init_len, n_dims) for i in range(n_kernels)] 41 | for i in range(len(dico)): 42 | dico[i] /= norm(dico[i], "fro") 43 | 44 | signals = list() 45 | decomposition = list() 46 | for _ in range(n_samples): 47 | s = zeros(shape=(n_features, n_dims)) 48 | d = zeros(shape=(n_nonzero_coefs, 3)) 49 | rk = permutation(range(n_kernels)) 50 | for j in range(n_nonzero_coefs): 51 | k_idx = rk[j] 52 | k_amplitude = 3.0 * rand() + 1.0 53 | k_offset = randint(n_features - kernel_init_len + 1) 54 | s[k_offset : k_offset + kernel_init_len, :] += k_amplitude * dico[k_idx] 55 | d[j, :] = array([k_amplitude, k_offset, k_idx]) 56 | decomposition.append(d) 57 | noise = randn(n_features, n_dims) 58 | if snr == 1000: 59 | alpha = 0 60 | else: 61 | ps = norm(s, "fro") 62 | pn = norm(noise, "fro") 63 | alpha = ps / (pn * 10 ** (snr / 20.0)) 64 | signals.append(s + alpha * noise) 65 | signals = array(signals) 66 | 67 | return dico, signals, decomposition 68 | 69 | 70 | def plot_boxes(fig, data, color="blue", n_iter=100, label=""): 71 | bp = fig.boxplot(data) 72 | plt.setp(bp["medians"], color=color) 73 | plt.setp(bp["caps"], color=color) 74 | plt.setp(bp["boxes"], color=color) 75 | plt.setp(bp["fliers"], color=color) 76 | plt.setp(bp["whiskers"], color=color) 77 | med = [m.get_ydata()[0] for n, m in enumerate(bp["medians"])] 78 | _ = fig.plot(arange(1, n_iter + 1), med, linewidth=1, color=color, label=label) 79 | 80 | 81 | def plot_recov_all( 82 | wc, wfs, wcpa, wbc, wg, wfb, hc, hfs, hcpa, hbc, hg, hfb, dr99, dr97, n_iter, figname 83 | ): 84 | snr = ["30", "20", "10"] 85 | fig = plt.figure(figsize=(18, 10)) 86 | for i, s in enumerate(snr): 87 | # plotting data from detection rate 88 | detection = fig.add_subplot(3, 3, i * 3 + 1) 89 | plot_boxes( 90 | detection, dr99[i, :, :] / 100.0, "green", n_iter, r"$c_\operatorname{99}$" 91 | ) 92 | plot_boxes( 93 | detection, dr97[i, :, :] / 100.0, "magenta", n_iter, r"$c_\operatorname{97}$" 94 | ) 95 | detection.axis([0, n_iter, 0, 1]) 96 | detection.set_xticks(arange(0, n_iter + 1, 10)) 97 | detection.set_xticklabels([]) 98 | detection.legend(loc="lower right") 99 | 100 | methaus = fig.add_subplot(3, 3, i * 3 + 2) 101 | plot_boxes(methaus, 1 - hc[i, :, :], "chartreuse", n_iter, r"$1-d_H^c$") 102 | plot_boxes(methaus, 1 - hcpa[i, :, :], "red", n_iter, r"$1-d_H^{cpa}$") 103 | plot_boxes(methaus, 1 - hfs[i, :, :], "magenta", n_iter, r"$1-d_H^{fs}$") 104 | plot_boxes(methaus, 1 - hbc[i, :, :], "blue", n_iter, r"$1-d_H^{bc}$") 105 | plot_boxes(methaus, 1 - hg[i, :, :], "deepskyblue", n_iter, r"$1-d_H^{g}$") 106 | plot_boxes(methaus, 1 - hfb[i, :, :], "orange", n_iter, r"$1-d_H^{fb}$") 107 | 108 | methaus.axis([0, n_iter, 0, 1]) 109 | methaus.set_xticks(arange(0, n_iter + 1, 10)) 110 | methaus.set_xticklabels([]) 111 | methaus.set_yticklabels([]) 112 | methaus.legend(loc="lower right") 113 | 114 | metwass = fig.add_subplot(3, 3, i * 3 + 3) 115 | plot_boxes(metwass, 1 - wc[i, :, :], "chartreuse", n_iter, r"$1-d_W^c$") 116 | plot_boxes(metwass, 1 - wcpa[i, :, :], "red", n_iter, r"$1-d_W^{cpa}$") 117 | plot_boxes(metwass, 1 - wfs[i, :, :], "magenta", n_iter, r"$1-d_W^{fs}$") 118 | plot_boxes(metwass, 1 - wbc[i, :, :], "blue", n_iter, r"$1-d_W^{bc}$") 119 | plot_boxes(metwass, 1 - wg[i, :, :], "deepskyblue", n_iter, r"$1-d_W^{g}$") 120 | plot_boxes(metwass, 1 - wfb[i, :, :], "orange", n_iter, r"$1-d_W^{fb}$") 121 | metwass.axis([0, n_iter, 0, 1]) 122 | metwass.set_xticks(arange(0, n_iter + 1, 10)) 123 | metwass.set_xticklabels([]) 124 | metwass.set_yticklabels([]) 125 | metwass.legend(loc="lower right") 126 | metwass.set_title(" ") 127 | 128 | metwass.annotate( 129 | "SNR " + s, 130 | xy=(0.51, 1.0 - i * 1.0 / 3.0 + i * 0.01 - 0.001), 131 | xycoords="figure fraction", 132 | horizontalalignment="center", 133 | verticalalignment="top", 134 | fontsize="large", 135 | ) 136 | 137 | detection.set_xticks(arange(0, n_iter + 1, 10)) 138 | detection.set_xticklabels(arange(0, n_iter + 1, 10)) 139 | methaus.set_xticks(arange(0, n_iter + 1, 10)) 140 | methaus.set_xticklabels(arange(0, n_iter + 1, 10)) 141 | metwass.set_xticks(arange(0, n_iter + 1, 10)) 142 | metwass.set_xticklabels(arange(0, n_iter + 1, 10)) 143 | plt.tight_layout(1.2) 144 | plt.savefig(figname + ".png") 145 | 146 | 147 | def plot_recov(wc, wfs, hc, hfs, dr99, dr97, n_iter, figname): 148 | snr = ["30", "20", "10"] 149 | fig = plt.figure(figsize=(18, 10)) 150 | for i, s in enumerate(snr): 151 | # plotting data from detection rate 152 | detection = fig.add_subplot(3, 3, i * 3 + 1) 153 | plot_boxes( 154 | detection, dr99[i, :, :] / 100.0, "green", n_iter, r"$c_\operatorname{99}$" 155 | ) 156 | plot_boxes( 157 | detection, dr97[i, :, :] / 100.0, "magenta", n_iter, r"$c_\operatorname{97}$" 158 | ) 159 | detection.axis([0, n_iter, 0, 1]) 160 | detection.set_xticks(arange(0, n_iter + 1, 10)) 161 | detection.set_xticklabels([]) 162 | detection.legend(loc="lower right") 163 | 164 | # plotting data from hausdorff metric 165 | methaus = fig.add_subplot(3, 3, i * 3 + 2) 166 | plot_boxes(methaus, 1 - hc[i, :, :], "cyan", n_iter, r"$1-d_H^c$") 167 | plot_boxes(methaus, 1 - hfs[i, :, :], "yellow", n_iter, r"$1-d_H^{fs}$") 168 | methaus.axis([0, n_iter, 0, 1]) 169 | methaus.set_xticks(arange(0, n_iter + 1, 10)) 170 | methaus.set_xticklabels([]) 171 | methaus.set_yticklabels([]) 172 | methaus.legend(loc="lower right") 173 | 174 | # plotting data from wasserstein metric 175 | metwass = fig.add_subplot(3, 3, i * 3 + 3) 176 | plot_boxes(metwass, 1 - wc[i, :, :], "red", n_iter, r"$1-d_W^c$") 177 | plot_boxes(metwass, 1 - wfs[i, :, :], "blue", n_iter, r"$1-d_W^{fs}$") 178 | metwass.axis([0, n_iter, 0, 1]) 179 | metwass.set_xticks(arange(0, n_iter + 1, 10)) 180 | metwass.set_xticklabels([]) 181 | metwass.set_yticklabels([]) 182 | metwass.legend(loc="lower right") 183 | metwass.set_title(" ") 184 | 185 | metwass.annotate( 186 | "SNR " + s, 187 | xy=(0.51, 1.0 - i * 1.0 / 3.0 + i * 0.01 - 0.001), 188 | xycoords="figure fraction", 189 | horizontalalignment="center", 190 | verticalalignment="top", 191 | fontsize="large", 192 | ) 193 | 194 | detection.set_xticks(arange(0, n_iter + 1, 10)) 195 | detection.set_xticklabels(arange(0, n_iter + 1, 10)) 196 | methaus.set_xticks(arange(0, n_iter + 1, 10)) 197 | methaus.set_xticklabels(arange(0, n_iter + 1, 10)) 198 | metwass.set_xticks(arange(0, n_iter + 1, 10)) 199 | metwass.set_xticklabels(arange(0, n_iter + 1, 10)) 200 | plt.tight_layout(1.2) 201 | plt.savefig(figname + ".png") 202 | 203 | 204 | def callback_recovery(loc): 205 | d = loc["dict_obj"] 206 | d.wc.append(emd(loc["dictionary"], d.generating_dict, "chordal", scale=True)) 207 | d.wfs.append(emd(loc["dictionary"], d.generating_dict, "fubinistudy", scale=True)) 208 | d.wcpa.append( 209 | emd(loc["dictionary"], d.generating_dict, "chordal_principal_angles", scale=True) 210 | ) 211 | d.wbc.append(emd(loc["dictionary"], d.generating_dict, "binetcauchy", scale=True)) 212 | d.wg.append(emd(loc["dictionary"], d.generating_dict, "geodesic", scale=True)) 213 | d.wfb.append(emd(loc["dictionary"], d.generating_dict, "frobenius", scale=True)) 214 | d.hc.append(hausdorff(loc["dictionary"], d.generating_dict, "chordal", scale=True)) 215 | d.hfs.append( 216 | hausdorff(loc["dictionary"], d.generating_dict, "fubinistudy", scale=True) 217 | ) 218 | d.hcpa.append( 219 | hausdorff( 220 | loc["dictionary"], d.generating_dict, "chordal_principal_angles", scale=True 221 | ) 222 | ) 223 | d.hbc.append( 224 | hausdorff(loc["dictionary"], d.generating_dict, "binetcauchy", scale=True) 225 | ) 226 | d.hg.append(hausdorff(loc["dictionary"], d.generating_dict, "geodesic", scale=True)) 227 | d.hfb.append(hausdorff(loc["dictionary"], d.generating_dict, "frobenius", scale=True)) 228 | d.dr99.append(detection_rate(loc["dictionary"], d.generating_dict, 0.99)) 229 | d.dr97.append(detection_rate(loc["dictionary"], d.generating_dict, 0.97)) 230 | 231 | 232 | rng_global = RandomState(1) 233 | n_samples, n_dims, n_kernels = 1500, 5, 50 234 | n_features = kernel_init_len = 20 235 | n_nonzero_coefs, learning_rate = 3, 1.5 236 | n_experiments, n_iter = 15, 25 237 | snr = [30, 20, 10] 238 | n_snr = len(snr) 239 | n_jobs, batch_size = -1, 60 240 | 241 | backup_fname = "expe_multi_reco_all.pck" 242 | 243 | if exists(backup_fname): 244 | with open(backup_fname, "r") as f: 245 | o = pickle.load(f) 246 | wc, wfs, hc, hfs = o["wc"], o["wfs"], o["hc"], o["hfs"] 247 | wcpa, wbc, wg, wfb = o["wcpa"], o["wbc"], o["wg"], o["wfb"] 248 | hcpa, hbc, hg, hfb = o["hcpa"], o["hbc"], o["hg"], o["hfb"] 249 | dr99, dr97 = o["dr99"], o["dr97"] 250 | plot_recov(wc, wfs, hc, hfs, dr99, dr97, n_iter, "multivariate_recov") 251 | else: 252 | wc = zeros((n_snr, n_experiments, n_iter)) 253 | wfs = zeros((n_snr, n_experiments, n_iter)) 254 | wcpa = zeros((n_snr, n_experiments, n_iter)) 255 | wbc = zeros((n_snr, n_experiments, n_iter)) 256 | wg = zeros((n_snr, n_experiments, n_iter)) 257 | wfb = zeros((n_snr, n_experiments, n_iter)) 258 | hc = zeros((n_snr, n_experiments, n_iter)) 259 | hfs = zeros((n_snr, n_experiments, n_iter)) 260 | hcpa = zeros((n_snr, n_experiments, n_iter)) 261 | hbc = zeros((n_snr, n_experiments, n_iter)) 262 | hg = zeros((n_snr, n_experiments, n_iter)) 263 | hfb = zeros((n_snr, n_experiments, n_iter)) 264 | dr99 = zeros((n_snr, n_experiments, n_iter)) 265 | dr97 = zeros((n_snr, n_experiments, n_iter)) 266 | 267 | for i, s in enumerate(snr): 268 | for e in range(n_experiments): 269 | g, X, code = _generate_testbed( 270 | kernel_init_len, 271 | n_nonzero_coefs, 272 | n_kernels, 273 | n_samples, 274 | n_features, 275 | n_dims, 276 | s, 277 | ) 278 | d = MiniBatchMultivariateDictLearning( 279 | n_kernels=n_kernels, 280 | batch_size=batch_size, 281 | n_iter=n_iter, 282 | n_nonzero_coefs=n_nonzero_coefs, 283 | callback=callback_recovery, 284 | n_jobs=n_jobs, 285 | learning_rate=learning_rate, 286 | kernel_init_len=kernel_init_len, 287 | verbose=1, 288 | random_state=rng_global, 289 | ) 290 | d.generating_dict = list(g) 291 | d.wc, d.wfs, d.hc, d.hfs = list(), list(), list(), list() 292 | d.wcpa, d.wbc, d.wg, d.wfb = list(), list(), list(), list() 293 | d.hcpa, d.hbc, d.hg, d.hfb = list(), list(), list(), list() 294 | d.dr99, d.dr97 = list(), list() 295 | print("\nExperiment", e + 1, "on", n_experiments) 296 | d = d.fit(X) 297 | wc[i, e, :] = array(d.wc) 298 | wfs[i, e, :] = array(d.wfs) 299 | hc[i, e, :] = array(d.hc) 300 | hfs[i, e, :] = array(d.hfs) 301 | wcpa[i, e, :] = array(d.wcpa) 302 | wbc[i, e, :] = array(d.wbc) 303 | wg[i, e, :] = array(d.wg) 304 | wfb[i, e, :] = array(d.wfb) 305 | hcpa[i, e, :] = array(d.hcpa) 306 | hbc[i, e, :] = array(d.hbc) 307 | hg[i, e, :] = array(d.hg) 308 | hfb[i, e, :] = array(d.hfb) 309 | dr99[i, e, :] = array(d.dr99) 310 | dr97[i, e, :] = array(d.dr97) 311 | # fmt: off 312 | with open(backup_fname, "w") as f: 313 | o = { 314 | "wc": wc, "wfs": wfs, "hc": hc, "hfs": hfs, "dr99": dr99, "dr97": dr97, 315 | "wcpa": wcpa, "wbc": wbc, "wg": wg, "wfb": wfb, "hcpa": hcpa, "hbc": hbc, 316 | "hg": hg, "hfb": hfb, 317 | } 318 | pickle.dump(o, f) 319 | # plot_recov(wc, wfs, hc, hfs, dr99, dr97, n_iter, "multivariate_recov") 320 | plot_recov_all( 321 | wc, wfs, wcpa, wbc, wg, wfb, hc, hfs, hcpa, hbc, hg, hfb, 322 | dr99, dr97, n_iter, "multivariate_recov_all", 323 | ) 324 | # fmt: on 325 | -------------------------------------------------------------------------------- /tests/test_mdla.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy.testing import assert_almost_equal, assert_array_almost_equal, assert_raises 3 | 4 | from mdla import ( 5 | MiniBatchMultivariateDictLearning, 6 | MultivariateDictLearning, 7 | SparseMultivariateCoder, 8 | multivariate_sparse_encode, 9 | reconstruct_from_code, 10 | ) 11 | 12 | 13 | rng_global = np.random.RandomState(0) 14 | 15 | 16 | def test_mdla_shapes(): 17 | n_samples, n_features, n_dims = 10, 5, 3 18 | X = [rng_global.randn(n_features, n_dims) for i in range(n_samples)] 19 | n_kernels = 8 20 | dico = MultivariateDictLearning( 21 | n_kernels=n_kernels, random_state=0, max_iter=10, verbose=5 22 | ).fit(X) 23 | for i in range(n_kernels): 24 | assert dico.kernels_[i].shape == (n_features, n_dims) 25 | 26 | dico = MiniBatchMultivariateDictLearning( 27 | n_kernels=n_kernels, random_state=0, verbose=5, n_iter=10 28 | ).fit(X) 29 | for i in range(n_kernels): 30 | assert dico.kernels_[i].shape == (n_features, n_dims) 31 | 32 | 33 | def test_multivariate_input_shape(): 34 | n_samples, n_features, n_dims = 10, 5, 3 35 | X = [rng_global.randn(n_features, n_dims) for i in range(n_samples)] 36 | n_kernels = 7 37 | n_dims_w = 6 38 | Xw = [rng_global.randn(n_features, n_dims_w) for i in range(n_samples)] 39 | 40 | dico = MultivariateDictLearning(n_kernels=n_kernels).fit(X) 41 | for i in range(n_kernels): 42 | assert dico.kernels_[i].shape == (n_features, n_dims) 43 | 44 | dico = MultivariateDictLearning(n_kernels=n_kernels) 45 | assert_raises(ValueError, dico.fit, Xw) 46 | 47 | dico = MiniBatchMultivariateDictLearning(n_kernels=n_kernels).fit(X) 48 | for i in range(n_kernels): 49 | assert dico.kernels_[i].shape == (n_features, n_dims) 50 | 51 | dico = MiniBatchMultivariateDictLearning(n_kernels=n_kernels) 52 | assert_raises(ValueError, dico.fit, Xw) 53 | 54 | dico = MiniBatchMultivariateDictLearning(n_kernels=n_kernels).partial_fit(X) 55 | for i in range(n_kernels): 56 | assert dico.kernels_[i].shape == (n_features, n_dims) 57 | 58 | dico = MiniBatchMultivariateDictLearning(n_kernels=n_kernels) 59 | assert_raises(ValueError, dico.partial_fit, Xw) 60 | 61 | 62 | def test_mdla_normalization(): 63 | n_samples, n_features, n_dims = 10, 5, 3 64 | X = [rng_global.randn(n_features, n_dims) for i in range(n_samples)] 65 | n_kernels = 8 66 | dico = MultivariateDictLearning( 67 | n_kernels=n_kernels, random_state=0, max_iter=2, verbose=1 68 | ).fit(X) 69 | for k in dico.kernels_: 70 | assert_almost_equal(np.linalg.norm(k, "fro"), 1.0) 71 | 72 | dico = MiniBatchMultivariateDictLearning( 73 | n_kernels=n_kernels, random_state=0, n_iter=2, verbose=1 74 | ).fit(X) 75 | for k in dico.kernels_: 76 | assert_almost_equal(np.linalg.norm(k, "fro"), 1.0) 77 | 78 | 79 | def test_callback(): 80 | n_samples, n_features, n_dims = 10, 5, 3 81 | X = [rng_global.randn(n_features, n_dims) for i in range(n_samples)] 82 | n_kernels = 8 83 | 84 | def my_callback(loc): 85 | _ = loc["dict_obj"] 86 | 87 | dico = MultivariateDictLearning( 88 | n_kernels=n_kernels, 89 | random_state=0, 90 | max_iter=2, 91 | n_nonzero_coefs=1, 92 | callback=my_callback, 93 | ) 94 | code = dico.fit(X).transform(X[0]) 95 | assert len(code[0]) <= 1 96 | dico = MiniBatchMultivariateDictLearning( 97 | n_kernels=n_kernels, 98 | random_state=0, 99 | n_iter=2, 100 | n_nonzero_coefs=1, 101 | callback=my_callback, 102 | ) 103 | code = dico.fit(X).transform(X[0]) 104 | assert len(code[0]) <= 1 105 | 106 | 107 | def test_mdla_nonzero_coefs(): 108 | n_samples, n_features, n_dims = 10, 5, 3 109 | X = [rng_global.randn(n_features, n_dims) for i in range(n_samples)] 110 | n_kernels = 8 111 | dico = MultivariateDictLearning( 112 | n_kernels=n_kernels, random_state=0, max_iter=3, n_nonzero_coefs=3, verbose=5 113 | ) 114 | code = dico.fit(X).transform(X[0]) 115 | assert len(code[0]) <= 3 116 | 117 | dico = MiniBatchMultivariateDictLearning( 118 | n_kernels=n_kernels, random_state=0, n_iter=3, n_nonzero_coefs=3, verbose=5 119 | ) 120 | code = dico.fit(X).transform(X[0]) 121 | assert len(code[0]) <= 3 122 | 123 | 124 | def test_X_array(): 125 | n_samples, n_features, n_dims = 10, 5, 3 126 | n_kernels = 8 127 | X = rng_global.randn(n_samples, n_features, n_dims) 128 | dico = MultivariateDictLearning( 129 | n_kernels=n_kernels, random_state=0, max_iter=3, n_nonzero_coefs=3, verbose=5 130 | ) 131 | code = dico.fit(X).transform(X[0]) 132 | assert len(code[0]) <= 3 133 | 134 | dico = MiniBatchMultivariateDictLearning( 135 | n_kernels=n_kernels, random_state=0, n_iter=3, n_nonzero_coefs=3, verbose=5 136 | ) 137 | code = dico.fit(X).transform(X[0]) 138 | assert len(code[0]) <= 3 139 | 140 | 141 | def test_mdla_shuffle(): 142 | n_samples, n_features, n_dims = 10, 5, 3 143 | X = [rng_global.randn(n_features, n_dims) for i in range(n_samples)] 144 | n_kernels = 8 145 | dico = MiniBatchMultivariateDictLearning( 146 | n_kernels=n_kernels, 147 | random_state=0, 148 | n_iter=3, 149 | n_nonzero_coefs=1, 150 | verbose=5, 151 | shuffle=False, 152 | ) 153 | code = dico.fit(X).transform(X[0]) 154 | assert len(code[0]) <= 1 155 | 156 | 157 | def test_n_kernels(): 158 | n_samples, n_features, n_dims = 10, 5, 3 159 | X = [rng_global.randn(n_features, n_dims) for i in range(n_samples)] 160 | dico = MultivariateDictLearning( 161 | random_state=0, max_iter=2, n_nonzero_coefs=1, verbose=5 162 | ).fit(X) 163 | assert len(dico.kernels_) == 2 * n_features 164 | 165 | dico = MiniBatchMultivariateDictLearning( 166 | random_state=0, n_iter=2, n_nonzero_coefs=1, verbose=5 167 | ).fit(X) 168 | assert len(dico.kernels_) == 2 * n_features 169 | 170 | dico = MiniBatchMultivariateDictLearning( 171 | random_state=0, n_iter=2, n_nonzero_coefs=1, verbose=5 172 | ).partial_fit(X) 173 | assert len(dico.kernels_) == 2 * n_features 174 | 175 | 176 | def test_mdla_nonzero_coef_errors(): 177 | n_samples, n_features, n_dims = 10, 5, 3 178 | X = [rng_global.randn(n_features, n_dims) for i in range(n_samples)] 179 | n_kernels = 8 180 | dico = MultivariateDictLearning( 181 | n_kernels=n_kernels, random_state=0, max_iter=2, n_nonzero_coefs=0 182 | ) 183 | assert_raises(ValueError, dico.fit, X) 184 | 185 | dico = MiniBatchMultivariateDictLearning( 186 | n_kernels=n_kernels, random_state=0, n_iter=2, n_nonzero_coefs=n_kernels + 1 187 | ) 188 | assert_raises(ValueError, dico.fit, X) 189 | 190 | 191 | def test_sparse_encode(): 192 | n_samples, n_features, n_dims = 10, 5, 3 193 | X = [rng_global.randn(n_features, n_dims) for i in range(n_samples)] 194 | n_kernels = 8 195 | dico = MultivariateDictLearning( 196 | n_kernels=n_kernels, random_state=0, max_iter=2, n_nonzero_coefs=1 197 | ) 198 | dico = dico.fit(X) 199 | _, code = multivariate_sparse_encode(X, dico, n_nonzero_coefs=1, n_jobs=-1, verbose=3) 200 | assert len(code[0]) <= 1 201 | 202 | 203 | def test_dict_init(): 204 | n_samples, n_features, n_dims = 10, 5, 3 205 | X = [rng_global.randn(n_features, n_dims) for i in range(n_samples)] 206 | n_kernels = 8 207 | d = [rng_global.randn(n_features, n_dims) for i in range(n_kernels)] 208 | for i in range(len(d)): 209 | d[i] /= np.linalg.norm(d[i], "fro") 210 | dico = MultivariateDictLearning( 211 | n_kernels=n_kernels, 212 | random_state=0, 213 | max_iter=1, 214 | n_nonzero_coefs=1, 215 | learning_rate=0.0, 216 | dict_init=d, 217 | verbose=5, 218 | ).fit(X) 219 | dico = dico.fit(X) 220 | for i in range(n_kernels): 221 | assert_array_almost_equal(dico.kernels_[i], d[i]) 222 | # code = dico.fit(X).transform(X[0]) 223 | # assert (len(code[0]) > 1) 224 | 225 | dico = MiniBatchMultivariateDictLearning( 226 | n_kernels=n_kernels, 227 | random_state=0, 228 | n_iter=1, 229 | n_nonzero_coefs=1, 230 | dict_init=d, 231 | verbose=1, 232 | learning_rate=0.0, 233 | ).fit(X) 234 | dico = dico.fit(X) 235 | for i in range(n_kernels): 236 | assert_array_almost_equal(dico.kernels_[i], d[i]) 237 | # code = dico.fit(X).transform(X[0]) 238 | # assert (len(code[0]) <= 1) 239 | 240 | 241 | def test_mdla_dict_init(): 242 | n_kernels = 10 243 | n_samples, n_features, n_dims = 20, 5, 3 244 | X = [rng_global.randn(n_features, n_dims) for i in range(n_samples)] 245 | dict_init = [np.random.randn(n_features, n_dims) for i in range(n_kernels)] 246 | dico = MultivariateDictLearning( 247 | n_kernels=n_kernels, random_state=0, max_iter=10, dict_init=dict_init 248 | ).fit(X) 249 | diff = 0.0 250 | for i in range(n_kernels): 251 | diff = diff + (dico.kernels_[i] - dict_init[i]).sum() 252 | assert diff != 0 253 | 254 | 255 | def test_mdla_dict_update(): 256 | n_kernels = 10 257 | # n_samples, n_features, n_dims = 100, 5, 3 258 | n_samples, n_features, n_dims = 80, 5, 3 259 | X = [rng_global.randn(n_features, n_dims) for i in range(n_samples)] 260 | dico = MultivariateDictLearning( 261 | n_kernels=n_kernels, random_state=0, max_iter=10, n_jobs=-1 262 | ).fit(X) 263 | first_epoch = list(dico.kernels_) 264 | dico = dico.fit(X) 265 | second_epoch = list(dico.kernels_) 266 | for k, c in zip(first_epoch, second_epoch): 267 | assert (k - c).sum() != 0.0 268 | 269 | dico = MiniBatchMultivariateDictLearning( 270 | n_kernels=n_kernels, random_state=0, n_iter=10, n_jobs=-1 271 | ).fit(X) 272 | first_epoch = list(dico.kernels_) 273 | dico = dico.fit(X) 274 | second_epoch = list(dico.kernels_) 275 | for k, c in zip(first_epoch, second_epoch): 276 | assert (k - c).sum() != 0.0 277 | 278 | dico = MiniBatchMultivariateDictLearning( 279 | n_kernels=n_kernels, random_state=0, n_iter=10, n_jobs=-1 280 | ).partial_fit(X) 281 | first_epoch = list(dico.kernels_) 282 | dico = dico.partial_fit(X) 283 | second_epoch = list(dico.kernels_) 284 | for k, c in zip(first_epoch, second_epoch): 285 | assert (k - c).sum() != 0.0 286 | 287 | 288 | def test_sparse_multivariate_coder(): 289 | n_samples, n_features, n_dims = 10, 5, 3 290 | X = [rng_global.randn(n_features, n_dims) for i in range(n_samples)] 291 | n_kernels = 8 292 | d = [np.random.randn(n_features, n_dims) for i in range(n_kernels)] 293 | coder = SparseMultivariateCoder(dictionary=d, n_nonzero_coefs=1, n_jobs=-1) 294 | coder.fit(X) 295 | for i in range(n_kernels): 296 | assert_array_almost_equal(d[i], coder.kernels_[i]) 297 | 298 | 299 | def TODO_test_shift_invariant_input(): 300 | dico = list() 301 | dico.append(np.array([1, 2, 3, 2, 1])) 302 | 303 | 304 | def _generate_testbed( 305 | kernel_init_len, n_nonzero_coefs, n_kernels, n_samples=10, n_features=5, n_dims=3 306 | ): 307 | dico = [np.random.randn(kernel_init_len, n_dims) for i in range(n_kernels)] 308 | for i in range(len(dico)): 309 | dico[i] /= np.linalg.norm(dico[i], "fro") 310 | 311 | signals = list() 312 | decomposition = list() 313 | for _ in range(n_samples): 314 | s = np.zeros(shape=(n_features, n_dims)) 315 | d = np.zeros(shape=(n_nonzero_coefs, 3)) 316 | rk = np.random.permutation(range(n_kernels)) 317 | for j in range(n_nonzero_coefs): 318 | k_idx = rk[j] 319 | k_amplitude = 3.0 * np.random.rand() + 1.0 320 | k_offset = np.random.randint(n_features - kernel_init_len + 1) 321 | s[k_offset : k_offset + kernel_init_len, :] += k_amplitude * dico[k_idx] 322 | d[j, :] = np.array([k_amplitude, k_offset, k_idx]) 323 | decomposition.append(d) 324 | signals.append(s) 325 | signals = np.array(signals) 326 | 327 | return dico, signals, decomposition 328 | 329 | 330 | def test_mdla_reconstruction(): 331 | n_features = 5 332 | n_kernels = 8 333 | n_nonzero_coefs = 3 334 | kernel_init_len = n_features 335 | dico, signals, decomposition = _generate_testbed( 336 | kernel_init_len, n_nonzero_coefs, n_kernels 337 | ) 338 | 339 | assert_array_almost_equal( 340 | reconstruct_from_code(decomposition, dico, n_features), signals 341 | ) 342 | 343 | 344 | def test_multivariate_OMP(): 345 | n_samples = 10 346 | n_features = 100 347 | n_dims = 90 348 | n_kernels = 8 349 | n_nonzero_coefs = 3 350 | kernel_init_len = n_features 351 | verbose = False 352 | 353 | dico, signals, decomposition = _generate_testbed( 354 | kernel_init_len, n_nonzero_coefs, n_kernels, n_samples, n_features, n_dims 355 | ) 356 | r, d = multivariate_sparse_encode(signals, dico, n_nonzero_coefs, n_jobs=1) 357 | if verbose is True: 358 | for i in range(n_samples): 359 | # original signal decomposition, sorted by amplitude 360 | sorted_decomposition = np.zeros_like(decomposition[i]).view("float, int, int") 361 | for j in range(decomposition[i].shape[0]): 362 | sorted_decomposition[j] = tuple(decomposition[i][j, :].tolist()) 363 | sorted_decomposition.sort(order=["f0"], axis=0) 364 | for j in reversed(sorted_decomposition): 365 | print(j) 366 | 367 | # decomposition found by OMP, also sorted 368 | sorted_d = np.zeros_like(d[i]).view("float, int, int") 369 | for j in range(d[i].shape[0]): 370 | sorted_d[j] = tuple(d[i][j, :].tolist()) 371 | sorted_d.sort(order=["f0"], axis=0) 372 | for j in reversed(sorted_d): 373 | print(j) 374 | 375 | assert_array_almost_equal( 376 | reconstruct_from_code(d, dico, n_features), signals, decimal=3 377 | ) 378 | 379 | 380 | def _test_with_pydico(): 381 | import pickle 382 | import shutil 383 | 384 | n_features = 5 385 | n_kernels = 8 386 | n_nonzero_coefs = 3 387 | kernel_init_len = n_features 388 | dico, signals, decomposition = _generate_testbed( 389 | kernel_init_len, n_nonzero_coefs, n_kernels 390 | ) 391 | o = {"signals": signals, "dico": dico, "decomposition": decomposition} 392 | with open("skmdla.pck", "w") as f: 393 | pickle.dump(o, f) 394 | f.close() 395 | shutil.copy("skmdla.pck", "../RC/skmdla.pck") 396 | 397 | print(signals) 398 | print(dico) 399 | 400 | r, d = multivariate_sparse_encode(signals, dico, n_nonzero_coefs, n_jobs=1, verbose=4) 401 | 402 | 403 | def _test_with_pydico_reload(): 404 | import pickle 405 | 406 | n_nonzero_coefs = 3 407 | with open("skmdla.pck", "w") as f: 408 | o = pickle.load(f) 409 | f.close() 410 | dico = o["dico"] 411 | signals = o["signals"] 412 | _ = o["decomposition"] 413 | 414 | r, d = multivariate_sparse_encode(signals, dico, n_nonzero_coefs, n_jobs=1, verbose=4) 415 | 416 | 417 | def _verif_OMP(): 418 | n_samples = 1000 419 | n_nonzero_coefs = 3 420 | 421 | for n_features in range(5, 50, 5): 422 | kernel_init_len = n_features - n_features / 2 423 | n_dims = n_features / 2 424 | n_kernels = n_features * 5 425 | dico, signals, _ = _generate_testbed( 426 | kernel_init_len, n_nonzero_coefs, n_kernels, n_samples, n_features, n_dims 427 | ) 428 | r, d = multivariate_sparse_encode(signals, dico, n_nonzero_coefs, n_jobs=1) 429 | reconstructed = reconstruct_from_code(d, dico, n_features) 430 | 431 | residual_energy = 0.0 432 | for sig, rec in zip(signals, reconstructed): 433 | residual_energy += ((sig - rec) ** 2).sum(1).mean() 434 | 435 | print( 436 | "Mean energy of the", 437 | n_samples, 438 | "residuals for", 439 | (n_features, n_dims), 440 | "features and", 441 | n_kernels, 442 | "kernels of", 443 | (kernel_init_len, n_dims), 444 | " is", 445 | residual_energy / n_samples, 446 | ) 447 | -------------------------------------------------------------------------------- /mdla/dict_metrics.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | The `dict_metrics` module implements utilities to compare 4 | frames and dictionaries. 5 | 6 | This module implements several criteria and metrics to compare different sets 7 | of atoms. This module is primarily focused on multivariate kernels and 8 | atoms. 9 | """ 10 | 11 | # Authors: Sylvain Chevallier 12 | # License: GPL v3 13 | 14 | # TODO: add docstring to criteria fonction 15 | # verify Fubini-Study scale parameter 16 | # verify beta dist behavior, seems like 1-bd 17 | # change scale behavior, replace 1-d with d ! 18 | 19 | import cvxopt as co 20 | import cvxopt.solvers as solv 21 | import numpy as np 22 | import scipy.linalg as sl 23 | from numpy import ( 24 | NaN, 25 | abs, 26 | all, 27 | arccos, 28 | arcsin, 29 | argmax, 30 | array, 31 | atleast_2d, 32 | concatenate, 33 | infty, 34 | max, 35 | min, 36 | ones, 37 | ones_like, 38 | sqrt, 39 | trace, 40 | unravel_index, 41 | zeros, 42 | zeros_like, 43 | ) 44 | from numpy.linalg import det, norm, svd 45 | 46 | 47 | def _kernel_registration(this_kernel, dictionary, g): 48 | k_len = this_kernel.shape[0] 49 | n_kernels = len(dictionary) 50 | k_max_len = array([i.shape[0] for i in dictionary]).max() 51 | 52 | m_dist = ones((n_kernels, k_max_len - k_len + 1)) * infty 53 | m_corr = zeros((n_kernels, k_max_len - k_len + 1)) 54 | for i, kernel in enumerate(dictionary): # kernel loop 55 | ks = kernel.shape[0] 56 | # for t in range(k_max_len-k_len+1): # convolution loop 57 | for t in range(ks - k_len + 1): # convolution loop 58 | # print ("t = ", t, "and l =", l) 59 | # print ("kernel = ", kernel.shape, 60 | # "and kernel[t:t+l,:] = ", kernel[t:t+k_len,:].shape) 61 | m_dist[i, t] = g(this_kernel, kernel[t : t + k_len, :]) 62 | m_corr[i, t] = trace(this_kernel.T.dot(kernel[t : t + k_len, :])) / ( 63 | norm(this_kernel, "fro") * norm(kernel[t : t + k_len, :], "fro") 64 | ) 65 | return m_dist, m_corr 66 | 67 | 68 | def principal_angles(A, B): 69 | """Compute the principal angles between subspaces A and B. 70 | 71 | The algorithm for computing the principal angles is described in : 72 | A. V. Knyazev and M. E. Argentati, 73 | Principal Angles between Subspaces in an A-Based Scalar Product: 74 | Algorithms and Perturbation Estimates. SIAM Journal on Scientific Computing, 75 | 23 (2002), no. 6, 2009-2041. 76 | http://epubs.siam.org/sam-bin/dbq/article/37733 77 | """ 78 | # eps = np.finfo(np.float64).eps**.981 79 | # for i in range(A.shape[1]): 80 | # normi = la.norm(A[:,i],np.inf) 81 | # if normi > eps: A[:,i] = A[:,i]/normi 82 | # for i in range(B.shape[1]): 83 | # normi = la.norm(B[:,i],np.inf) 84 | # if normi > eps: B[:,i] = B[:,i]/normi 85 | QA = sl.orth(A) 86 | QB = sl.orth(B) 87 | _, s, Zs = svd(QA.T.dot(QB), full_matrices=False) 88 | s = np.minimum(s, ones_like(s)) 89 | theta = np.maximum(np.arccos(s), np.zeros_like(s)) 90 | V = QB.dot(Zs) 91 | idxSmall = s > np.sqrt(2.0) / 2.0 92 | if np.any(idxSmall): 93 | RB = V[:, idxSmall] 94 | _, x, _ = svd(RB - QA.dot(QA.T.dot(RB)), full_matrices=False) 95 | thetaSmall = np.flipud( 96 | np.maximum(arcsin(np.minimum(x, ones_like(x))), zeros_like(x)) 97 | ) 98 | theta[idxSmall] = thetaSmall 99 | return theta 100 | 101 | 102 | def chordal_principal_angles(A, B): 103 | """ 104 | chordal_principal_angles(A, B) Compute the chordal distance based on 105 | principal angles. 106 | Compute the chordal distance based on principal angles between A and B 107 | as :math:`d=\sqrt{ \sum_i \sin^2 \theta_i}` 108 | """ 109 | return sqrt(np.sum(np.sin(principal_angles(A, B)) ** 2)) 110 | 111 | 112 | def chordal(A, B): 113 | """ 114 | chordal(A, B) Compute the chordal distance 115 | Compute the chordal distance between A and B 116 | as d=\sqrt{K - ||\bar{A}^T\bar{B}||_F^2} 117 | where K is the rank of A and B, || . ||_F is the Frobenius norm, 118 | \bar{A} is the orthogonal basis associated with A and the same goes for B. 119 | """ 120 | if A.shape != B.shape: 121 | raise ValueError( 122 | f"Atoms have not the same dimension ({A.shape} and {B.shape}). Error raised" 123 | f"in chordal(A, B)", 124 | ) 125 | 126 | if np.allclose(A, B): 127 | return 0.0 128 | else: 129 | d2 = A.shape[1] - norm(sl.orth(A).T.dot(sl.orth(B)), "fro") ** 2 130 | if d2 < 0.0: 131 | return sqrt(abs(d2)) 132 | else: 133 | return sqrt(d2) 134 | 135 | 136 | def fubini_study(A, B): 137 | """ 138 | fubini_study(A, B) Compute the Fubini-Study distance 139 | Compute the Fubini-Study distance based on principal angles between A and B 140 | as d=\acos{ \prod_i \theta_i} 141 | """ 142 | if A.shape != B.shape: 143 | raise ValueError( 144 | f"Atoms have different dim ({A.shape} and {B.shape}). Error raised in" 145 | f"fubini_study(A, B)", 146 | ) 147 | if np.allclose(A, B): 148 | return 0.0 149 | return arccos(det(sl.orth(A).T.dot(sl.orth(B)))) 150 | 151 | 152 | def binet_cauchy(A, B): 153 | """Compute the Binet-Cauchy distance 154 | Compute the Binet-Cauchy distance based on principal angles between A 155 | and B with d=\sqrt{ 1 - \prod_i \cos^2 \theta_i} 156 | """ 157 | theta = principal_angles(A, B) 158 | return sqrt(1.0 - np.prod(np.cos(theta) ** 2)) 159 | 160 | 161 | def geodesic(A, B): 162 | """ 163 | geodesic (A, B) Compute the arc length or geodesic distance 164 | Compute the arc length or geodesic distance based on principal angles between A 165 | and B with d=\sqrt{ \sum_i \theta_i^2} 166 | """ 167 | theta = principal_angles(A, B) 168 | return norm(theta) 169 | 170 | 171 | def frobenius(A, B): 172 | if A.shape != B.shape: 173 | raise ValueError( 174 | f"Atoms have different dim ({A.shape} and {B.shape}). Error raised in" 175 | f"frobenius(A, B)", 176 | ) 177 | return norm(A - B, "fro") 178 | 179 | 180 | def abs_euclidean(A, B): 181 | if (A.ndim != 1 and A.shape[1] != 1) or (B.ndim != 1 and B.shape[1] != 1): 182 | raise ValueError( 183 | f"Atoms are not univariate ({A.shape} and {B.shape}). Error raised" 184 | f"in abs_euclidean(A, B)", 185 | ) 186 | if np.allclose(A, B): 187 | return 0.0 188 | else: 189 | return sqrt(2.0 * (1.0 - np.abs(A.T.dot(B)))) 190 | 191 | 192 | def euclidean(A, B): 193 | if (A.ndim != 1 and A.shape[1] != 1) or (B.ndim != 1 and B.shape[1] != 1): 194 | raise ValueError( 195 | f"Atoms are not univariate ({A.shape} and {B.shape}). Error raised in" 196 | f"euclidean(A, B)", 197 | ) 198 | if np.allclose(A, B): 199 | return 0.0 200 | else: 201 | return sqrt(2.0 * (1.0 - A.T.dot(B))) 202 | 203 | 204 | def _valid_atom_metric(gdist): 205 | """Verify that atom metric exist and return the correct function""" 206 | if gdist == "chordal": 207 | return chordal 208 | elif gdist == "chordal_principal_angles": 209 | return chordal_principal_angles 210 | elif gdist == "fubinistudy": 211 | return fubini_study 212 | elif gdist == "binetcauchy": 213 | return binet_cauchy 214 | elif gdist == "geodesic": 215 | return geodesic 216 | elif gdist == "frobenius": 217 | return frobenius 218 | elif gdist == "abs_euclidean": 219 | return abs_euclidean 220 | elif gdist == "euclidean": 221 | return euclidean 222 | else: 223 | return None 224 | 225 | 226 | def _scale_metric(gdist, d, D1): 227 | if ( 228 | gdist == "chordal" 229 | or gdist == "chordal_principal_angles" 230 | or gdist == "fubinistudy" 231 | or gdist == "binetcauchy" 232 | or gdist == "geodesic" 233 | ): 234 | # TODO: scale with max n_features 235 | return d / sqrt(D1[0].shape[0]) 236 | elif gdist == "frobenius": 237 | return d / sqrt(2.0) 238 | else: 239 | return d 240 | 241 | 242 | def _compute_gdm(D1, D2, g): 243 | """Compute ground distance matrix from dictionaries D1 and D2 244 | 245 | Distance g acts as ground distance. 246 | A kernel registration is applied if dictionary atoms do not have 247 | the same size. 248 | """ 249 | # Do we need a registration? If kernel do not have the same shape, yes 250 | if not all(array([i.shape[0] for i in D1 + D2]) == D1[0].shape[0]): 251 | # compute correlation and distance matrices 252 | k_dim = D1[0].shape[1] 253 | # minl = np.array([i.shape[1] for i in D1+D2]).min() 254 | max_l1 = array([i.shape[0] for i in D1]).max() 255 | max_l2 = array([i.shape[0] for i in D2]).max() 256 | if max_l2 > max_l1: 257 | Da = D1 258 | Db = D2 259 | max_l = max_l2 260 | else: 261 | Da = D2 262 | Db = D1 263 | max_l = max_l1 264 | # Set all Db atom to largest value 265 | Dbe = [] 266 | for i in range(len(Db)): 267 | k_l = Db[i].shape[0] 268 | Dbe.append(concatenate((zeros((max_l - k_l, k_dim)), Db[i]), axis=0)) 269 | gdm = zeros((len(Da), len(Db))) 270 | for i in range(len(Da)): 271 | m_dist, m_corr = _kernel_registration(Da[i], Dbe, g) 272 | k_l = Da[i].shape[0] 273 | # m_dist, m_corr = _kernel_registration(np.concatenate((zeros((np.int(np.floor((max_l-k_l)/2.)), k_dim)), Da[i], zeros((np.int(np.ceil((max_l-k_l)/2.)), k_dim))), axis=0), Dbe, g) 274 | for j in range(len(Dbe)): 275 | gdm[i, j] = m_dist[ 276 | j, unravel_index(abs(m_corr[j, :]).argmax(), m_corr[j, :].shape) 277 | ] 278 | else: 279 | # all atoms have the same length, no registration 280 | gdm = zeros((len(D1), len(D2))) 281 | for i in range(len(D1)): 282 | for j in range(len(D2)): 283 | gdm[i, j] = g(D1[i], D2[j]) 284 | return gdm 285 | 286 | 287 | def hausdorff(D1, D2, gdist, scale=False): 288 | """ 289 | Compute the Hausdorff distance between two sets of elements, here 290 | dictionary atoms, using a ground distance. 291 | Possible choice are "chordal", "fubinistudy", "binetcauchy", "geodesic", 292 | "frobenius", "abs_euclidean" or "euclidean". 293 | The scale parameter changes the return value to be between 0 and 1. 294 | """ 295 | g = _valid_atom_metric(gdist) 296 | if g is None: 297 | print("Unknown ground distance, exiting.") 298 | return NaN 299 | gdm = _compute_gdm(D1, D2, g) 300 | d = max([max(min(gdm, axis=0)), max(min(gdm, axis=1))]) 301 | if not scale: 302 | return d 303 | else: 304 | return _scale_metric(gdist, d, D1) 305 | 306 | 307 | def emd(D1, D2, gdist, scale=False): 308 | """ 309 | Compute the Earth Mover's Distance (EMD) between two sets of elements, 310 | here dictionary atoms, using a ground distance. 311 | Possible choice are "chordal", "fubinistudy", "binetcauchy", "geodesic", 312 | "frobenius", "abs_euclidean" or "euclidean". 313 | The scale parameter changes the return value to be between 0 and 1. 314 | """ 315 | g = _valid_atom_metric(gdist) 316 | if g is None: 317 | print("Unknown ground distance, exiting.") 318 | return NaN 319 | # if gdist == "chordal": 320 | # g = chordal 321 | # elif gdist == "chordal_principal_angles": 322 | # g = chordal_principal_angles 323 | # elif gdist == "fubinistudy": 324 | # g = fubini_study 325 | # elif gdist == "binetcauchy": 326 | # g = binet_cauchy 327 | # elif gdist == "geodesic": 328 | # g = geodesic 329 | # elif gdist == "frobenius": 330 | # g = frobenius 331 | # elif gdist == "abs_euclidean": 332 | # g = abs_euclidean 333 | # elif gdist == "euclidean": 334 | # g = euclidean 335 | # else: 336 | # print 'Unknown ground distance, exiting.' 337 | # return NaN 338 | 339 | # # Do we need a registration? If kernel do not have the same shape, yes 340 | # if not np.all(np.array([i.shape[0] for i in D1+D2]) == D1[0].shape[0]): 341 | # # compute correlation and distance matrices 342 | # k_dim = D1[0].shape[1] 343 | # # minl = np.array([i.shape[1] for i in D1+D2]).min() 344 | # max_l1 = np.array([i.shape[0] for i in D1]).max() 345 | # max_l2 = np.array([i.shape[0] for i in D2]).max() 346 | # if max_l2 > max_l1: 347 | # Da = D1 348 | # Db = D2 349 | # max_l = max_l2 350 | # else: 351 | # Da = D2 352 | # Db = D1 353 | # max_l = max_l1 354 | # Dbe = [] 355 | # for i in range(len(Db)): 356 | # k_l = Db[i].shape[0] 357 | # Dbe.append(np.concatenate((zeros((max_l-k_l, k_dim)), Db[i]), axis=0)) 358 | # gdm = zeros((len(Da), len(Db))) 359 | # for i in range(len(Da)): 360 | # k_l = Da[i].shape[0] 361 | # m_dist, m_corr = _kernel_registration(np.concatenate((zeros(( np.int(np.floor((max_l-k_l)/2.)), k_dim)), Da[i], zeros((np.int(np.ceil((max_l-k_l)/2.)), k_dim))), axis=0), Dbe, g) 362 | # for j in range(len(Dbe)): 363 | # gdm[i,j] = m_dist[j, np.unravel_index(np.abs(m_corr[j,:]).argmax(), m_corr[j,:].shape)] 364 | # else: 365 | # # all atoms have the same length, no registration 366 | # gdm = np.zeros((len(D1), len(D2))) 367 | # for i in range(len(D1)): 368 | # for j in range(len(D2)): 369 | # gdm[i,j] = g(D1[i], D2[j]) 370 | gdm = _compute_gdm(D1, D2, g) 371 | 372 | c = co.matrix(gdm.flatten(order="F")) 373 | G1 = co.spmatrix([], [], [], (len(D1), len(D1) * len(D2))) 374 | G2 = co.spmatrix([], [], [], (len(D2), len(D1) * len(D2))) 375 | G3 = co.spmatrix(-1.0, range(len(D1) * len(D2)), range(len(D1) * len(D2))) 376 | for i in range(len(D1)): 377 | for j in range(len(D2)): 378 | k = j + (i * len(D2)) 379 | G1[i, k] = 1.0 380 | G2[j, k] = 1.0 381 | G = co.sparse([G1, G2, G3]) 382 | h1 = co.matrix(1.0 / len(D1), (len(D1), 1)) 383 | h2 = co.matrix(1.0 / len(D2), (len(D2), 1)) 384 | h3 = co.spmatrix([], [], [], (len(D1) * len(D2), 1)) 385 | h = co.matrix([h1, h2, h3]) 386 | A = co.matrix(1.0, (1, len(D1) * len(D2))) 387 | b = co.matrix([1.0]) 388 | 389 | co.solvers.options["show_progress"] = False 390 | sol = solv.lp(c, G, h, A, b) 391 | d = sol["primal objective"] 392 | 393 | if not scale: 394 | return d 395 | else: 396 | return _scale_metric(gdist, d, D1) 397 | # if (gdist == "chordal" or gdist == "chordal_principal_angles" or 398 | # gdist == "fubinistudy" or gdist == "binetcauchy" or 399 | # gdist == "geodesic"): 400 | # return d/sqrt(D1[0].shape[0]) 401 | # elif gdist == "frobenius": 402 | # return d/sqrt(2.) 403 | # else: 404 | # return d 405 | 406 | 407 | def _multivariate_correlation(s, D): 408 | """Compute correlation between multivariate atoms 409 | 410 | Compute the correlation between a multivariate atome s and dictionary D 411 | as the sum of the correlation in each n_dims dimensions. 412 | """ 413 | n_features = s.shape[0] 414 | n_dims = s.shape[1] 415 | n_kernels = len(D) 416 | corr = np.zeros((n_kernels, n_features)) 417 | for k in range(n_kernels): # for all atoms 418 | corrTmp = 0 419 | for j in range(n_dims): # for all dimensions 420 | corrTmp += np.correlate(s[:, j], D[k][:, j]) 421 | corr[k, : len(corrTmp)] = corrTmp 422 | return corr 423 | 424 | 425 | def detection_rate(ref, recov, threshold): 426 | """Compute the detection rate between reference and recovered dictionaries 427 | 428 | The reference ref and the recovered recov are univariate or multivariate 429 | dictionaries. An atom a of the ref dictionary is considered as recovered if 430 | $c < threshold$ with $c = argmax_{r \in R} ||$, that is the absolute 431 | value of the maximum correlation between a and any atom r of the recovered 432 | dictionary R is above a given threshold. 433 | The process is iterative and an atom r could be matched only once with an 434 | atom a of the reference dictionary. In other word, each atom a is matched 435 | with a different atom r. 436 | """ 437 | n_kernels_ref, n_kernels_recov = len(ref), len(recov) 438 | n_features = ref[0].shape[0] 439 | if ref[0].ndim == 1: 440 | n_dims = 1 441 | for k in range(n_kernels_ref): 442 | ref[k] = atleast_2d(ref[k]).T 443 | else: 444 | n_dims = ref[0].shape[1] 445 | if recov[0].ndim == 1: 446 | for k in range(n_kernels_recov): 447 | recov[k] = atleast_2d(recov[k]).T 448 | dr = 0 449 | corr = zeros((n_kernels_ref, n_kernels_recov)) 450 | for k in range(n_kernels_ref): 451 | c_tmp = _multivariate_correlation( 452 | concatenate( 453 | (zeros((n_features, n_dims)), ref[k], zeros((n_features, n_dims))), axis=0 454 | ), 455 | recov, 456 | ) 457 | for j in range(n_kernels_recov): 458 | idx_max = argmax(abs(c_tmp[j, :])) 459 | corr[k, j] = c_tmp[j, idx_max] 460 | c_local = np.abs(corr.copy()) 461 | for _ in range(n_kernels_ref): 462 | max_corr = c_local.max() 463 | if max_corr >= threshold: 464 | dr += 1 465 | idx_max = np.unravel_index(c_local.argmax(), c_local.shape) 466 | c_local[:, idx_max[1]] = zeros(n_kernels_ref) 467 | c_local[idx_max[0], :] = zeros(n_kernels_recov) 468 | return float(dr) / n_kernels_recov * 100.0 469 | 470 | 471 | def _convert_array(ref, recov): 472 | if ref[0].ndim == 1: 473 | for k in range(len(ref)): 474 | ref[k] = atleast_2d(ref[k]).T 475 | if recov[0].ndim == 1: 476 | for k in range(len(recov)): 477 | recov[k] = atleast_2d(recov[k]).T 478 | D1 = np.array(ref) 479 | D2 = np.array(recov) 480 | M = D1.shape[0] 481 | N = D1.shape[1] 482 | D1 = D1.reshape((M, N)) 483 | D2 = D2.reshape((M, N)) 484 | return D1, D2, M 485 | 486 | 487 | def precision_recall(ref, recov, threshold): 488 | """Compute precision and recall for recovery experiment""" 489 | D1, D2, M = _convert_array(ref, recov) 490 | corr = D1.dot(D2.T) 491 | precision = float((np.max(corr, axis=0) > threshold).sum()) / float(M) 492 | recall = float((np.max(corr, axis=1) > threshold).sum()) / float(M) 493 | return precision * 100.0, recall * 100.0 494 | 495 | 496 | def precision_recall_points(ref, recov): 497 | """Compute the precision and recall for each atom in a recovery experiment""" 498 | # if ref[0].ndim == 1: 499 | # for k in range(len(ref)): 500 | # ref[k] = atleast_2d(ref[k]).T 501 | # if recov[0].ndim == 1: 502 | # for k in range(len(recov)): 503 | # recov[k] = atleast_2d(recov[k]).T 504 | # D1 = np.array(ref) 505 | # D2 = np.array(recov) 506 | # M = D1.shape[0] 507 | # N = D1.shape[1] 508 | # D1 = D1.reshape((M, N)) 509 | # D2 = D2.reshape((M, N)) 510 | D1, D2, _ = _convert_array(ref, recov) 511 | corr = D1.dot(D2.T) 512 | precision = np.max(corr, axis=0) 513 | recall = np.max(corr, axis=1) 514 | return precision, recall 515 | 516 | 517 | def beta_dist(D1, D2): 518 | """Compute the Beta-distance proposed by Skretting and Engan 519 | 520 | The beta-distance is: 521 | $\beta(D1, D2)=1/(M1+M2)(\sum_j \beta(D1, d^2_j)+\sum_j \beta(D2, d^1_j))$ 522 | with $\beta(D, x) = arccos(\max_i |d^T_i x|/||x||)$ 523 | as proposed in: 524 | Karl Skretting and Kjersti Engan, 525 | Learned dictionaries for sparse image representation: properties and results, 526 | SPIE, 2011. 527 | """ 528 | if D1[0].shape != D2[0].shape: 529 | raise ValueError( 530 | f"Dictionaries have different dim : {D1[0].shape} and {D2[0].shape}." 531 | ) 532 | D1 = np.array(D1) 533 | M1 = D1.shape[0] 534 | N = D1.shape[1] 535 | D1 = D1.reshape((M1, N)) 536 | D2 = np.array(D2) 537 | M2 = D2.shape[0] 538 | D2 = D2.reshape((M2, N)) 539 | corr = D1.dot(D2.T) 540 | if np.allclose(np.max(corr, axis=0), ones(M2)) and np.allclose( 541 | np.max(corr, axis=1), ones(M1) 542 | ): 543 | return 0.0 544 | return ( 545 | np.sum(np.arccos(np.max(corr, axis=0))) + np.sum(np.arccos(np.max(corr, axis=1))) 546 | ) / (M1 + M2) 547 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cvxopt==1.2.7; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") \ 2 | --hash=sha256:dd87afaa82bc4f0b4436eeeee0c0c86cd504bf519d7b0fe28057be7479676b51 \ 3 | --hash=sha256:b2c30772b8d0cd4ae528af60c3ac732c07670c65f0539746f9a30eb14c5d7a17 \ 4 | --hash=sha256:803a1c2bdce5f15fe0f20f0a26e39a251cede121091317f91ef1bcfd73f7ef38 \ 5 | --hash=sha256:0cdf9abac90cdd727558f57588e776f6cdb12362f6adf2cb4d2b6d629d9125c8 \ 6 | --hash=sha256:6cd7e6cd626e202b085995e63805b645567bfe3bd133cdc2cdb89b9842062081 \ 7 | --hash=sha256:4ed667bd82f91b832d33f14f535deb66c8eb0a3ad0c5f9e9cbd0a10523ff79bc \ 8 | --hash=sha256:ec6e46c3392adaae455286aab02daf03ccf3878a543de4c18f11ea6e1131c74b \ 9 | --hash=sha256:ac02d62a311b84d3b88ea8472052d85e72714e96024608542d48eb04a788b4d7 \ 10 | --hash=sha256:2c292500d8252d6895ab998f84d7c1724a272ee4e9851ca74b2947429cb2b087 \ 11 | --hash=sha256:c91ae903b69cc1bed7a58ea1bbc74f19daec9f03a7d09c41abb21c5328d2ea58 \ 12 | --hash=sha256:bc0d997e8ca3fb8eab733cc7c32eff8137890637de24f7b0d892ad185fbc54e6 \ 13 | --hash=sha256:22fb842920fcaf8adb9a60cf9af9401363b32430150986d7191e9504b0235254 \ 14 | --hash=sha256:b55ebc43095ca29b4c835bdc22f2a2c4df06ccbbb9fa6a7e6096dceefd412b4f \ 15 | --hash=sha256:6c6fa32cc49eaa89f8ed00c03e4717854827e0753c7da0026e934d5c24c5dd28 \ 16 | --hash=sha256:8002390c4a8b55fbf4011cf6e663f0b219459edf2423e9791e393b92e41cf8c8 \ 17 | --hash=sha256:1381315f68f728a3fab4eba1c2352605f074b10494ebe8fe008b0947ce13b589 \ 18 | --hash=sha256:712975afb3ed3d14dea2ab1eca774b70f2b58db61a88e70708c912e15339113c \ 19 | --hash=sha256:fac53f7d2814278475ecd9f2f22e997a92fbf7f044672caee946d9773c79fef9 \ 20 | --hash=sha256:03bbfe48b9d6148064cc5af9cfb288de3c99cbe93601aa2de65c61d3c7253bad \ 21 | --hash=sha256:97262506af8c5fffadcabb4b0ed9caa67ab94c46e08e16b563736a274f7b8601 \ 22 | --hash=sha256:b6604befe48283f6d71a31d10a6a286e13c11644dd34e108d873293aa5ccec8d \ 23 | --hash=sha256:3f9db1f4d4e820aaea81d6fc21054c89dc6327c84f935dd5a1eda1af11e1d504 24 | cycler==0.11.0; python_version >= "3.7" \ 25 | --hash=sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3 \ 26 | --hash=sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f 27 | fonttools==4.28.1; python_version >= "3.7" \ 28 | --hash=sha256:68071406009e7ef6a5fdcd85d95975cd6963867bb226f2b786bfffe15d1959ef \ 29 | --hash=sha256:8c8f84131bf04f3b1dcf99b9763cec35c347164ab6ad006e18d2f99fcab05529 30 | joblib==1.1.0; python_version >= "3.7" \ 31 | --hash=sha256:f21f109b3c7ff9d95f8387f752d0d9c34a02aa2f7060c2135f465da0e5160ff6 \ 32 | --hash=sha256:4158fcecd13733f8be669be0683b96ebdbbd38d23559f54dca7205aea1bf1e35 33 | kiwisolver==1.3.2; python_version >= "3.7" \ 34 | --hash=sha256:1d819553730d3c2724582124aee8a03c846ec4362ded1034c16fb3ef309264e6 \ 35 | --hash=sha256:8d93a1095f83e908fc253f2fb569c2711414c0bfd451cab580466465b235b470 \ 36 | --hash=sha256:c4550a359c5157aaf8507e6820d98682872b9100ce7607f8aa070b4b8af6c298 \ 37 | --hash=sha256:2210f28778c7d2ee13f3c2a20a3a22db889e75f4ec13a21072eabb5693801e84 \ 38 | --hash=sha256:82f49c5a79d3839bc8f38cb5f4bfc87e15f04cbafa5fbd12fb32c941cb529cfb \ 39 | --hash=sha256:9661a04ca3c950a8ac8c47f53cbc0b530bce1b52f516a1e87b7736fec24bfff0 \ 40 | --hash=sha256:2ddb500a2808c100e72c075cbb00bf32e62763c82b6a882d403f01a119e3f402 \ 41 | --hash=sha256:72be6ebb4e92520b9726d7146bc9c9b277513a57a38efcf66db0620aec0097e0 \ 42 | --hash=sha256:83d2c9db5dfc537d0171e32de160461230eb14663299b7e6d18ca6dca21e4977 \ 43 | --hash=sha256:cba430db673c29376135e695c6e2501c44c256a81495da849e85d1793ee975ad \ 44 | --hash=sha256:4116ba9a58109ed5e4cb315bdcbff9838f3159d099ba5259c7c7fb77f8537492 \ 45 | --hash=sha256:19554bd8d54cf41139f376753af1a644b63c9ca93f8f72009d50a2080f870f77 \ 46 | --hash=sha256:a7a4cf5bbdc861987a7745aed7a536c6405256853c94abc9f3287c3fa401b174 \ 47 | --hash=sha256:0007840186bacfaa0aba4466d5890334ea5938e0bb7e28078a0eb0e63b5b59d5 \ 48 | --hash=sha256:ec2eba188c1906b05b9b49ae55aae4efd8150c61ba450e6721f64620c50b59eb \ 49 | --hash=sha256:3dbb3cea20b4af4f49f84cffaf45dd5f88e8594d18568e0225e6ad9dec0e7967 \ 50 | --hash=sha256:5326ddfacbe51abf9469fe668944bc2e399181a2158cb5d45e1d40856b2a0589 \ 51 | --hash=sha256:c6572c2dab23c86a14e82c245473d45b4c515314f1f859e92608dcafbd2f19b8 \ 52 | --hash=sha256:b5074fb09429f2b7bc82b6fb4be8645dcbac14e592128beeff5461dcde0af09f \ 53 | --hash=sha256:22521219ca739654a296eea6d4367703558fba16f98688bd8ce65abff36eaa84 \ 54 | --hash=sha256:c358721aebd40c243894298f685a19eb0491a5c3e0b923b9f887ef1193ddf829 \ 55 | --hash=sha256:7ba5a1041480c6e0a8b11a9544d53562abc2d19220bfa14133e0cdd9967e97af \ 56 | --hash=sha256:44e6adf67577dbdfa2d9f06db9fbc5639afefdb5bf2b4dfec25c3a7fbc619536 \ 57 | --hash=sha256:1d45d1c74f88b9f41062716c727f78f2a59a5476ecbe74956fafb423c5c87a76 \ 58 | --hash=sha256:70adc3658138bc77a36ce769f5f183169bc0a2906a4f61f09673f7181255ac9b \ 59 | --hash=sha256:b6a5431940f28b6de123de42f0eb47b84a073ee3c3345dc109ad550a3307dd28 \ 60 | --hash=sha256:ee040a7de8d295dbd261ef2d6d3192f13e2b08ec4a954de34a6fb8ff6422e24c \ 61 | --hash=sha256:8dc3d842fa41a33fe83d9f5c66c0cc1f28756530cd89944b63b072281e852031 \ 62 | --hash=sha256:a498bcd005e8a3fedd0022bb30ee0ad92728154a8798b703f394484452550507 \ 63 | --hash=sha256:80efd202108c3a4150e042b269f7c78643420cc232a0a771743bb96b742f838f \ 64 | --hash=sha256:f8eb7b6716f5b50e9c06207a14172cf2de201e41912ebe732846c02c830455b9 \ 65 | --hash=sha256:f441422bb313ab25de7b3dbfd388e790eceb76ce01a18199ec4944b369017009 \ 66 | --hash=sha256:30fa008c172355c7768159983a7270cb23838c4d7db73d6c0f6b60dde0d432c6 \ 67 | --hash=sha256:2f8f6c8f4f1cff93ca5058d6ec5f0efda922ecb3f4c5fb76181f327decff98b8 \ 68 | --hash=sha256:ba677bcaff9429fd1bf01648ad0901cea56c0d068df383d5f5856d88221fe75b \ 69 | --hash=sha256:7843b1624d6ccca403a610d1277f7c28ad184c5aa88a1750c1a999754e65b439 \ 70 | --hash=sha256:e6f5eb2f53fac7d408a45fbcdeda7224b1cfff64919d0f95473420a931347ae9 \ 71 | --hash=sha256:eedd3b59190885d1ebdf6c5e0ca56828beb1949b4dfe6e5d0256a461429ac386 \ 72 | --hash=sha256:dedc71c8eb9c5096037766390172c34fb86ef048b8e8958b4e484b9e505d66bc \ 73 | --hash=sha256:bf7eb45d14fc036514c09554bf983f2a72323254912ed0c3c8e697b62c4c158f \ 74 | --hash=sha256:2b65bd35f3e06a47b5c30ea99e0c2b88f72c6476eedaf8cfbc8e66adb5479dcf \ 75 | --hash=sha256:25405f88a37c5f5bcba01c6e350086d65e7465fd1caaf986333d2a045045a223 \ 76 | --hash=sha256:bcadb05c3d4794eb9eee1dddf1c24215c92fb7b55a80beae7a60530a91060560 \ 77 | --hash=sha256:fc4453705b81d03568d5b808ad8f09c77c47534f6ac2e72e733f9ca4714aa75c 78 | matplotlib==3.5.0; python_version >= "3.7" \ 79 | --hash=sha256:4b018ea6f26424a0852eb60eb406420d9f0d34f65736ea7bbfbb104946a66d86 \ 80 | --hash=sha256:a07ff2565da72a7b384a9e000b15b6b8270d81370af8a3531a16f6fbcee023cc \ 81 | --hash=sha256:2eea16883aa7724c95eea0eb473ab585c6cf66f0e28f7f13e63deb38f4fd6d0f \ 82 | --hash=sha256:0e020a42f3338823a393dd2f80e39a2c07b9f941dfe2c778eb104eeb33d60bb5 \ 83 | --hash=sha256:9bac8eb1eccef540d7f4e844b6313d9f7722efd48c07e1b4bfec1056132127fd \ 84 | --hash=sha256:7a7cb59ebd63a8ac4542ec1c61dd08724f82ec3aa7bb6b4b9e212d43c611ce3d \ 85 | --hash=sha256:6e0e6b2111165522ad336705499b1f968c34a9e84d05d498ee5af0b5697d1efe \ 86 | --hash=sha256:ff5d9fe518ad2de14ce82ab906b6ab5c2b0c7f4f984400ff8a7a905daa580a0a \ 87 | --hash=sha256:66b172610db0ececebebb09d146f54205f87c7b841454e408fba854764f91bdd \ 88 | --hash=sha256:ee3d9ff16d749a9aa521bd7d86f0dbf256b2d2ac8ce31b19e4d2c86d2f2ff0b6 \ 89 | --hash=sha256:970aa97297537540369d05fe0fd1bb952593f9ab696c9b427c06990a83e2418b \ 90 | --hash=sha256:153a0cf6a6ff4f406a0600d2034710c49988bacc6313d193b32716f98a697580 \ 91 | --hash=sha256:6db02c5605f063b67780f4d5753476b6a4944343284aa4e93c5e8ff6e9ec7f76 \ 92 | --hash=sha256:df0042cab69f4d246f4cb8fc297770ac4ae6ec2983f61836b04a117722037dcd \ 93 | --hash=sha256:a7bf8b05c214d32fb7ca7c001fde70b9b426378e897b0adbf77b85ea3569d56a \ 94 | --hash=sha256:0abf8b51cc6d3ba34d1b15b26e329f23879848a0cf1216954c1f432ffc7e1af7 \ 95 | --hash=sha256:13930a0c9bec0fd25f43c448b047a21af1353328b946f044a8fc3be077c6b1a8 \ 96 | --hash=sha256:18f6e52386300db5cc4d1e9019ad9da2e80658bab018834d963ebb0aa5355095 \ 97 | --hash=sha256:ba107add08e12600b072cf3c47aaa1ab85dd4d3c48107a5d3377d1bf80f8b235 \ 98 | --hash=sha256:2089b9014792dcc87bb1d620cde847913338abf7d957ef05587382b0cb76d44e \ 99 | --hash=sha256:f23fbf70d2e80f4e03a83fc1206a8306d9bc50482fee4239f10676ce7e470c83 \ 100 | --hash=sha256:71a1851111f23f82fc43d2b6b2bfdd3f760579a664ebc939576fe21cc6133d01 \ 101 | --hash=sha256:d092b7ba63182d2dd427904e3eb58dd5c46ec67c5968de14a4b5007010a3a4cc \ 102 | --hash=sha256:ac17a7e7b06ee426a4989f0b7f24ab1a592e39cdf56353a90f4e998bc0bf44d6 \ 103 | --hash=sha256:a5b62d1805cc83d755972033c05cea78a1e177a159fc84da5c9c4ab6303ccbd9 \ 104 | --hash=sha256:666d717a4798eb9c5d3ae83fe80c7bc6ed696b93e879cb01cb24a74155c73612 \ 105 | --hash=sha256:65f877882b7ddede7090c7d87be27a0f4720fe7fc6fddd4409c06e1aa0f1ae8d \ 106 | --hash=sha256:7baf23adb698d8c6ca7339c9dde00931bc47b2dd82fa912827fef9f93db77f5e \ 107 | --hash=sha256:b3b687e905da32e5f2e5f16efa713f5d1fcd9fb8b8c697895de35c91fedeb086 \ 108 | --hash=sha256:a6cef5b31e27c31253c0f852b629a38d550ae66ec6850129c49d872f9ee428cb \ 109 | --hash=sha256:a0dcaf5648cecddc328e81a0421821a1f65a1d517b20746c94a1f0f5c36fb51a \ 110 | --hash=sha256:b5e439d9e55d645f2a4dca63e2f66d68fe974c405053b132d61c7e98c25dfeb2 \ 111 | --hash=sha256:dc8c5c23e7056e126275dbf29efba817b3d94196690930d0968873ac3a94ab82 \ 112 | --hash=sha256:a0ea10faa3bab0714d3a19c7e0921279a68d57552414d6eceaea99f97d7735db \ 113 | --hash=sha256:38892a254420d95594285077276162a5e9e9c30b6da08bdc2a4d53331ad9a6fa 114 | numpy==1.21.1; python_version >= "3.7" \ 115 | --hash=sha256:38e8648f9449a549a7dfe8d8755a5979b45b3538520d1e735637ef28e8c2dc50 \ 116 | --hash=sha256:fd7d7409fa643a91d0a05c7554dd68aa9c9bb16e186f6ccfe40d6e003156e33a \ 117 | --hash=sha256:a75b4498b1e93d8b700282dc8e655b8bd559c0904b3910b144646dbbbc03e062 \ 118 | --hash=sha256:1412aa0aec3e00bc23fbb8664d76552b4efde98fb71f60737c83efbac24112f1 \ 119 | --hash=sha256:e46ceaff65609b5399163de5893d8f2a82d3c77d5e56d976c8b5fb01faa6b671 \ 120 | --hash=sha256:c6a2324085dd52f96498419ba95b5777e40b6bcbc20088fddb9e8cbb58885e8e \ 121 | --hash=sha256:73101b2a1fef16602696d133db402a7e7586654682244344b8329cdcbbb82172 \ 122 | --hash=sha256:7a708a79c9a9d26904d1cca8d383bf869edf6f8e7650d85dbc77b041e8c5a0f8 \ 123 | --hash=sha256:95b995d0c413f5d0428b3f880e8fe1660ff9396dcd1f9eedbc311f37b5652e16 \ 124 | --hash=sha256:635e6bd31c9fb3d475c8f44a089569070d10a9ef18ed13738b03049280281267 \ 125 | --hash=sha256:4a3d5fb89bfe21be2ef47c0614b9c9c707b7362386c9a3ff1feae63e0267ccb6 \ 126 | --hash=sha256:8a326af80e86d0e9ce92bcc1e65c8ff88297de4fa14ee936cb2293d414c9ec63 \ 127 | --hash=sha256:791492091744b0fe390a6ce85cc1bf5149968ac7d5f0477288f78c89b385d9af \ 128 | --hash=sha256:0318c465786c1f63ac05d7c4dbcecd4d2d7e13f0959b01b534ea1e92202235c5 \ 129 | --hash=sha256:9a513bd9c1551894ee3d31369f9b07460ef223694098cf27d399513415855b68 \ 130 | --hash=sha256:91c6f5fc58df1e0a3cc0c3a717bb3308ff850abdaa6d2d802573ee2b11f674a8 \ 131 | --hash=sha256:978010b68e17150db8765355d1ccdd450f9fc916824e8c4e35ee620590e234cd \ 132 | --hash=sha256:9749a40a5b22333467f02fe11edc98f022133ee1bfa8ab99bda5e5437b831214 \ 133 | --hash=sha256:d7a4aeac3b94af92a9373d6e77b37691b86411f9745190d2c351f410ab3a791f \ 134 | --hash=sha256:d9e7912a56108aba9b31df688a4c4f5cb0d9d3787386b87d504762b6754fbb1b \ 135 | --hash=sha256:25b40b98ebdd272bc3020935427a4530b7d60dfbe1ab9381a39147834e985eac \ 136 | --hash=sha256:8a92c5aea763d14ba9d6475803fc7904bda7decc2a0a68153f587ad82941fec1 \ 137 | --hash=sha256:05a0f648eb28bae4bcb204e6fd14603de2908de982e761a2fc78efe0f19e96e1 \ 138 | --hash=sha256:f01f28075a92eede918b965e86e8f0ba7b7797a95aa8d35e1cc8821f5fc3ad6a \ 139 | --hash=sha256:88c0b89ad1cc24a5efbb99ff9ab5db0f9a86e9cc50240177a571fbe9c2860ac2 \ 140 | --hash=sha256:01721eefe70544d548425a07c80be8377096a54118070b8a62476866d5208e33 \ 141 | --hash=sha256:2d4d1de6e6fb3d28781c73fbde702ac97f03d79e4ffd6598b880b2d95d62ead4 \ 142 | --hash=sha256:dff4af63638afcc57a3dfb9e4b26d434a7a602d225b42d746ea7fe2edf1342fd 143 | packaging==21.3; python_version >= "3.7" \ 144 | --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522 \ 145 | --hash=sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb 146 | pillow==10.0.1; python_version >= "3.7" \ 147 | --hash=sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff \ 148 | --hash=sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f \ 149 | --hash=sha256:19e9adb3f22d4c416e7cd79b01375b17159d6990003633ff1d8377e21b7f1b21 \ 150 | --hash=sha256:28444cb6ad49726127d6b340217f0627abc8732f1194fd5352dec5e6a0105635 \ 151 | --hash=sha256:2872f2d7846cf39b3dbff64bc1104cc48c76145854256451d33c5faa55c04d1a \ 152 | --hash=sha256:2cc6b86ece42a11f16f55fe8903595eff2b25e0358dec635d0a701ac9586588f \ 153 | --hash=sha256:2d7e91b4379f7a76b31c2dda84ab9e20c6220488e50f7822e59dac36b0cd92b1 \ 154 | --hash=sha256:2fa6dd2661838c66f1a5473f3b49ab610c98a128fc08afbe81b91a1f0bf8c51d \ 155 | --hash=sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db \ 156 | --hash=sha256:3855447d98cced8670aaa63683808df905e956f00348732448b5a6df67ee5849 \ 157 | --hash=sha256:3a04359f308ebee571a3127fdb1bd01f88ba6f6fb6d087f8dd2e0d9bff43f2a7 \ 158 | --hash=sha256:3a0d3e54ab1df9df51b914b2233cf779a5a10dfd1ce339d0421748232cea9876 \ 159 | --hash=sha256:44e7e4587392953e5e251190a964675f61e4dae88d1e6edbe9f36d6243547ff3 \ 160 | --hash=sha256:459307cacdd4138edee3875bbe22a2492519e060660eaf378ba3b405d1c66317 \ 161 | --hash=sha256:4ce90f8a24e1c15465048959f1e94309dfef93af272633e8f37361b824532e91 \ 162 | --hash=sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d \ 163 | --hash=sha256:522ff4ac3aaf839242c6f4e5b406634bfea002469656ae8358644fc6c4856a3b \ 164 | --hash=sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd \ 165 | --hash=sha256:5d6c9049c6274c1bb565021367431ad04481ebb54872edecfcd6088d27edd6ed \ 166 | --hash=sha256:697a06bdcedd473b35e50a7e7506b1d8ceb832dc238a336bd6f4f5aa91a4b500 \ 167 | --hash=sha256:71671503e3015da1b50bd18951e2f9daf5b6ffe36d16f1eb2c45711a301521a7 \ 168 | --hash=sha256:723bd25051454cea9990203405fa6b74e043ea76d4968166dfd2569b0210886a \ 169 | --hash=sha256:764d2c0daf9c4d40ad12fbc0abd5da3af7f8aa11daf87e4fa1b834000f4b6b0a \ 170 | --hash=sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0 \ 171 | --hash=sha256:7f771e7219ff04b79e231d099c0a28ed83aa82af91fd5fa9fdb28f5b8d5addaf \ 172 | --hash=sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f \ 173 | --hash=sha256:84efb46e8d881bb06b35d1d541aa87f574b58e87f781cbba8d200daa835b42e1 \ 174 | --hash=sha256:898f1d306298ff40dc1b9ca24824f0488f6f039bc0e25cfb549d3195ffa17088 \ 175 | --hash=sha256:8b451d6ead6e3500b6ce5c7916a43d8d8d25ad74b9102a629baccc0808c54971 \ 176 | --hash=sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a \ 177 | --hash=sha256:92a23b0431941a33242b1f0ce6c88a952e09feeea9af4e8be48236a68ffe2205 \ 178 | --hash=sha256:93139acd8109edcdeffd85e3af8ae7d88b258b3a1e13a038f542b79b6d255c54 \ 179 | --hash=sha256:98533fd7fa764e5f85eebe56c8e4094db912ccbe6fbf3a58778d543cadd0db08 \ 180 | --hash=sha256:9f665d1e6474af9f9da5e86c2a3a2d2d6204e04d5af9c06b9d42afa6ebde3f21 \ 181 | --hash=sha256:b059ac2c4c7a97daafa7dc850b43b2d3667def858a4f112d1aa082e5c3d6cf7d \ 182 | --hash=sha256:b1be1c872b9b5fcc229adeadbeb51422a9633abd847c0ff87dc4ef9bb184ae08 \ 183 | --hash=sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e \ 184 | --hash=sha256:bc2e3069569ea9dbe88d6b8ea38f439a6aad8f6e7a6283a38edf61ddefb3a9bf \ 185 | --hash=sha256:bcf1207e2f2385a576832af02702de104be71301c2696d0012b1b93fe34aaa5b \ 186 | --hash=sha256:ca26ba5767888c84bf5a0c1a32f069e8204ce8c21d00a49c90dabeba00ce0145 \ 187 | --hash=sha256:cbe68deb8580462ca0d9eb56a81912f59eb4542e1ef8f987405e35a0179f4ea2 \ 188 | --hash=sha256:d6caf3cd38449ec3cd8a68b375e0c6fe4b6fd04edb6c9766b55ef84a6e8ddf2d \ 189 | --hash=sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d \ 190 | --hash=sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf \ 191 | --hash=sha256:de596695a75496deb3b499c8c4f8e60376e0516e1a774e7bc046f0f48cd620ad \ 192 | --hash=sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d \ 193 | --hash=sha256:ed2d9c0704f2dc4fa980b99d565c0c9a543fe5101c25b3d60488b8ba80f0cce1 \ 194 | --hash=sha256:ee7810cf7c83fa227ba9125de6084e5e8b08c59038a7b2c9045ef4dde61663b4 \ 195 | --hash=sha256:f0b4b06da13275bc02adfeb82643c4a6385bd08d26f03068c2796f60d125f6f2 \ 196 | --hash=sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19 \ 197 | --hash=sha256:f5bb289bb835f9fe1a1e9300d011eef4d69661bb9b34d5e196e5e82c4cb09b37 \ 198 | --hash=sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4 \ 199 | --hash=sha256:fcb59711009b0168d6ee0bd8fb5eb259c4ab1717b2f538bbf36bacf207ef7a68 \ 200 | --hash=sha256:fd2a5403a75b54661182b75ec6132437a181209b901446ee5724b589af8edef1 201 | pyparsing==3.0.6; python_version >= "3.7" \ 202 | --hash=sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4 \ 203 | --hash=sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81 204 | python-dateutil==2.8.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.7" \ 205 | --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ 206 | --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 207 | scikit-learn==1.0.1; python_version >= "3.7" \ 208 | --hash=sha256:ac2ca9dbb754d61cfe1c83ba8483498ef951d29b93ec09d6f002847f210a99da \ 209 | --hash=sha256:116e05fd990d9b363fc29bd3699ec2117d7da9088f6ca9a90173b240c5a063f1 \ 210 | --hash=sha256:bd78a2442c948536f677e2744917c37cff014559648102038822c23863741c27 \ 211 | --hash=sha256:32d941f12fd7e245f01da2b82943c5ce6f1133fa5375eb80caa51457532b3e7e \ 212 | --hash=sha256:fb7214103f6c36c1371dd8c166897e3528264a28f2e2e42573ba8c61ed4d7142 \ 213 | --hash=sha256:46248cc6a8b72490f723c73ff2e65e62633d14cafe9d2df3a7b3f87d332a6f7e \ 214 | --hash=sha256:fecb5102f0a36c16c1361ec519a7bb0260776ef40e17393a81f530569c916a7b \ 215 | --hash=sha256:02aee3b257617da0ec98dee9572b10523dc00c25b68c195ddf100c1a93b1854b \ 216 | --hash=sha256:538f3a85c4980c7572f3e754f0ba8489363976ef3e7f6a94e8f1af5ae45f6f6a \ 217 | --hash=sha256:59b1d6df8724003fa16b7365a3b43449ee152aa6e488dd7a19f933640bb2d7fb \ 218 | --hash=sha256:515b227f01f569145dc9f86e56f4cea9f00a613fc4d074bbfc0a92ca00bff467 \ 219 | --hash=sha256:fc75f81571137b39f9b31766e15a0e525331637e7fe8f8000a3fbfba7da3add9 \ 220 | --hash=sha256:648f4dbfdd0a1b45bf6e2e4afe3f431774c55dee05e2d28f8394d6648296f373 \ 221 | --hash=sha256:53bb7c605427ab187869d7a05cd3f524a3015a90e351c1788fc3a662e7f92b69 \ 222 | --hash=sha256:a800665527c1a63f7395a0baae3c89b0d97b54d2c23769c1c9879061bb80bc19 \ 223 | --hash=sha256:ee59da47e18b703f6de17d5d51b16ce086c50969d5a83db5217f0ae9372de232 \ 224 | --hash=sha256:ebbe4275556d3c02707bd93ae8b96d9651acd4165126e0ae64b336afa2a6dcb1 \ 225 | --hash=sha256:11a57405c1c3514227d0c6a0bee561c94cd1284b41e236f7a1d76b3975f77593 \ 226 | --hash=sha256:a51fdbc116974d9715957366df73e5ec6f0a7a2afa017864c2e5f5834e6f494d \ 227 | --hash=sha256:944f47b2d881b9d24aee40d643bfdc4bd2b6dc3d25b62964411c6d8882f940a1 \ 228 | --hash=sha256:fc60e0371e521995a6af2ef3f5d911568506124c272889b318b8b6e497251231 \ 229 | --hash=sha256:62ce4e3ddb6e6e9dcdb3e5ac7f0575dbaf56f79ce2b2edee55192b12b52df5be \ 230 | --hash=sha256:059c5be0c0365321ddbcac7abf0db806fad8ecb64ee6c7cbcd58313c7d61634d \ 231 | --hash=sha256:c6b9510fd2e1642314efb7aa951a0d05d963f3523e01c30b2dadde2395ebe6b4 \ 232 | --hash=sha256:c604a813df8e7d6dfca3ae0db0a8fd7e5dff4ea9d94081ab263c81bf0b61ab4b 233 | scipy==1.6.1; python_version >= "3.7" \ 234 | --hash=sha256:a15a1f3fc0abff33e792d6049161b7795909b40b97c6cc2934ed54384017ab76 \ 235 | --hash=sha256:e79570979ccdc3d165456dd62041d9556fb9733b86b4b6d818af7a0afc15f092 \ 236 | --hash=sha256:a423533c55fec61456dedee7b6ee7dce0bb6bfa395424ea374d25afa262be261 \ 237 | --hash=sha256:33d6b7df40d197bdd3049d64e8e680227151673465e5d85723b3b8f6b15a6ced \ 238 | --hash=sha256:6725e3fbb47da428794f243864f2297462e9ee448297c93ed1dcbc44335feb78 \ 239 | --hash=sha256:5fa9c6530b1661f1370bcd332a1e62ca7881785cc0f80c0d559b636567fab63c \ 240 | --hash=sha256:bd50daf727f7c195e26f27467c85ce653d41df4358a25b32434a50d8870fc519 \ 241 | --hash=sha256:f46dd15335e8a320b0fb4685f58b7471702234cba8bb3442b69a3e1dc329c345 \ 242 | --hash=sha256:0e5b0ccf63155d90da576edd2768b66fb276446c371b73841e3503be1d63fb5d \ 243 | --hash=sha256:2481efbb3740977e3c831edfd0bd9867be26387cacf24eb5e366a6a374d3d00d \ 244 | --hash=sha256:68cb4c424112cd4be886b4d979c5497fba190714085f46b8ae67a5e4416c32b4 \ 245 | --hash=sha256:5f331eeed0297232d2e6eea51b54e8278ed8bb10b099f69c44e2558c090d06bf \ 246 | --hash=sha256:0c8a51d33556bf70367452d4d601d1742c0e806cd0194785914daf19775f0e67 \ 247 | --hash=sha256:83bf7c16245c15bc58ee76c5418e46ea1811edcc2e2b03041b804e46084ab627 \ 248 | --hash=sha256:794e768cc5f779736593046c9714e0f3a5940bc6dcc1dba885ad64cbfb28e9f0 \ 249 | --hash=sha256:5da5471aed911fe7e52b86bf9ea32fb55ae93e2f0fac66c32e58897cfb02fa07 \ 250 | --hash=sha256:8e403a337749ed40af60e537cc4d4c03febddcc56cd26e774c9b1b600a70d3e4 \ 251 | --hash=sha256:a5193a098ae9f29af283dcf0041f762601faf2e595c0db1da929875b7570353f \ 252 | --hash=sha256:c4fceb864890b6168e79b0e714c585dbe2fd4222768ee90bc1aa0f8218691b11 253 | setuptools-scm==6.3.2; python_version >= "3.7" \ 254 | --hash=sha256:4c64444b1d49c4063ae60bfe1680f611c8b13833d556fd1d6050c0023162a119 \ 255 | --hash=sha256:a49aa8081eeb3514eb9728fa5040f2eaa962d6c6f4ec9c32f6c1fba88f88a0f2 256 | six==1.16.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.7" \ 257 | --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 \ 258 | --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 259 | threadpoolctl==3.0.0; python_version >= "3.7" \ 260 | --hash=sha256:4fade5b3b48ae4b1c30f200b28f39180371104fccc642e039e0f2435ec8cc211 \ 261 | --hash=sha256:d03115321233d0be715f0d3a5ad1d6c065fe425ddc2d671ca8e45e9fd5d7a52a 262 | tomli==1.2.2; python_version >= "3.7" \ 263 | --hash=sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade \ 264 | --hash=sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee 265 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------