├── .github └── workflows │ └── CI.yml ├── .gitignore ├── LICENSE ├── Others ├── 1_moment.png ├── 2_moment.png ├── J_t.png ├── SDE_1.png ├── W_t.png ├── X_trajectory.png ├── a_xt.png ├── b_xt.png ├── lambda.png └── xi.png ├── README.md ├── contributions.md ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ ├── 1_moment.png │ ├── 2_moment.png │ ├── X_trajectory.png │ └── q_ratio.png │ ├── conf.py │ ├── functions │ ├── formulae.rst │ ├── helper_functions.rst │ ├── index.rst │ ├── jd_process.rst │ ├── moments.rst │ ├── parameters.rst │ └── q_ratio.rst │ ├── index.rst │ ├── installation.rst │ ├── jd_processes.rst │ ├── license.rst │ ├── other_functions.rst │ └── q_ratio_exp.rst ├── jumpdiff ├── __init__.py ├── binning.py ├── formulae.py ├── jd_process.py ├── kernels.py ├── moments.py ├── parameters.py └── q_ratio.py ├── requirements.txt ├── setup.py ├── test ├── Qratio_test.py ├── binning_test.py ├── formulae_test.py ├── jd_test.py ├── kernels_test.py ├── moments_test.py ├── parameters_test.py └── versioning_libraries.py └── trial.py /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: jumpdiff CI 5 | 6 | on: push 7 | 8 | jobs: 9 | build: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [ubuntu-latest, macos-latest] 15 | python-version: [3.7, 3.8, 3.9, '3.10', '3.11'] 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install coverage pytest 26 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 27 | 28 | - name: Testing 29 | run: | 30 | coverage run -m pytest test/ 31 | 32 | - name: Upload code coverage 33 | uses: codecov/codecov-action@v1 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | Figs/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | 107 | *.gz 108 | 109 | *.bak 110 | 111 | *.out 112 | 113 | *.aux 114 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2021 Leonardo Rydin Gorjão 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Others/1_moment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRydin/jumpdiff/8f5e9fdbc223c70b37ccff09a80d738a052f6b44/Others/1_moment.png -------------------------------------------------------------------------------- /Others/2_moment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRydin/jumpdiff/8f5e9fdbc223c70b37ccff09a80d738a052f6b44/Others/2_moment.png -------------------------------------------------------------------------------- /Others/J_t.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRydin/jumpdiff/8f5e9fdbc223c70b37ccff09a80d738a052f6b44/Others/J_t.png -------------------------------------------------------------------------------- /Others/SDE_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRydin/jumpdiff/8f5e9fdbc223c70b37ccff09a80d738a052f6b44/Others/SDE_1.png -------------------------------------------------------------------------------- /Others/W_t.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRydin/jumpdiff/8f5e9fdbc223c70b37ccff09a80d738a052f6b44/Others/W_t.png -------------------------------------------------------------------------------- /Others/X_trajectory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRydin/jumpdiff/8f5e9fdbc223c70b37ccff09a80d738a052f6b44/Others/X_trajectory.png -------------------------------------------------------------------------------- /Others/a_xt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRydin/jumpdiff/8f5e9fdbc223c70b37ccff09a80d738a052f6b44/Others/a_xt.png -------------------------------------------------------------------------------- /Others/b_xt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRydin/jumpdiff/8f5e9fdbc223c70b37ccff09a80d738a052f6b44/Others/b_xt.png -------------------------------------------------------------------------------- /Others/lambda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRydin/jumpdiff/8f5e9fdbc223c70b37ccff09a80d738a052f6b44/Others/lambda.png -------------------------------------------------------------------------------- /Others/xi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRydin/jumpdiff/8f5e9fdbc223c70b37ccff09a80d738a052f6b44/Others/xi.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![PyPI - License](https://img.shields.io/pypi/l/jumpdiff) 2 | ![PyPI](https://img.shields.io/pypi/v/jumpdiff) 3 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/jumpdiff) 4 | [![Build Status](https://github.com/LRydin/jumpdiff/actions/workflows/CI.yml/badge.svg)](https://github.com/LRydin/jumpdiff/actions/workflows/CI.yml) 5 | [![codecov](https://codecov.io/gh/LRydin/jumpdiff/branch/master/graph/badge.svg)](https://codecov.io/gh/LRydin/jumpdiff) 6 | [![Documentation Status](https://readthedocs.org/projects/jumpdiff/badge/?version=latest)](https://jumpdiff.readthedocs.io/en/latest/?badge=latest) 7 | 8 | # jumpdiff 9 | `jumpdiff` is a `python` library with non-parametric Nadaraya─Watson estimators to extract the parameters of jump-diffusion processes. 10 | With `jumpdiff` one can extract the parameters of a jump-diffusion process from one-dimensional timeseries, employing both a kernel-density estimation method combined with a set on second-order corrections for a precise retrieval of the parameters for short timeseries. 11 | 12 | ## Installation 13 | To install `jumpdiff`, run 14 | 15 | ``` 16 | pip install jumpdiff 17 | ``` 18 | 19 | Then on your favourite editor just use 20 | 21 | ```python 22 | import jumpdiff as jd 23 | ``` 24 | 25 | ## Dependencies 26 | The library parameter estimation depends on `numpy` and `scipy` solely. The mathematical formulae depend on `sympy`. It stems from [`kramersmoyal`](https://github.com/LRydin/KramersMoyal) project, but functions independently from it3. 27 | 28 | ## Documentation 29 | You can find the documentation [here](https://jumpdiff.readthedocs.io/). 30 | 31 | # Jump-diffusion processes 32 | ## The theory 33 | Jump-diffusion processes1, as the name suggest, are a mixed type of stochastic processes with a diffusive and a jump term. 34 | One form of these processes which is mathematically traceable is given by the [Stochastic Differential Equation](https://en.wikipedia.org/wiki/Stochastic_differential_equation) 35 | 36 | 37 | 38 | which has 4 main elements: a drift term , a diffusion term , and jump amplitude term , which is given by a Gaussian distribution, and finally a jump rate . 39 | You can find a good review on this topic in Ref. 2. 40 | 41 | ## Integrating a jump-diffusion process 42 | Let us use the functions in `jumpdiff` to generate a jump-difussion process, and subsequently retrieve the parameters. This is a good way to understand the usage of the integrator and the non-parametric retrieval of the parameters. 43 | 44 | First we need to load our library. We will call it `jd` 45 | ```python 46 | import jumpdiff as jd 47 | ``` 48 | Let us thus define a jump-diffusion process and use `jd_process` to integrate it. Do notice here that we need the drift and diffusion as functions. 49 | 50 | ```python 51 | # integration time and time sampling 52 | t_final = 10000 53 | delta_t = 0.001 54 | 55 | # A drift function 56 | def a(x): 57 | return -0.5*x 58 | 59 | # and a (constant) diffusion term 60 | def b(x): 61 | return 0.75 62 | 63 | # Now define a jump amplitude and rate 64 | xi = 2.5 65 | lamb = 1.75 66 | 67 | # and simply call the integration function 68 | X = jd.jd_process(t_final, delta_t, a=a, b=b, xi=xi, lamb=lamb) 69 | ``` 70 | 71 | This will generate a jump diffusion process `X` of length `int(10000/0.001)` with the given parameters. 72 | 73 | 74 | 75 | ## Using `jumpdiff` to retrieve the parameters 76 | ### Moments and Kramers─Moyal coefficients 77 | Take the timeseries `X` and use the function `moments` to retrieve the conditional moments of the process. 78 | For now let us focus on the shortest time lag, so we can best approximate the Kramers─Moyal coefficients. 79 | For this case we can simply employ 80 | 81 | ```python 82 | edges, moments = jd.moments(timeseries = X) 83 | ``` 84 | In the array `edges` are the limits of our space, and in our array `moments` are recorded all 6 powers/order of our conditional moments. 85 | Let us take a look at these before we proceed, to get acquainted with them. 86 | 87 | We can plot the first moment with any conventional plotter, so lets use here `plotly` from `matplotlib` 88 | 89 | ```python 90 | import matplotlib.plotly as plt 91 | 92 | # we want the first power, so we need 'moments[1,...]' 93 | plt.plot(edges, moments[1,...]) 94 | ``` 95 | The first moment here (i.e., the first Kramers─Moyal coefficient) is given solely by the drift term that we have selected `-0.5*x` 96 | 97 | 98 | 99 | And the second moment (i.e., the second Kramers─Moyal coefficient) is a mixture of both the contributions of the diffusive term and the jump terms and . 100 | 101 | 102 | 103 | You have this stored in `moments[2,...]`. 104 | 105 | ### Retrieving the jump-related terms 106 | Naturally one of the most pertinent questions when addressing jump-diffusion processes is the possibility of recovering these same parameters from data. For the given jump-diffusion process we can use the `jump_amplitude` and `jump_rate` functions to non-parametrically estimate the jump amplitude and jump rate terms. 107 | 108 | After having the `moments` in hand, all we need is 109 | 110 | ```python 111 | # first estimate the jump amplitude 112 | xi_est = jd.jump_amplitude(moments = moments) 113 | 114 | # and now estimated the jump rate 115 | lamb_est = jd.jump_rate(moments = moments) 116 | ``` 117 | which resulted in our case in `(xi_est) ξ = 2.43 ± 0.17` and `(lamb_est) λ = 1.744 * delta_t` (don't forget to divide `lamb_est` by `delta_t`)! 118 | 119 | ### Other functions and options 120 | Include in this package is also the [Milstein scheme](https://en.wikipedia.org/wiki/Milstein_method) of integration, particularly important when the diffusion term has some spacial `x` dependence. `moments` can actually calculate the conditional moments for different lags, using the parameter `lag`. 121 | 122 | In `formulae` the set of formulas needed to calculate the second order corrections are given (in `sympy`). 123 | 124 | # Contributions 125 | We welcome reviews and ideas from everyone. If you want to share your ideas, upgrades, doubts, or simply report a bug, open an [issue](https://github.com/LRydin/jumpdiff/issues) here on GitHub, or contact us directly. 126 | If you need help with the code, the theory, or the implementation, drop us an email. 127 | We abide to a [Conduct of Fairness](contributions.md). 128 | 129 | # Changelog 130 | - Version 0.4 - Designing a set of self-consistency checks, the documentation, examples, and a trial code. Code at PyPi. 131 | - Version 0.3 - Designing a straightforward procedure to retrieve the jump amplitude and jump rate functions, alongside with a easy `sympy` displaying the correction. 132 | - Version 0.2 - Introducing the second-order corrections to the moments 133 | - Version 0.1 - Design an implementation of the `moments` functions, generalising `kramersmoyal` `km`. 134 | 135 | # Literature and Support 136 | 137 | ### History 138 | This project was started in 2017 at the [neurophysik](https://www.researchgate.net/lab/Klaus-Lehnertz-Lab-2) by Leonardo Rydin Gorjão, Jan Heysel, Klaus Lehnertz, and M. Reza Rahimi Tabar, and separately by Pedro G. Lind, at the Department of Computer Science, Oslo Metropolitan University. From 2019 to 2021, Pedro G. Lind, Leonardo Rydin Gorjão, and Dirk Witthaut developed a set of corrections and an implementation for python, presented here. 139 | 140 | ### Funding 141 | Helmholtz Association Initiative _Energy System 2050 - A Contribution of the Research Field Energy_ and the grant No. VH-NG-1025 and *STORM - Stochastics for Time-Space Risk Models* project of the Research Council of Norway (RCN) No. 274410. 142 | 143 | --- 144 | ##### Bibliography 145 | 146 | 1 Tabar, M. R. R. *Analysis and Data-Based Reconstruction of Complex Nonlinear Dynamical Systems.* Springer, International Publishing (2019), Chapter [*Stochastic Processes with Jumps and Non-vanishing Higher-Order Kramers–Moyal Coefficients*](https://doi.org/10.1007/978-3-030-18472-8_11). 147 | 148 | 2 Friedrich, R., Peinke, J., Sahimi, M., Tabar, M. R. R. *Approaching complexity by stochastic methods: From biological systems to turbulence,* [Physics Reports 506, 87–162 (2011)](https://doi.org/10.1016/j.physrep.2011.05.003). 149 | 150 | 3 Rydin Gorjão, L., Meirinhos, F. *kramersmoyal: Kramers–Moyal coefficients for stochastic processes.* [Journal of Open Source Software, **4**(44) (2019)](https://doi.org/10.21105/joss.01693). 151 | 152 | ##### Extended Literature 153 | You can find further reading on SDE, non-parametric estimatons, and the general principles of the Fokker–Planck equation, Kramers–Moyal expansion, and related topics in the classic (physics) books 154 | 155 | - Risken, H. *The Fokker–Planck equation.* Springer, Berlin, Heidelberg (1989). 156 | - Gardiner, C.W. *Handbook of Stochastic Methods.* Springer, Berlin (1985). 157 | 158 | And an extensive review on the subject [here](http://sharif.edu/~rahimitabar/pdfs/80.pdf) 159 | -------------------------------------------------------------------------------- /contributions.md: -------------------------------------------------------------------------------- 1 | # Contributions 2 | jumpdiff has lived so far out of collaborations. We welcome reviews and 3 | ideas from everyone. If you want to share your ideas or report a bug, open an 4 | [issue](https://github.com/LRydin/jumpdiff/issues) here on GitHub, or 5 | contact us directly: [leonardo.rydin"at"gmail.com](mailto:leonardo.rydin@gmail.com) 6 | 7 | We are happy to have you join us in furthering this project, helping us improve 8 | our code and documentation, make it more user-friendly, broadening its 9 | applicability, or devising other methodologies of implementation. 10 | 11 | If you need any help at understanding the code or theory behind it, or you wish 12 | to implement it in your own project, contact us, we are here to help. 13 | 14 | # Conduct of Fairness 15 | This package is a research-oriented project and abides to a strict conduct of 16 | fairness. 17 | We do not discriminate or accept to partake in any discriminatory acts, either 18 | against gender, gender identity and expression, sexual orientation, disability, 19 | personal appearance, ethnicity, race, age, or religion. 20 | We, as the authors of the package, reserve the right to remove any indication 21 | from third parties or other contributing members that display behaviours 22 | directed against the aforementioned. 23 | Posts, comments, issues raised, or harassment made online or elsewhere that does 24 | not abide to this conduct of fairness will be dealt with swiftly, deleted or 25 | edited, and reported when deemed threatening to others or ourselves. 26 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/_static/1_moment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRydin/jumpdiff/8f5e9fdbc223c70b37ccff09a80d738a052f6b44/docs/source/_static/1_moment.png -------------------------------------------------------------------------------- /docs/source/_static/2_moment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRydin/jumpdiff/8f5e9fdbc223c70b37ccff09a80d738a052f6b44/docs/source/_static/2_moment.png -------------------------------------------------------------------------------- /docs/source/_static/X_trajectory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRydin/jumpdiff/8f5e9fdbc223c70b37ccff09a80d738a052f6b44/docs/source/_static/X_trajectory.png -------------------------------------------------------------------------------- /docs/source/_static/q_ratio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LRydin/jumpdiff/8f5e9fdbc223c70b37ccff09a80d738a052f6b44/docs/source/_static/q_ratio.png -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | 14 | import sphinx_rtd_theme 15 | import os 16 | import sys 17 | sys.path.insert(0, os.path.abspath('../..')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'jumpdiff' 23 | copyright = '2019-2021, Leonardo Rydin Gorjão' 24 | author = 'Leonardo Rydin Gorjão' 25 | 26 | # The full version, including alpha/beta/rc tags 27 | release = '0.4.2' 28 | version = '0.4.2' 29 | 30 | # -- General configuration --------------------------------------------------- 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = ['sphinx.ext.napoleon', 36 | 'sphinx.ext.autodoc', 37 | 'sphinx.ext.coverage', 38 | ] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # List of patterns, relative to source directory, that match files and 44 | # directories to ignore when looking for source files. 45 | # This pattern also affects html_static_path and html_extra_path. 46 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 47 | 48 | 49 | # generate autosummary pages 50 | autosummary_generate = True 51 | autodoc_member_order = 'bysource' 52 | 53 | # Add any paths that contain templates here, relative to this directory. 54 | # templates_path = [''] 55 | 56 | suppress_warnings = ["ref.citation", "ref.footnote"] 57 | 58 | # The suffix of source filenames. 59 | source_suffix = ".rst" 60 | 61 | # The encoding of source files. 62 | source_encoding = "utf-8" 63 | 64 | # The master toctree document. 65 | master_doc = "index" 66 | 67 | # class_members_toctree = False 68 | 69 | # html_sidebars = { '**': ['localtoc.html', 'relations.html', 'sourcelink.html', 'searchbox.html'] } 70 | 71 | # -- Options for HTML output ------------------------------------------------- 72 | 73 | # The theme to use for HTML and HTML Help pages. See the documentation for 74 | # a list of builtin themes. 75 | # 76 | html_theme = 'sphinx_rtd_theme' 77 | # html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 78 | 79 | html_theme_options = { 80 | 'display_version': True, 81 | 'style_external_links': False, 82 | 'style_nav_header_background': 'black', 83 | # Toc options 84 | 'collapse_navigation': True, 85 | 'sticky_navigation': True, 86 | 'navigation_depth': 3, 87 | 'titles_only': False 88 | } 89 | 90 | # corrects line numbers being copyable 91 | html_codeblock_linenos_style = 'table' 92 | 93 | # Add any paths that contain custom static files (such as style sheets) here, 94 | # relative to this directory. They are copied after the builtin static files, 95 | # so a file named "default.css" will overwrite the builtin "default.css". 96 | html_static_path = ['_static'] 97 | -------------------------------------------------------------------------------- /docs/source/functions/formulae.rst: -------------------------------------------------------------------------------- 1 | Formulae 2 | -------- 3 | 4 | .. currentmodule:: jumpdiff.formulae 5 | 6 | .. automodule:: jumpdiff.formulae 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/source/functions/helper_functions.rst: -------------------------------------------------------------------------------- 1 | Helping functions 2 | ----------------- 3 | 4 | Kernels function 5 | ^^^^^^^^^^^^^^^^ 6 | 7 | .. currentmodule:: jumpdiff.kernels 8 | 9 | .. automodule:: jumpdiff.kernels 10 | :members: 11 | -------------------------------------------------------------------------------- /docs/source/functions/index.rst: -------------------------------------------------------------------------------- 1 | Functions 2 | ========= 3 | 4 | .. currentmodule:: jumpdiff 5 | 6 | Documentation for all the functions in :code:`jumpdiff`. 7 | 8 | .. include:: jd_process.rst 9 | 10 | .. include:: moments.rst 11 | 12 | .. include:: parameters.rst 13 | 14 | .. include:: q_ratio.rst 15 | 16 | .. include:: formulae.rst 17 | 18 | .. include:: helper_functions.rst 19 | -------------------------------------------------------------------------------- /docs/source/functions/jd_process.rst: -------------------------------------------------------------------------------- 1 | Jump-diffusion timeseries generator 2 | ----------------------------------- 3 | 4 | .. currentmodule:: jumpdiff.jd_process 5 | 6 | .. automodule:: jumpdiff.jd_process 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/source/functions/moments.rst: -------------------------------------------------------------------------------- 1 | Moments 2 | ------- 3 | 4 | .. currentmodule:: jumpdiff.moments 5 | 6 | .. automodule:: jumpdiff.moments 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/source/functions/parameters.rst: -------------------------------------------------------------------------------- 1 | Parameters 2 | ---------- 3 | 4 | .. currentmodule:: jumpdiff.parameters 5 | 6 | .. automodule:: jumpdiff.parameters 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/source/functions/q_ratio.rst: -------------------------------------------------------------------------------- 1 | Q-ratio 2 | ------- 3 | 4 | .. currentmodule:: jumpdiff.q_ratio 5 | 6 | .. automodule:: jumpdiff.q_ratio 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | jumpdiff 2 | ======== 3 | 4 | .. toctree:: 5 | :maxdepth: 3 6 | 7 | 8 | :code:`jumpdiff` is a :code:`python` library with non-parametric Nadaraya---Watson estimators to extract the parameters of jump-diffusion processes. 9 | With :code:`jumpdiff` one can extract the parameters of a jump-diffusion process from one-dimensional timeseries, employing both a kernel-density estimation method combined with a set on second-order corrections for a precise retrieval of the parameters for short timeseries. 10 | 11 | .. include:: installation.rst 12 | 13 | .. include:: jd_processes.rst 14 | 15 | .. include:: q_ratio_exp.rst 16 | 17 | Table of Content 18 | ================ 19 | 20 | .. toctree:: 21 | :maxdepth: 3 22 | 23 | installation 24 | jd_processes 25 | q_ratio_exp 26 | functions/index 27 | license 28 | 29 | Literature 30 | ========== 31 | 32 | | :sup:`1` Tabar, M. R. R. *Analysis and Data-Based Reconstruction of Complex Nonlinear Dynamical Systems.* Springer, International Publishing (2019), Chapter `Stochastic Processes with Jumps and Non-vanishing Higher-Order Kramers---Moyal Coefficients* `_. 33 | | :sup:`2` Friedrich, R., Peinke, J., Sahimi, M., Tabar, M. R. R. *Approaching complexity by stochastic methods: From biological systems to turbulence,* `Physics Reports 506, 87–162 (2011) `_. 34 | | :sup:`3` Rydin Gorjão, L., Meirinhos, F. *kramersmoyal: Kramers–Moyal coefficients for stochastic processes.* `Journal of Open Source Software, 4(44) (2019) `_. 35 | 36 | An extensive review on the subject can be found `here `_. 37 | 38 | Funding 39 | ======= 40 | 41 | Helmholtz Association Initiative *Energy System 2050 - A Contribution of the Research Field Energy* and the grant No. VH-NG-1025 and *STORM - Stochastics for Time-Space Risk Models* project of the Research Council of Norway (RCN) No. 274410. 42 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | To install :code:`jumpdiff` simply use 5 | 6 | :: 7 | 8 | pip install jumpdiff 9 | 10 | Then on your favourite editor just use 11 | 12 | .. code:: python 13 | 14 | import jumpdiff as jd 15 | 16 | The library depends on :code:`numpy`, :code:`scipy`, and :code:`sympy`. 17 | -------------------------------------------------------------------------------- /docs/source/jd_processes.rst: -------------------------------------------------------------------------------- 1 | Jump-diffusion processes 2 | ======================== 3 | 4 | We will show here how to: (1) generate trajectories of jump-diffusion processes; (2) retrieve the parameters from a single trajectory of a jump-diffusion process. 5 | Naturally, if we already had some data -- maybe from a real-world recording of a stochastic process -- we would simply look at estimating the parameters for this process. 6 | 7 | The theory 8 | ---------- 9 | Jump-diffusion processes\ :sup:`1`, as the name suggest, are a mixed type of stochastic processes with a diffusive and a jump term. 10 | One form of these processes which is mathematically traceable is given by the `Stochastic Differential Equation `_ 11 | 12 | .. math:: 13 | \mathrm{d} X(t) = a(x,t)\;\mathrm{d} t + b(x,t)\;\mathrm{d} W(t) + \xi\;\mathrm{d} J(t), 14 | 15 | which has four main elements: a drift term :math:`a(x,t)`, a diffusion term :math:`b(x,t)`, linked with a Wiener process :math:`W(t)`, a jump amplitude term :math:`\xi(x,t)`, which is given by a Gaussian distribution :math:`\mathcal{N}(0,\sigma_\xi^2)` coupled with a jump rate :math:`\lambda`, which is the rate of the Poissonian jumps :math:`J(t)`. 16 | You can find a good review on this topic in Ref. 2. 17 | 18 | Integrating a jump-diffusion process 19 | ------------------------------------ 20 | Let us use the functions in :code:`jumpdiff` to generate a jump-difussion process, and subsequently retrieve the parameters. This is a good way to understand the usage of the integrator and the non-parametric retrieval of the parameters. 21 | 22 | First we need to load our library. We will call it :code:`jd` 23 | 24 | .. code-block:: python 25 | :linenos: 26 | 27 | import jumpdiff as jd 28 | 29 | Let us thus define a jump-diffusion process and use :code:`jd_process` to integrate it. Do notice here that we need the drift :math:`a(x,t)` and diffusion :math:`b(x,t)` as functions. 30 | 31 | .. code-block:: python 32 | :linenos: 33 | :lineno-start: 2 34 | 35 | # integration time and time sampling 36 | t_final = 10000 37 | delta_t = 0.001 38 | 39 | # A drift function 40 | def a(x): 41 | return -0.5*x 42 | 43 | # and a (constant) diffusion term 44 | def b(x): 45 | return 0.75 46 | 47 | # Now define a jump amplitude and rate 48 | xi = 2.5 49 | lamb = 1.75 50 | 51 | # and simply call the integration function 52 | X = jd.jd_process(t_final, delta_t, a=a, b=b, xi=xi, lamb=lamb) 53 | 54 | 55 | This will generate a jump diffusion process :code:`X` of length :code:`int(10000/0.001)` with the given parameters. 56 | 57 | .. image:: /_static/X_trajectory.png 58 | :height: 250 59 | :align: center 60 | :alt: A jump-difussion process 61 | 62 | 63 | Using :code:`jumpdiff` to retrieve the parameters 64 | ------------------------------------------------- 65 | Moments and Kramers─Moyal coefficients 66 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 67 | Take the timeseries :code:`X` and use the function :code:`moments` to retrieve the conditional moments of the process. 68 | For now let us focus on the shortest time lag, so we can best approximate the Kramers---Moyal coefficients. 69 | For this case we can simply employ 70 | 71 | .. code-block:: python 72 | :linenos: 73 | :lineno-start: 20 74 | 75 | edges, moments = jd.moments(timeseries = X) 76 | 77 | In the array :code:`edges` are the limits of our space, and in our array :code:`moments` are recorded all 6 powers/order of our conditional moments. 78 | Let us take a look at these before we proceed, to get acquainted with them. 79 | 80 | We can plot the first moment with any conventional plotter, so lets use here :code:`plotly` from :code:`matplotlib`. 81 | To visualise the first moment, simply use 82 | 83 | .. code-block:: python 84 | :linenos: 85 | :lineno-start: 21 86 | 87 | import matplotlib.pyplot as plt 88 | plt.plot(edges, moments[1]/delta_t) 89 | 90 | .. image:: /_static/1_moment.png 91 | :height: 250 92 | :align: center 93 | :alt: The 1st Kramers---Moyal coefficient 94 | 95 | The first moment here (i.e., the first Kramers---Moyal coefficient) is given solely by the drift term that we have selected :code:`-0.5*x`. 96 | In the plot we have also included the theoretical curve, which we know from having selected the value of :code:`a(x)` in line :code:`8`. 97 | 98 | Similarly, we can extract the second moment (i.e., the second Kramers---Moyal coefficient) is a mixture of both the contributions of the diffusive term :math:`b(x)` and the jump terms :math:`\xi` and :math:`\lambda`. 99 | 100 | .. image:: /_static/2_moment.png 101 | :height: 250 102 | :align: center 103 | :alt: The 2nd Kramers---Moyal coefficient 104 | 105 | You have this stored in :code:`moments[2]`. 106 | 107 | Retrieving the jump-related terms 108 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 109 | Naturally one of the most pertinent questions when addressing jump-diffusion processes is the possibility of recovering these same parameters from data. For the given jump-diffusion process we can use the :code:`jump_amplitude` and :code:`jump_rate` functions to non-parametrically estimate the jump amplitude :math:`\xi` and :math:`\lambda` terms. 110 | 111 | After having the :code:`moments` in hand, all we need is 112 | 113 | .. code-block:: python 114 | :linenos: 115 | :lineno-start: 23 116 | 117 | # first estimate the jump amplitude 118 | xi_est = jd.jump_amplitude(moments = moments) 119 | 120 | # and now estimated the jump rate 121 | lamb_est = jd.jump_rate(moments = moments) 122 | 123 | which resulted in our case in :code:`(xi_est) ξ = 2.43 ± 0.17` and :code:`(lamb_est) λ = 1.744 * delta_t` (don't forget to divide :code:`lamb_est` by :code:`delta_t`)! 124 | We can compare these with our chose values in lines :code:`15-16`. 125 | -------------------------------------------------------------------------------- /docs/source/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | MIT License 5 | 6 | Copyright (c) 2019-2021 Leonardo Rydin Gorjão 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | 26 | Contact 27 | ======= 28 | If you need help with something, find a bug, issue, or typo on the repository 29 | or in the code, you can contact me here: leonardo.rydin@gmail.com or open an 30 | issue on the GitHub repository. 31 | -------------------------------------------------------------------------------- /docs/source/other_functions.rst: -------------------------------------------------------------------------------- 1 | Other functions and options 2 | --------------------------- 3 | Include in this package is also the `Milstein scheme `_ of integration, particularly important when the diffusion term has some spacial :code:`x` dependence. :code:`moments` can actually calculate the conditional moments for different lags, using the parameter :code:`lag`. 4 | 5 | In :code:`formulae` the set of formulas needed to calculate the second order corrections are given (in :code:`sympy`). 6 | -------------------------------------------------------------------------------- /docs/source/q_ratio_exp.rst: -------------------------------------------------------------------------------- 1 | Distinguishing pure diffusions from jump-diffusions 2 | --------------------------------------------------- 3 | 4 | One important question when we have some time series -- possibly from real-world data -- is to be able to discern if this timeseries is a pure diffusion process (a continuous stochastic process) or a jump-diffusion process (a discontinuous stochastic process). 5 | For this, :code:`jumpdiff` has an easy to use function, called :code:`q_ratio`. 6 | The idea behind distinguishing continuous and discontinuous processes is simple: 7 | diffusion processes *diffuse* over time, thus they take time to occupy space; jump-diffusion processes can jump, and thus statistically, they occupy all space very fast. 8 | 9 | To analyse this let us design a simple example -- with some numerically generated data -- that shows the use of :code:`q_ratio` and how to read it. 10 | 11 | Let us generate two trajectories, using :code:`jd_process`, denoted :code:`d_timeseries` and :code:`j_timeseries`, for *diffusion* timeseries and *jumpy* timeseries. 12 | Naturally the first must not include a jump term. 13 | To keep it simple, we will use the same parameters for both, expect for the jumps: 14 | 15 | .. code-block:: python 16 | :linenos: 17 | :lineno-start: 1 18 | 19 | import jumpdiff as jd 20 | 21 | # integration time and time sampling 22 | t_final = 10000 23 | delta_t = 0.01 24 | 25 | # Drift function 26 | def a(x): 27 | return -0.5*x 28 | 29 | # Diffusion function 30 | def b(x): 31 | return 0.75 32 | 33 | # generate 2 trajectories 34 | d_timeseries = jd.jd_process(t_final, delta_t, a=a, b=b, xi=0, lamb=0) 35 | j_timeseries = jd.jd_process(t_final, delta_t, a=a, b=b, xi=2.5, lamb=1.75) 36 | 37 | Note how :code:`xi` and :code:`lamb` are different for each process 38 | To now examine the rate of diffusion of the processes, we need to generate a time arrow, which we denote :code:`lag`. 39 | This needs to be a integer list :code:`>0`. 40 | 41 | 42 | .. code-block:: python 43 | :linenos: 44 | :lineno-start: 18 45 | 46 | import numpy as np 47 | lag = np.logspace(0, 3, 25, dtype=int) 48 | 49 | Lastly we just need to can the :code:`q_ratio` for our two timeseries 50 | 51 | .. code-block:: python 52 | :linenos: 53 | :lineno-start: 20 54 | 55 | d_lag, d_Q = jd.q_ratio(lag, d_timeseries) 56 | j_lag, j_Q = jd.q_ratio(lag, j_timeseries) 57 | 58 | 59 | And with the help of :code:`matplotlib`'s :code:`plotly`, we can visualise the results in a double logarithmic scale 60 | 61 | .. code-block:: python 62 | :linenos: 63 | :lineno-start: 22 64 | 65 | import matplotlib.plotly as plt 66 | 67 | plt.loglog(d_lag, d_Q, '-', label='diffusion') 68 | plt.loglog(j_lag, j_Q, 'o-', label='jump-diffusion') 69 | 70 | .. image:: /_static/q_ratio.png 71 | :height: 250 72 | :align: center 73 | :alt: Q-ratio of a diffusion and a jump-diffusion process 74 | 75 | As we can see, the diffusion process *grows* with our time arrow :code:`lag`, where the jump-diffusion is constant (does not depend on :code:`lag`). 76 | Jump processes will show a constant relation with code:`lag`, where diffusion processes a linear relation. 77 | -------------------------------------------------------------------------------- /jumpdiff/__init__.py: -------------------------------------------------------------------------------- 1 | from .q_ratio import q_ratio 2 | from .kernels import epanechnikov, silvermans_rule 3 | from .moments import moments, corrections 4 | from .jd_process import jd_process 5 | from .parameters import jump_amplitude, jump_rate 6 | from .formulae import m_formula, f_formula, f_formula_solver 7 | 8 | name = "jumpdiff" 9 | 10 | __version__ = "0.4.2" 11 | __author__ = "Leonardo Rydin Gorjão" 12 | __copyright__ = "Copyright 2019-2021 Leonardo Rydin Gorjão, MIT License" 13 | -------------------------------------------------------------------------------- /jumpdiff/binning.py: -------------------------------------------------------------------------------- 1 | ## This was developed by Francisco Meirinhos and Leonardo Rydin Gorjão for 2 | # kernel-density estimation of Kramers–Moyal coefficients in 3 | # https://github.com/LRydin/KramersMoyal and published in 4 | # 'kramersmoyal: Kramers--Moyal coefficients for stochastic processes'. Journal 5 | # of Open Source Software, 4(44), 1693, doi: 10.21105/joss.01693 6 | 7 | from scipy.sparse import csr_matrix 8 | import numpy as np 9 | 10 | def bincount1(x, weights, minlength=0): 11 | return np.array( 12 | [np.bincount(x, w, minlength = minlength) for w in weights]) 13 | 14 | def bincount2(x, weights, minlength=0): 15 | # Small speedup if # of weights is large 16 | assert len(x.shape) == 1 17 | 18 | ans_size = x.max() + 1 19 | 20 | if (ans_size < minlength): 21 | ans_size = minlength 22 | 23 | csr = csr_matrix((np.ones(x.shape[0]), (np.arange(x.shape[0]), x)), shape=[ 24 | x.shape[0], ans_size]) 25 | return weights * csr 26 | 27 | _range = range 28 | 29 | 30 | # An alternative to Numpy's histogramdd, supporting a weights matrix 31 | # Part of the following code is licensed under the BSD-3 License (from Numpy) 32 | def histogramdd(sample, bins=10, range=None, normed=None, weights=None, 33 | density=None, bw=0.0): 34 | 35 | def _get_outer_edges(a, range, bw): 36 | """ 37 | Determine the outer bin edges to use, from either the data or the range 38 | argument 39 | """ 40 | if range is not None: 41 | first_edge, last_edge = range 42 | if first_edge > last_edge: 43 | raise ValueError( 44 | 'max must be larger than min in range parameter.') 45 | if not (np.isfinite(first_edge) and np.isfinite(last_edge)): 46 | raise ValueError( 47 | "supplied range of [{}, {}] " 48 | " is not finite".format(first_edge, last_edge)) 49 | elif a.size == 0: 50 | # handle empty arrays. Can't determine range, so use 0-1. 51 | first_edge, last_edge = 0, 1 52 | else: 53 | first_edge, last_edge = a.min() - bw, a.max() + bw 54 | if not (np.isfinite(first_edge) and np.isfinite(last_edge)): 55 | raise ValueError( 56 | "autodetected range of [{}, {}] " 57 | "is not finite".format(first_edge, last_edge)) 58 | 59 | # expand empty range to avoid divide by zero 60 | if first_edge == last_edge: 61 | first_edge = first_edge - 0.5 62 | last_edge = last_edge + 0.5 63 | 64 | return first_edge, last_edge 65 | 66 | try: 67 | # Sample is an ND-array. 68 | N, D = sample.shape 69 | except (AttributeError, ValueError): 70 | # Sample is a sequence of 1D arrays. 71 | sample = np.atleast_2d(sample).T 72 | N, D = sample.shape 73 | 74 | nbin = np.empty(D, int) 75 | edges = D * [None] 76 | dedges = D * [None] 77 | if weights is not None: 78 | weights = np.asarray(weights) 79 | 80 | try: 81 | M = len(bins) 82 | if M != D: 83 | raise ValueError( 84 | "The dimension of bins must be equal to the dimension of the " 85 | "sample x.") 86 | except TypeError: 87 | # bins is an integer 88 | bins = D * [bins] 89 | 90 | # normalize the range argument 91 | if range is None: 92 | range = (None,) * D 93 | elif len(range) != D: 94 | raise ValueError("range argument must have one entry per dimension") 95 | 96 | # Create edge arrays 97 | for i in _range(D): 98 | if np.ndim(bins[i]) == 0: 99 | if bins[i] < 1: 100 | raise ValueError( 101 | "`bins[{}]` must be positive, when an integer".format(i)) 102 | smin, smax = _get_outer_edges(sample[:, i], range[i], bw) 103 | edges[i] = np.linspace(smin, smax, bins[i] + 1) 104 | elif np.ndim(bins[i]) == 1: 105 | edges[i] = np.asarray(bins[i]) 106 | if np.any(edges[i][:-1] > edges[i][1:]): 107 | raise ValueError( 108 | "`bins[{}]` must be monotonically increasing, when an array" 109 | .format(i)) 110 | else: 111 | raise ValueError( 112 | "`bins[{}]` must be a scalar or 1d array".format(i)) 113 | 114 | nbin[i] = len(edges[i]) + 1 # includes an outlier on each end 115 | dedges[i] = np.diff(edges[i]) 116 | 117 | # Compute the bin number each sample falls into. 118 | Ncount = tuple( 119 | # avoid np.digitize to work around gh-11022 120 | np.searchsorted(edges[i], sample[:, i], side='right') 121 | for i in _range(D) 122 | ) 123 | 124 | # Using digitize, values that fall on an edge are put in the right bin. 125 | # For the rightmost bin, we want values equal to the right edge to be 126 | # counted in the last bin, and not as an outlier. 127 | for i in _range(D): 128 | # Find which points are on the rightmost edge. 129 | on_edge = (sample[:, i] == edges[i][-1]) 130 | # Shift these points one bin to the left. 131 | Ncount[i][on_edge] -= 1 132 | 133 | # Compute the sample indices in the flattened histogram matrix. 134 | # This raises an error if the array is too large. 135 | xy = np.ravel_multi_index(Ncount, nbin) 136 | 137 | # Compute the number of repetitions in xy and assign it to the 138 | # flattened histmat. 139 | hist = bincount1(xy, weights, minlength=nbin.prod()) 140 | 141 | # Shape into a proper matrix 142 | if weights.ndim == 1: 143 | hist = hist.reshape(nbin) 144 | else: 145 | hist = hist.reshape((weights.shape[0], *nbin)) 146 | 147 | # This preserves the (bad) behavior observed in gh-7845, for now. 148 | hist = hist.astype(float, casting='safe') 149 | 150 | # Remove outliers (indices 0 and -1 for each dimension). 151 | core = D * (slice(1, -1),) 152 | hist = hist[(...,) + core] 153 | 154 | # handle the aliasing normed argument 155 | if normed is None: 156 | if density is None: 157 | density = False 158 | elif density is None: 159 | # an explicit normed argument was passed, alias it to the new name 160 | density = normed 161 | else: 162 | raise TypeError("Cannot specify both 'normed' and 'density'") 163 | 164 | if density: 165 | if weights.ndim == 1: 166 | # calculate the probability density function 167 | s = hist.sum() 168 | for i in _range(D): 169 | shape = np.ones(D, int) 170 | shape[i] = nbin[i] - 2 171 | hist = hist / dedges[i].reshape(shape) 172 | hist /= s 173 | else: 174 | for d in _range(weights.shape[0]): 175 | s = hist[d, ...].sum() 176 | for i in _range(D): 177 | shape = np.ones(D, int) 178 | shape[i] = nbin[i] - 2 179 | hist[d, ...] = hist[d, ...] / dedges[i].reshape(shape) 180 | hist[d, ...] /= s 181 | 182 | if weights.ndim == 1: 183 | if (hist.shape != nbin - 2).any(): 184 | raise RuntimeError("Internal Shape Error") 185 | else: 186 | if (hist.shape != np.array([weights.shape[0], *(nbin - 2)])).any(): 187 | raise RuntimeError("Internal Shape Error") 188 | 189 | return hist, edges 190 | -------------------------------------------------------------------------------- /jumpdiff/formulae.py: -------------------------------------------------------------------------------- 1 | ## Delevoped by Leonardo Rydin Gorjão and Pedro G. Lind. 2 | 3 | from sympy import bell, symbols, factorial, simplify 4 | 5 | def m_formula(power, tau = True): 6 | r""" 7 | Generate the formula for the conditional moments with second-order 8 | corrections based on the relation with the ordinary Bell polynomials 9 | 10 | .. math:: 11 | 12 | M_n(x^{\prime},\tau) \sim (n!)\tau D_n(x^{\prime}) + \frac{(n!)\tau^2}{2} 13 | \sum_{m=1}^{n-1} D_m(x^{\prime}) D_{n-m}(x^{\prime}) 14 | 15 | Parameters 16 | ---------- 17 | power: int 18 | Desired order of the formula. 19 | 20 | Returns 21 | ------- 22 | term: sympy.symbols 23 | Expression up to given ``power``. 24 | """ 25 | init_sym = symbols('D:'+str(int(power+1)))[1:] 26 | sym = () 27 | for i in range(1,power + 1): 28 | sym += (factorial(i)*init_sym[i-1],) 29 | 30 | if tau == True: 31 | t = symbols('tau') 32 | 33 | term = t*bell(power, 1, sym) + t**2*bell(power, 2, sym) 34 | 35 | else: 36 | term = bell(power, 1, sym) + bell(power, 2, sym) 37 | 38 | return term 39 | 40 | def f_formula(power): 41 | r""" 42 | Generate the formula for the conditional moments with second-order 43 | corrections based on the relation with the ordinary Bell polynomials 44 | 45 | .. math:: 46 | 47 | D_n(x) &= \frac{1}{\tau (n!)} \bigg[ \hat{B}_{n,1} 48 | \left(M_1(x,\tau),M_2(x,\tau),\ldots,M_{n}(x,\tau)\right) \\ 49 | &\qquad \left.-\frac{\tau}{2} \hat{B}_{n,2} 50 | \left(M_1(x,\tau),M_2(x,\tau),\ldots,M_{n-1}(x,\tau)\right)\right]. 51 | 52 | Parameters 53 | ---------- 54 | power: int 55 | Desired order of the formula. 56 | 57 | Returns 58 | ------- 59 | term: sympy.symbols 60 | Expression up to given ``power``. 61 | """ 62 | 63 | init_sym = symbols('D:'+str(int(power+1)))[1:] 64 | sym = () 65 | for i in range(1,power + 1): 66 | sym += (factorial(i)*init_sym[i-1],) 67 | 68 | term = (symbols('M'+str(int(power))) - bell(power, 2, sym))/factorial(power) 69 | 70 | return term 71 | 72 | 73 | def f_formula_solver(power): 74 | r""" 75 | Generate the reciprocal relation of the moments to the Kramers─Moyal 76 | coefficients by sequential iteration. 77 | 78 | .. math:: 79 | 80 | D_n(x) &= \frac{1}{\tau (n!)} \bigg[ \hat{B}_{n,1} 81 | \left(M_1(x,\tau),M_2(x,\tau),\ldots,M_{n}(x,\tau)\right) \\ 82 | &\qquad \left.-\frac{\tau}{2} \hat{B}_{n,2} 83 | \left(M_1(x,\tau),M_2(x,\tau),\ldots,M_{n-1}(x,\tau)\right)\right]. 84 | 85 | Parameters 86 | ---------- 87 | power: int 88 | Desired order of the formula. 89 | 90 | Returns 91 | ------- 92 | term: sympy.symbols 93 | Expression up to given ``power``. 94 | """ 95 | power = power + 1 96 | terms_to_sub = [] 97 | for i in range(1, power): 98 | terms_to_sub += [f_formula(i)] 99 | for j in range(i): 100 | terms_to_sub[i-1] = terms_to_sub[i-1].subs('D'+str(j+1),terms_to_sub[j]) 101 | 102 | # for i in range 103 | # term = (symbols('M'+str(int(power))) + bell(power, 2, sym))/factorial(power) 104 | 105 | return (terms_to_sub[power-2]*factorial(power-1)).expand(basic = True) 106 | -------------------------------------------------------------------------------- /jumpdiff/jd_process.py: -------------------------------------------------------------------------------- 1 | ## This is an implementation of a simple integrator for a generic 2 | # jump-diffusion process. This includes for a Euler─Mayurama and a Milstein 3 | # integration scheme. To use the Milstein scheme the derivative of the diffusion 4 | # function needs to be given. Created by Leonardo Rydin Gorjão and Pedro G. Lind 5 | 6 | import numpy as np 7 | 8 | def jd_process(time: float, delta_t: float, a: callable, b: callable, 9 | xi: float, lamb: float, init: float = None, solver: str = 'Euler', 10 | b_prime: callable = None) -> np.ndarray: 11 | r""" 12 | Integrates a jump-diffusion process with drift a(x), diffusion b(x), jump 13 | amplitude xi (:math:`\xi`), and jump rate lamb (:math:`\lambda`). 14 | 15 | .. math:: 16 | 17 | \mathrm{d} X(t) = a(x,t)\;\mathrm{d} t + b(x,t)\;\mathrm{d} W(t) 18 | + \xi\;\mathrm{d} J(t), 19 | 20 | with :math:`J` Poisson with jump rate :math:`\lambda`. This integrator has 21 | both an Euler─Maruyama and a Milstein method of integration. For Milstein 22 | one has to introduce the derivative of the diffusion term ``b``, denoted 23 | ``b_prime``. 24 | 25 | Parameters 26 | ---------- 27 | time: float > 0 28 | Total integration time. Positive float or int. 29 | 30 | delta_t: float > 0 31 | Time sampling, the smaller the better. 32 | 33 | a: callable 34 | The drift function. Can be a function of a ``lambda``. For an 35 | Ornstein─Uhlenbeck process with drift ``-2x``, a takes the form 36 | ``a = lambda x: -2x``. 37 | 38 | b: callable 39 | The diffusion function. Can be a function of a ``lambda``. For an 40 | Ornstein─Uhlenbeck process with diffusion ``1``, a takes the form 41 | ``b = lambda x: 1``. 42 | 43 | xi: float > 0 44 | Variance of the jump amplitude, which will be turned into a normal 45 | distribution like :math:`\mathcal{N}`\ ``(0,√xi)``. 46 | 47 | lamb: float > 0 48 | Jump rate of the Poissonian jumps. This is implemented as the numpy 49 | function ``np.random.poisson(lam = lamb * delta_t)``. 50 | 51 | init: float (defaul ``None``) 52 | Initial conditions. If ``None`` given, generates a random value from a 53 | normal distribution ~ :math:`\mathcal{N}`\ ``(0,√delta_t)``. 54 | 55 | solver: 'Euler' or 'Milstein' (defaul 'Euler') 56 | The regular Euler─Maruyama solver 'Euler' is the default, with an order 57 | of ``√delta_t``. To employ a state-dependent diffusion, i.e., b(x) as a 58 | function of x, the Milstein scheme has an order of ``delta_t``. You must 59 | introduce as well the derivative of b(x), i.e., b'(x), as the argument 60 | ``b_prime``. 61 | 62 | Returns 63 | ------- 64 | X: np.array 65 | Timeseries of size ``int(time/delta_t)`` 66 | """ 67 | 68 | # assert and conditions 69 | assert time > 0, "Total integration time must be positive" 70 | assert delta_t > 0, "Time sampling must be positive" 71 | if solver == 'Milstein': 72 | assert b_prime != None, "Introduce b'(x) to use the Milstein solver" 73 | assert callable(b_prime) == True, "b'(x) must be a function" 74 | 75 | assert callable(a) == True, "drift a(x) must be a function" 76 | assert callable(b) == True, "diffusion b(x) must be a function" 77 | assert isinstance(lamb, int) or isinstance(lamb, float), ("'lamb' is not an" 78 | " int or float") 79 | assert isinstance(xi, int) or isinstance(xi, float), ("'xi' is not an int " 80 | "or float") 81 | 82 | 83 | # Define total length of timeseries 84 | length = int(time/delta_t) 85 | 86 | # Initialise the array X 87 | X = np.zeros(length) 88 | 89 | # randomise initial starting value or use given (after assert) 90 | if init is None: 91 | X[0] = np.random.normal(loc=0, scale=np.sqrt(delta_t), size=1) 92 | else: 93 | assert isinstance(init, int) or isinstance(init, float), ("'init' is " 94 | "not an int or float") 95 | X[0] = float(init) 96 | 97 | # Generate the Gaussian noise 98 | dw = np.random.normal(loc=0, scale=np.sqrt(delta_t), size=length) 99 | 100 | # Generate the Poissonian Jumps 101 | dJ = np.random.poisson(lam=lamb * delta_t, size=length) 102 | 103 | 104 | # Integration, either Euler 105 | if solver == 'Euler': 106 | for i in range(1, length): 107 | X[i] = X[i-1] + a(X[i-1]) * delta_t + b(X[i-1]) * dw[i] 108 | if dJ[i] > 0.: 109 | # correction by @JChonpca_Huang (issue #5) 110 | X[i] += np.sum(np.random.normal(0, np.sqrt(xi), size=dJ[i])) 111 | 112 | if solver == 'Milstein': 113 | # Generate corrective terms of the Milstein integration method 114 | dw_2 = (dw**2 - delta_t) * 0.5 115 | 116 | for i in range(1, length): 117 | X[i] = X[i-1] + a(X[i-1]) * delta_t + b(X[i-1]) * dw[i] \ 118 | + b(X[i-1]) * b_prime(X[i-1]) * dw_2[i] 119 | if dJ[i] > 0.: 120 | # correction by @JChonpca_Huang (issue #5) 121 | X[i] += np.sum(np.random.normal(0, np.sqrt(xi), size=dJ[i])) 122 | 123 | return X 124 | -------------------------------------------------------------------------------- /jumpdiff/kernels.py: -------------------------------------------------------------------------------- 1 | ## This was developed by Francisco Meirinhos and Leonardo Rydin Gorjão for 2 | # kernel-density estimation of Kramers–Moyal coefficients in 3 | # https://github.com/LRydin/KramersMoyal and published in 4 | # 'kramersmoyal: Kramers--Moyal coefficients for stochastic processes'. Journal 5 | # of Open Source Software, 4(44), 1693, doi: 10.21105/joss.01693 6 | 7 | import numpy as np 8 | from functools import wraps 9 | from scipy.special import gamma, factorial2 10 | from scipy.stats import norm 11 | 12 | def kernel(kernel_func): 13 | r""" 14 | Transforms a kernel function into a scaled kernel function (for a certain 15 | bandwidth ``bw``). 16 | 17 | Currently implemented kernels are: 18 | Epanechnikov, Gaussian, Uniform, Triangular, Quartic. 19 | 20 | For a good overview of various kernels see 21 | https://en.wikipedia.org/wiki/Kernel_(statistics) 22 | """ 23 | @wraps(kernel_func) # just for naming 24 | def decorated(x, bw): 25 | if len(x.shape) == 1: 26 | x = x.reshape(-1, 1) 27 | 28 | dims = x.shape[-1] 29 | 30 | # Euclidean norm 31 | dist = np.sqrt((x * x).sum(axis=-1)) 32 | 33 | return kernel_func(dist / bw, dims) / (bw ** dims) 34 | return decorated 35 | 36 | 37 | def volume_unit_ball(dims: int) -> float: 38 | """ 39 | Returns the volume of a unit ball in dimensions dims. 40 | """ 41 | return np.pi ** (dims / 2.0) / gamma(dims / 2.0 + 1.0) 42 | 43 | @kernel 44 | def epanechnikov(x: np.ndarray, dims: int) -> np.ndarray: 45 | """ 46 | The Epanechnikov kernel in dimensions dims. 47 | """ 48 | x2 = (x ** 2) 49 | mask = x2 < 1.0 50 | kernel = np.zeros_like(x) 51 | kernel[mask] = (1.0 - x2[mask]) 52 | normalisation = 2.0 / (dims + 2.0) * volume_unit_ball(dims) 53 | return kernel / normalisation 54 | 55 | @kernel 56 | def gaussian(x: np.ndarray, dims: int) -> np.ndarray: 57 | """ 58 | Gaussian kernel in dimensions dims. 59 | """ 60 | def gaussian_integral(n): 61 | if n % 2 == 0: 62 | return np.sqrt(np.pi * 2) * factorial2(n - 1) / 2 63 | elif n % 2 == 1: 64 | return np.sqrt(np.pi * 2) * factorial2(n - 1) * norm.pdf(0) 65 | kernel = np.exp(-x ** 2 / 2.0) 66 | normalisation = dims * gaussian_integral(dims - 1) * volume_unit_ball(dims) 67 | return kernel / normalisation 68 | 69 | @kernel 70 | def uniform(x: np.ndarray, dims: int) -> np.ndarray: 71 | """ 72 | Uniform, or rectangular kernel in dimensions dims 73 | """ 74 | mask = x < 1.0 75 | kernel = np.zeros_like(x) 76 | kernel[mask] = 1.0 77 | normalisation = volume_unit_ball(dims) 78 | return kernel / normalisation 79 | 80 | @kernel 81 | def triagular(x: np.ndarray, dims: int) -> np.ndarray: 82 | """ 83 | Triagular kernel in dimensions dims 84 | """ 85 | mask = x < 1.0 86 | kernel = np.zeros_like(x) 87 | kernel[mask] = 1.0 - np.abs(x[mask]) 88 | normalisation = volume_unit_ball(dims) / 2.0 89 | return kernel / normalisation 90 | 91 | 92 | @kernel 93 | def quartic(x: np.ndarray, dims: int) -> np.ndarray: 94 | """ 95 | Quartic, or biweight kernel in dimensions dims 96 | """ 97 | x2 = (x ** 2) 98 | mask = x2 < 1.0 99 | kernel = np.zeros_like(x) 100 | kernel[mask] = ((1.0 - x2[mask]) ** 2) 101 | normalisation = 2.0 / (dims + 2.0) * volume_unit_ball(dims) 102 | return kernel / normalisation 103 | 104 | 105 | _kernels = {epanechnikov, gaussian, uniform, triagular, quartic} 106 | 107 | 108 | def silvermans_rule(timeseries: np.ndarray) -> float: 109 | n = timeseries.size 110 | sigma = np.std(timeseries, axis=0).max() 111 | 112 | return ( (4.0 * sigma**5) / (3 * n)) ** (1 / 5) 113 | -------------------------------------------------------------------------------- /jumpdiff/moments.py: -------------------------------------------------------------------------------- 1 | ## This was developed originally by Francisco Meirinhos and Leonardo Rydin 2 | # Gorjão for kernel-density estimation of Kramers–Moyal coefficients in 3 | # https://github.com/LRydin/KramersMoyal and published in 'kramersmoyal: 4 | # Kramers--Moyal coefficients for stochastic processes'. Journal of Open Source 5 | # Software, 4(44), 1693, doi: 10.21105/joss.01693. It is now extend by Leonardo 6 | # Rydin Gorjão and Pedro G. Lind to include second-order corrections of the 7 | # Kramers─Moyal conditional moments, as well as a built-in lag to calculate the 8 | # moments at different timesteps. 9 | 10 | import numpy as np 11 | from scipy.signal import convolve 12 | from scipy.special import factorial 13 | 14 | from .binning import histogramdd 15 | from .kernels import silvermans_rule, epanechnikov, _kernels 16 | 17 | def moments(timeseries: np.ndarray, bw: float = None, bins: np.ndarray = None, 18 | power: int = 6, lag: list = [1], correction: bool = True, 19 | norm: bool = False, kernel: callable = None, tol: float = 1e-10, 20 | conv_method: str = 'auto', verbose: bool = False) -> np.ndarray: 21 | r""" 22 | Estimates the moments of the Kramers─Moyal expansion from a timeseries using 23 | a Nadaraya─Watson kernel estimator method. These later can be turned into 24 | the drift and diffusion coefficients after normalisation. 25 | 26 | Parameters 27 | ---------- 28 | timeseries: np.ndarray 29 | A 1-dimensional timeseries. 30 | 31 | bw: float 32 | Desired bandwidth of the kernel. A value of 1 occupies the full space of 33 | the bin space. Recommended are values ``0.005 < bw < 0.4``. 34 | 35 | bins: np.ndarray (default ``None``) 36 | The number of bins for each dimension, defaults to ``np.array([5000])``. 37 | This is the underlying space for the Kramers─Moyal conditional moments. 38 | 39 | power: int (default ``6``) 40 | Upper limit of the the Kramers─Moyal conditional moments to calculate. 41 | It will generate all Kramers─Moyal conditional moments up to power. 42 | 43 | lag: list (default ``1``) 44 | Calculates the Kramers─Moyal conditional moments at each indicated lag, 45 | i.e., for ``timeseries[::lag[]]``. Defaults to ``1``, the shortest 46 | timestep in the data. 47 | 48 | corrections: bool (default ``True``) 49 | Implements the second-order corrections of the Kramers─Moyal conditional 50 | moments directly 51 | 52 | norm: bool (default ``False``) 53 | Sets the normalisation. ``False`` returns the Kramers─Moyal conditional 54 | moments, and ``True`` returns the Kramers─Moyal coefficients. 55 | 56 | kernel: callable (default ``None``) 57 | Kernel used to convolute with the Kramers─Moyal conditional moments. To 58 | select example an Epanechnikov kernel use 59 | kernel = kernels.epanechnikov 60 | If None the Epanechnikov kernel will be used. 61 | 62 | tol: float (default ``1e-10``) 63 | Round to zero absolute values smaller than ``tol``, after convolutions. 64 | 65 | conv_method: str (default ``auto``) 66 | A string indicating which method to use to calculate the convolution. 67 | docs.scipy.org/doc/scipy/reference/generated/scipy.signal.convolve. 68 | 69 | verbose: bool (default ``False``) 70 | If ``True`` will report on the bandwidth used. 71 | 72 | Returns 73 | ------- 74 | edges: np.ndarray 75 | The bin edges with shape (D,bins.shape) of the calculated moments. 76 | 77 | moments: np.ndarray 78 | The calculated moments from the Kramers─Moyal expansion of the 79 | timeseries at each lag. To extract the selected orders of the moments, 80 | use ``moments[i,:,j]``, with ``i`` the order according to powers, ``j`` 81 | the lag (if any given). 82 | 83 | """ 84 | 85 | timeseries = np.asarray_chkfinite(timeseries, dtype=float) 86 | if len(timeseries.shape) == 1: 87 | timeseries = timeseries.reshape(-1, 1) 88 | 89 | assert len(timeseries.shape) == 2, "Timeseries must be 1-dimensional" 90 | assert timeseries.shape[0] > 0, "No data in timeseries" 91 | 92 | if bins is None: 93 | bins = np.array([5000]) 94 | 95 | if lag is None: 96 | lag = [1] 97 | 98 | powers = np.linspace(0,power,power+1).astype(int) 99 | if len(powers.shape) == 1: 100 | powers = powers.reshape(-1, 1) 101 | 102 | if bw is None: 103 | bw = silvermans_rule(timeseries) 104 | elif callable(bw): 105 | bw = bw(timeseries) 106 | 107 | assert bw > 0.0, "Bandwidth must be > 0" 108 | 109 | if kernel is None: 110 | kernel = epanechnikov 111 | assert kernel in _kernels, "Kernel not found" 112 | 113 | if verbose == True: 114 | print(r'bandwidth = {:f}'.format(bw) + r', bins = {:d}'.format(bins[0])) 115 | 116 | edges, moments = _moments(timeseries, bins, powers, lag, kernel, bw, tol, 117 | conv_method) 118 | 119 | if correction == True: 120 | moments = corrections(m = moments, power = power) 121 | 122 | if norm == True: 123 | for i in range(power): 124 | moments = moments / float(factorial(i)) 125 | 126 | 127 | return (edges, moments) 128 | 129 | 130 | def _moments(timeseries: np.ndarray, bins: np.ndarray, powers: np.ndarray, 131 | lag: list, kernel: callable, bw: float, tol: float, conv_method: str): 132 | """ 133 | Helper function for km that does the heavy lifting and actually estimates 134 | the Kramers─Moyal coefficients from the timeseries. 135 | """ 136 | 137 | def cartesian_product(arrays: np.ndarray): 138 | # Taken from https://stackoverflow.com/questions/11144513 139 | la = len(arrays) 140 | arr = np.empty([len(a) for a in arrays] + [la], dtype=np.float64) 141 | for i, a in enumerate(np.ix_(*arrays)): 142 | arr[..., i] = a 143 | return arr.reshape(-1, la) 144 | 145 | def kernel_edges(edges: np.ndarray): 146 | # Generates the kernel edges 147 | edges_k = list() 148 | for edge in edges: 149 | dx = edge[1] - edge[0] 150 | L = edge.size 151 | edges_k.append(np.linspace(-dx * L, dx * L, int(2 * L + 1))) 152 | return edges_k 153 | 154 | # Calculate derivative and the product of its powers 155 | 156 | grads = np.diff(timeseries, axis=0) 157 | weights = np.prod(np.power(grads.T, powers[..., None]), axis=1) 158 | 159 | # Get weighted histogram 160 | hist, edges = histogramdd(timeseries[:-1, ...], bins=bins, 161 | weights=weights, bw=bw) 162 | 163 | # Generate centred kernel 164 | edges_k = kernel_edges(edges) 165 | mesh = cartesian_product(edges_k) 166 | kernel_ = kernel(mesh, bw=bw).reshape(*(edge.size for edge in edges_k)) 167 | kernel_ /= np.sum(kernel_) 168 | 169 | # Convolve weighted histogram with kernel and trim it 170 | kmc_temp = convolve(hist, kernel_[None, ...], mode='same', method=conv_method) 171 | 172 | moments = np.zeros(kmc_temp.shape + (len(lag),)) 173 | edge_ = np.zeros(edges[0][:-1].shape + (len(lag),)) 174 | 175 | for i in range(len(lag)): 176 | ts = timeseries[::lag[i]] 177 | grads = np.diff(ts, axis=0) 178 | weights = np.prod(np.power(grads.T, powers[..., None]), axis=1) 179 | 180 | # Get weighted histogram 181 | hist, edges = histogramdd(ts[:-1, ...], bins=bins, weights=weights, 182 | bw=bw) 183 | 184 | # Convolve weighted histogram with kernel and trim it 185 | kmc = convolve(hist, kernel_[None, ...], mode='same', 186 | method=conv_method) 187 | 188 | # Normalise 189 | mask = np.abs(kmc[0]) < tol 190 | kmc[0:, mask] = 0.0 191 | kmc[1:, ~mask] /= kmc[0, ~mask] 192 | 193 | # Pack moments and edges here 194 | moments[..., i] = kmc 195 | edge_[...,i] = [edge[:-1] + 0.5*(edge[1] - edge[0]) for edge in edges][0] 196 | 197 | 198 | return edge_, moments 199 | 200 | def corrections(m: np.ndarray, power: int): 201 | r""" 202 | The moments function will by default apply the corrections. You can turn 203 | the corrections off in that fuction by setting ``corrections = False``. 204 | 205 | Second-order corrections of the Kramers─Moyal coefficients (conditional 206 | moments), given by 207 | 208 | .. math:: 209 | 210 | F_1 &= M_1,\\ 211 | F_2 &= \frac{1}{2}\! \left(M_2-M_1^2\right ), \\ 212 | F_3 &= \frac{1}{6}\! \left( M_3-3M_1M_2+3M_1^3 \right ), \\ 213 | F_4 &= \frac{1}{24}\! \left(M_4-4M_1M_3+18M_1^2M_2-3M_2^2 214 | -15M_1^4 \right) , \\ 215 | F_5 &= \frac{1}{120}\!\left(M_5 -5 M_1 M_4 +30 M_1^2 M_3 -150 M_1^3 M_2 216 | +45 M_1 M_2^2-10 M_2 M_3 + 105 M_1^5 \right) \\ 217 | F_6 &= \frac{1} {720}\! \left (M_6 -6 M_1 M_5 + 45 M_1^2 M_4 -300 M_1^3 M_3 218 | +1575 M_1^4 M_2-675 M_1^2 M_2^2 \right. \\ 219 | & \qquad \left . \ +180 M_1 M_2 M_3+45 M_2^3 -15 M_2 M_4-10 M_3^2 220 | - 945 M_1^6\right ) \, , 221 | 222 | with the prefactor the normalisation, i.e., the normalised results are the 223 | Kramers─Moyal coefficients. If ``norm`` is False, this results in the 224 | Kramers─Moyal conditional moments. 225 | 226 | Parameters 227 | ---------- 228 | m (moments): np.ndarray 229 | The calculated conditional moments from the Kramers─Moyal expansion of 230 | the at each lag. To extract the selected orders of the moments use 231 | ``moments[i,:,j]``, with ``i`` the order according to powers, ``j`` the 232 | lag. 233 | 234 | power: int 235 | Upper limit of the Kramers─Moyal conditional moments to calculate. 236 | It will generate all Kramers─Moyal conditional moments up to power. 237 | 238 | Returns 239 | ------- 240 | F: np.ndarray 241 | The corrections of the calculated Kramers─Moyal conditional moments 242 | from the Kramers─Moyal expansion of the timeseries at each lag. To 243 | extract the selected orders of the moments, use ``F[i,:,j]``, with ``i`` 244 | the order according to powers, ``j`` the lag (if any introduced). 245 | """ 246 | 247 | powers = np.linspace(0, power, power + 1).astype(int) 248 | if len(powers.shape) == 1: 249 | powers = powers.reshape(-1, 1) 250 | 251 | # remnant of the normalisation factor, moved to 'moments' 252 | normalise = np.ones_like(powers) 253 | 254 | F = np.zeros_like(m) 255 | 256 | if 0 in powers: 257 | F[0] = normalise[0] * m[0] 258 | if 1 in powers: 259 | F[1] = normalise[1] * m[1] 260 | if 2 in powers: 261 | F[2] = normalise[2] * ( m[2] - (m[1]**2) ) 262 | if 3 in powers: 263 | F[3] = normalise[3] * ( m[3] - 3*m[1]*m[2] + 3*(m[1]**3) ) 264 | if 4 in powers: 265 | F[4] = normalise[4] * ( m[4] - 4*m[1]*m[3] + 18*(m[1]**2)*m[2] \ 266 | - 3*(m[2]**2) - 15*(m[1]**4) ) 267 | if 5 in powers: 268 | F[5] = normalise[5] * ( m[5] - 5*m[1]*m[4] + 30*(m[1]**2)*m[3] \ 269 | - 150*(m[1]**3)*m[2] + 45*m[1]*(m[2]**2) - 10*m[2]*m[3] \ 270 | + 105*(m[1]**5) ) 271 | if 6 in powers: 272 | F[6] = normalise[6] * ( m[6] - 6*m[1]*m[5] + 45*(m[1]**2)*m[4] \ 273 | - 300*(m[1]**3)*m[3] + 1575*(m[1]**4)*m[2] \ 274 | - 675*(m[1]**2)*(m[2]**2) + 180*m[1]*m[2]*m[3] \ 275 | + 45*(m[2]**3) - 15*m[2]*m[4] - 10*(m[3]**2) \ 276 | - 945*(m[1]**6) ) 277 | 278 | return F 279 | -------------------------------------------------------------------------------- /jumpdiff/parameters.py: -------------------------------------------------------------------------------- 1 | ## This is based on Lehnertz 2018, now implemented by Leonardo Rydin Gorjão and 2 | # Pedro G. Lind for direct retrieval of the jump amplitude and jump rate of a 3 | # jump diffusion process. 4 | 5 | import numpy as np 6 | 7 | def jump_amplitude(moments: np.ndarray, tol: float = 1e-10, 8 | full: bool = False, verbose: bool = False) -> np.ndarray: 9 | r""" 10 | Retrieves the jump amplitude xi (:math:`\xi`) via 11 | 12 | .. math:: 13 | 14 | \lambda(x,t) = \frac{M_4(x,t)}{3\sigma_{\xi}^4}. 15 | 16 | Take notice that the different normalisation of the ``moments`` leads to a 17 | different results. 18 | 19 | Parameters 20 | ---------- 21 | moments: np.ndarray 22 | Moments extracted with the function ``moments``. Needs moments up to 23 | order ``6``. 24 | 25 | tol: float (defaul ``1e-10``) 26 | Toleration for the division of the moments. 27 | 28 | full: bool (defaul ``False``) 29 | If ``True`` returns also the (biased) weighed standard deviation of the 30 | averaging process. 31 | 32 | verbose: bool (defaul ``True``) 33 | Prints the result. 34 | 35 | Returns 36 | ------- 37 | xi_est: np.ndarray 38 | Estimator of the jump amplitude xi (:math:`\xi`). 39 | 40 | References 41 | ---------- 42 | Anvari, M., Tabar, M. R. R., Peinke, J., Lehnertz, K., 'Disentangling the 43 | stochastic behavior of complex time series.' Scientific Reports, 6, 35435, 44 | 2016. doi: 10.1038/srep35435. 45 | 46 | Lehnertz, K., Zabawa, L., and Tabar, M. R. R., 'Characterizing abrupt 47 | transitions in stochastic dynamics.' New Journal of Physics, 20(11):113043, 48 | 2018. doi: 10.1088/1367-2630/aaf0d7. 49 | """ 50 | 51 | # pre-allocate variable 52 | xi_est = np.zeros(moments.shape[2]) 53 | xi_est_std = np.zeros(moments.shape[2]) 54 | 55 | 56 | for i in range(moments.shape[2]): 57 | mask = moments[0,:,i] < tol 58 | 59 | temp = (moments[6,~mask,i]) / (5 * moments[4,~mask,i]) 60 | 61 | xi_est[i] = np.average(temp, weights = moments[0,~mask,i]) 62 | 63 | xi_est_std[i] = np.average((temp-xi_est[i])**2, 64 | weights = moments[0,~mask,i]) 65 | 66 | if verbose == True: 67 | print(r'ξ = {:f}'.format(xi_est[i]) + r' ± {:f}'.format(xi_est_std[i])) 68 | 69 | if full == True: 70 | return xi_est, xi_est_std 71 | 72 | if full == False: 73 | return xi_est 74 | 75 | 76 | def jump_rate(moments: np.ndarray, xi_est: np.ndarray = None, 77 | tol: float = 1e-10, full: bool = False, 78 | verbose: bool = False) -> np.ndarray: 79 | r""" 80 | Retrieves the jump rate lamb (:math:`\lambda`) via 81 | 82 | .. math:: 83 | 84 | \sigma_{\xi}^2 = \frac{M_6(x,t)}{5M_4(x,t)}. 85 | 86 | Take notice that the different normalisation of the ``moments`` leads to a 87 | different results. 88 | 89 | Parameters 90 | ---------- 91 | moments: np.ndarray 92 | moments extracted with the function 'moments'. Needs moments of order 6. 93 | 94 | tol: float (defaul ``1e-10``) 95 | Toleration for the division of the moments. 96 | 97 | full: bool (defaul ``False``) 98 | If ``True`` returns also the (biased) weighed standard deviation of the 99 | averaging process. 100 | 101 | verbose: bool (defaul ``True``) 102 | Prints the result. 103 | 104 | Returns 105 | ------- 106 | xi_est: np.ndarray 107 | Estimator on the jump rate lamb (:math:`\lambda`) 108 | 109 | References 110 | ---------- 111 | Anvari, M., Tabar, M. R. R., Peinke, J., Lehnertz, K., 'Disentangling the 112 | stochastic behavior of complex time series.' Scientific Reports, 6, 35435, 113 | 2016. doi: 10.1038/srep35435. 114 | 115 | Lehnertz, K., Zabawa, L., and Tabar, M. R. R., 'Characterizing abrupt 116 | transitions in stochastic dynamics.' New Journal of Physics, 20(11):113043, 117 | 2018. doi: 10.1088/1367-2630/aaf0d7. 118 | """ 119 | 120 | # pre-allocate variable 121 | lamb_est = np.zeros(moments.shape[2]) 122 | lamb_est_std = np.zeros(moments.shape[2]) 123 | 124 | # requires knowing the jump amplitude of the process 125 | if xi_est == None: 126 | xi_est = jump_amplitude(moments = moments, tol = tol, 127 | full = False, verbose = False) 128 | else: 129 | # is xi_est is not iterable, turn it into a 1-entry array 130 | xi_est = np.array([xi_est]) 131 | 132 | for i in range(moments.shape[2]): 133 | mask = moments[0,:,i] < tol 134 | 135 | temp = (moments[4,~mask,i]) / (3 * (xi_est[i]**2) ) 136 | 137 | lamb_est[i] = np.average(temp, weights = moments[0,~mask,i]) 138 | 139 | lamb_est_std[i] = np.average((temp-lamb_est[i])**2, 140 | weights=moments[0,~mask,i]) 141 | 142 | if verbose == True: 143 | print((r'λ = {:f}'.format(lamb_est[i]) + 144 | r' ± {:f}'.format(lamb_est_std[i]))) 145 | 146 | if full == True: 147 | return lamb_est, lamb_est_std 148 | 149 | if full == False: 150 | return lamb_est 151 | -------------------------------------------------------------------------------- /jumpdiff/q_ratio.py: -------------------------------------------------------------------------------- 1 | ## This is based on Anvari, M., Tabar, M. R. R., Peinke, J., Lehnertz, K., 2 | # 'Disentangling the stochastic behavior of complex time series.' Scientific 3 | # Reports, 6, 35435, 2016. doi: 10.1038/srep35435, now implemented by Leonardo 4 | # Rydin Gorjão and Pedro G. Lind for direct application. 5 | 6 | import numpy as np 7 | from .moments import moments 8 | 9 | def q_ratio(lag: np.ndarray, timeseries: np.ndarray, loc: int = None, 10 | correction: bool = False) -> np.ndarray: 11 | r""" 12 | q_ratio method to distinguish pure diffusion from jump-diffusion timeseries, 13 | Given by the relation of the 4th and 6th Kramers─Moyal coefficient with 14 | increasing lag 15 | 16 | .. math:: 17 | 18 | Q(x,\tau) = \frac{D_6(x,\tau)}{5 D_4(x,\tau)} = \left\{\begin{array}{ll} 19 | b(x)^2 \tau, & \text{diffusive} \\ \sigma_\xi^2(x), & \text{jumpy} 20 | \end{array}\right. 21 | 22 | Parameters 23 | ---------- 24 | lag: np.ndarray of ints 25 | An array with the time-lag to extract the Kramers–Moyal coefficient for 26 | different lags. 27 | 28 | timeseries: np.ndarray 29 | A 1-dimensional timeseries. 30 | 31 | loc: float (defaul ``None``) 32 | Use a particular point in space to calculate the ratio. If ``None`` 33 | given, the maximum of the probability density function is taken. 34 | 35 | corrections: bool (defaul ``False``) 36 | Select whether to use corrective terms. 37 | 38 | Returns 39 | ------- 40 | lag: np.ndarray of ints 41 | Same as input, but only lag > 0 and as ints. 42 | 43 | ratio: np.ndarray of len(lag) 44 | Ratio of the sixth-order over forth-order Kramers–Moyal coefficient. 45 | 46 | References 47 | ---------- 48 | Anvari, M., Tabar, M. R. R., Peinke, J., Lehnertz, K., 'Disentangling the 49 | stochastic behavior of complex time series.' Scientific Reports, 6, 35435, 50 | 2016. doi: 10.1038/srep35435. 51 | 52 | Lehnertz, K., Zabawa, L., and Tabar, M. R. R., 'Characterizing abrupt 53 | transitions in stochastic dynamics.' New Journal of Physics, 20(11):113043, 54 | 2018. doi: 10.1088/1367-2630/aaf0d7. 55 | """ 56 | 57 | # Force lag to be ints, ensure lag > order + 1, and removes duplicates 58 | lag = lag[lag > 0] 59 | lag = np.round(np.unique(lag)).astype(int) 60 | 61 | # Assert if timeseries is 1 dimensional 62 | if timeseries.ndim > 1: 63 | assert timeseries.shape[1] == 1, "Timeseries needs to be 1-dimensional" 64 | 65 | # Find maximum of distribution 66 | if loc == None: 67 | temp = moments(timeseries, power=0, bins=np.array([5000]))[1] 68 | loc = np.argmax(temp[0]) 69 | 70 | temp = moments(timeseries, power=6, bins=np.array([5000]), lag = lag, 71 | correction = correction)[1] 72 | ratio = temp[6,loc,:]/(5 * temp[4,loc,:]) 73 | 74 | return lag, ratio 75 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | scipy 3 | sympy 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="jumpdiff", 8 | version="0.4.2", 9 | author="Leonardo Rydin Gorjão", 10 | author_email="leonardo.rydin@gmail.com", 11 | description="jumpdiff: Non-parametric estimators for jump-diffusion processes for Python.", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/LRydin/jumpdiff", 15 | packages=setuptools.find_packages(), 16 | install_requires=[ 17 | "numpy", 18 | "scipy", 19 | "sympy", 20 | ], 21 | classifiers=[ 22 | "Programming Language :: Python :: 3", 23 | "License :: OSI Approved :: MIT License", 24 | "Operating System :: OS Independent", 25 | ], 26 | license="MIT License", 27 | python_requires='>=3.5', 28 | ) 29 | -------------------------------------------------------------------------------- /test/Qratio_test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from jumpdiff import q_ratio, jd_process 3 | 4 | def test_Qratio(): 5 | for delta in [1,0.1,0.01,0.001,0.0001]: 6 | t_final = 1000 7 | delta_t = delta 8 | 9 | # let us define a drift function 10 | def a(x): 11 | return -0.5*x 12 | 13 | # and a (constant) diffusion term 14 | def b(x): 15 | return 0.75 16 | 17 | # Now define a jump amplitude and rate 18 | xi = 1.5 19 | lamb = 1.25 20 | 21 | # and simply call the integration function 22 | X = jd_process(t_final, delta_t, a=a, b=b, xi=xi, lamb=lamb) 23 | 24 | lag = np.unique(np.logspace(0, np.log10(int(t_final/delta_t) // 100), 200).astype(int)+1) 25 | 26 | _, ratio = q_ratio(lag = lag, timeseries = X) 27 | 28 | assert isinstance(ratio, np.ndarray) 29 | assert ratio.shape[0] == lag.shape[0] 30 | -------------------------------------------------------------------------------- /test/binning_test.py: -------------------------------------------------------------------------------- 1 | # Addapted from 'kramersmoyal' for jumpdiff 2 | 3 | import numpy as np 4 | 5 | from jumpdiff.binning import histogramdd 6 | 7 | N = 1000000 8 | 9 | def test_binning(): 10 | for dim in [1]: 11 | bins = np.array([30] * dim) 12 | timeseries = np.random.rand(N, dim) 13 | 14 | Nw = 10 15 | weights = np.random.rand(N, Nw) 16 | 17 | hist1 = [np.histogramdd(timeseries, bins=bins, 18 | weights=w, density=True)[0] for w in weights.T] 19 | 20 | hist2 = histogramdd(timeseries, bins=bins, 21 | weights=weights.T, density=True)[0] 22 | 23 | assert np.array( 24 | list(map(lambda i: (hist1[i] == hist2[i, ...]), range(Nw)))).all() 25 | -------------------------------------------------------------------------------- /test/formulae_test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from jumpdiff.formulae import m_formula, f_formula, f_formula_solver 3 | 4 | def test_fomulae(): 5 | for power in [1,2,3,4,5,6,7,8,9]: 6 | 7 | m_formula(power = power) 8 | 9 | f_formula(power = power) 10 | 11 | f_formula_solver(power = power) 12 | -------------------------------------------------------------------------------- /test/jd_test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from jumpdiff import jd_process 3 | 4 | def test_jdprocess(): 5 | for delta in [1,0.1,0.01,0.001,0.0001]: 6 | for scheme in ['Euler', 'Milstein']: 7 | t_final = 1000 8 | delta_t = delta 9 | 10 | # let us define a drift function 11 | def a(x): 12 | return -0.5*x 13 | 14 | # and a (constant) diffusion term 15 | def b(x): 16 | return 0.75 17 | 18 | if scheme == 'Milstein': 19 | def b_prime(x): 20 | return 0. 21 | 22 | # Now define a jump amplitude and rate 23 | xi = 1.5 24 | lamb = 1.25 25 | if scheme == 'Euler': 26 | X = jd_process(t_final, delta_t, a=a, b=b, xi=xi, lamb=lamb) 27 | 28 | if scheme == 'Milstein': 29 | X = jd_process(t_final, delta_t, a=a, b=b, xi=xi, lamb=lamb, 30 | b_prime=b_prime, init= 0., solver = scheme) 31 | 32 | assert isinstance(X, np.ndarray) 33 | assert X.shape[0] == int(t_final/delta_t) 34 | -------------------------------------------------------------------------------- /test/kernels_test.py: -------------------------------------------------------------------------------- 1 | # Based on 'kramersmoyal'. Identical test scheme 2 | 3 | import numpy as np 4 | from itertools import product 5 | 6 | from jumpdiff.kernels import * 7 | 8 | def test_kernels(): 9 | for dim in [1]: 10 | edges = [np.linspace(-10, 10, 100000 // 10**dim, endpoint=True)] * dim 11 | mesh = np.asarray(list(product(*edges))) 12 | dx = (edges[0][1] - edges[0][0]) ** dim 13 | for kernel in [epanechnikov, gaussian, uniform, triagular]: 14 | for bw in [0.1, 0.3, 0.5, 1.0, 1.5, 2.0]: 15 | kernel_ = kernel(mesh, bw=bw).reshape( 16 | *(edge.size for edge in edges)) 17 | assert np.allclose(kernel_.sum() * dx, 1, atol=1e-2) 18 | -------------------------------------------------------------------------------- /test/moments_test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from jumpdiff import jd_process, moments 3 | 4 | def test_moments(): 5 | for delta in [1,0.1,0.01,0.001]: 6 | for lag in [None, [1,2,3]]: 7 | t_final = 1000 8 | delta_t = delta 9 | 10 | # let us define a drift function 11 | def a(x): 12 | return -0.5*x 13 | 14 | # and a (constant) diffusion term 15 | def b(x): 16 | return 0.75 17 | 18 | 19 | # Now define a jump amplitude and rate 20 | xi = 1.5 21 | lamb = 1.25 22 | 23 | X = jd_process(t_final, delta_t, a = a, b = b, xi = xi, lamb = lamb) 24 | 25 | edges, m = moments(timeseries = X, lag = lag) 26 | 27 | assert isinstance(edges, np.ndarray) 28 | assert isinstance(m, np.ndarray) 29 | 30 | edges, m = moments(timeseries = X, lag = lag, bw = 0.3) 31 | 32 | assert isinstance(edges, np.ndarray) 33 | assert isinstance(m, np.ndarray) 34 | 35 | edges, m = moments(timeseries = X, lag = lag, norm = True) 36 | 37 | assert isinstance(edges, np.ndarray) 38 | assert isinstance(m, np.ndarray) 39 | -------------------------------------------------------------------------------- /test/parameters_test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from jumpdiff import jd_process, jump_amplitude, jump_rate, moments 3 | 4 | def test_parameters(): 5 | for delta in [1,0.1,0.01,0.001,0.0001]: 6 | for full in [True, False]: 7 | t_final = 1000 8 | delta_t = delta 9 | 10 | # let us define a drift function 11 | def a(x): 12 | return -0.5*x 13 | 14 | # and a (constant) diffusion term 15 | def b(x): 16 | return 0.75 17 | 18 | # Now define a jump amplitude and rate 19 | xi = 1.5 20 | lamb = 1.25 21 | 22 | # and simply call the integration function 23 | X = jd_process(t_final, delta_t, a=a, b=b, xi=xi, lamb=lamb) 24 | 25 | edges, m = moments(timeseries = X) 26 | 27 | if full == False: 28 | xi_est = jump_amplitude(moments = m, full = full) 29 | lamb_est = jump_rate(moments = m, full = full) 30 | 31 | assert isinstance(lamb_est, np.ndarray) 32 | assert isinstance(xi_est, np.ndarray) 33 | 34 | if full == True: 35 | xi_est, xi_est_std = jump_amplitude(moments = m, full = full) 36 | lamb_est, lamb_est_std = jump_rate(moments = m, full = full) 37 | 38 | assert isinstance(lamb_est, np.ndarray) 39 | assert isinstance(xi_est, np.ndarray) 40 | assert isinstance(lamb_est_std, np.ndarray) 41 | assert isinstance(xi_est_std, np.ndarray) 42 | -------------------------------------------------------------------------------- /test/versioning_libraries.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import numpy 3 | import scipy 4 | import sympy 5 | 6 | def versioning_libraries(): 7 | print('python:', sys.version) 8 | print('numpy:', numpy.__version__) 9 | print('scipy:', scipy.__version__) 10 | print('sympy:', sympy.__version__) 11 | -------------------------------------------------------------------------------- /trial.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | 4 | # This is a short python script to play around with jumpdiff 5 | import jumpdiff as jd 6 | 7 | # %% Let's first integrate a jump-diffusion process 8 | # Define: 9 | # integration time and time sampling 10 | t_final = 10000 11 | delta_t = 0.01 12 | 13 | # A drift function 14 | def a(x): 15 | return -0.5*x 16 | 17 | # and a (constant) diffusion term 18 | def b(x): 19 | return 0.75 20 | 21 | # Now define a jump amplitude and rate 22 | xi = 2.5 23 | lamb = 1.75 24 | 25 | # and let jdprocess integrate the stochastic path 26 | X = jd.jd_process(t_final, delta_t, a=a, b=b, xi=xi, lamb=lamb) 27 | 28 | 29 | # %% Plot the trajectory of the jump-diffusion process 30 | fig, ax = plt.subplots(1,1,figsize=(6,3)) 31 | 32 | ax.plot(np.linspace(0,t_final,int(t_final/delta_t)), X, color = 'black') 33 | ax.set_xlabel('t', fontsize=16) 34 | ax.set_ylabel('x', fontsize=16) 35 | 36 | 37 | # %% To retrieve the moments, use: 38 | edges, moments = jd.moments(timeseries = X, bw = 0.35) 39 | # and don't forget that the Kramers─Moyal coefficient need `moments/delta_t` 40 | 41 | 42 | # %% Let us plot the first Kramers─Moyal coefficient 'moments[1,...]/delta_t' 43 | fig, ax = plt.subplots(1,1,figsize=(6,3)) 44 | 45 | ax.plot(edges, moments[1,...]/delta_t, color = 'black', label = '1st Kramers─Moyal coefficient') 46 | ax.plot(edges, a(edges), '--', color = 'black', label = 'Theoretical curve a = -0.5*x') 47 | ax.set_xlim([-5,5]); ax.set_ylim([-5,5]) 48 | 49 | ax.set_xlabel('x', fontsize=16) 50 | ax.set_ylabel('$D_1$(x)', fontsize=16) 51 | ax.legend(fontsize=13) 52 | 53 | 54 | # %% The second Kramers─Moyal coefficient 'moments[2,...]/delta_t' 55 | fig, ax = plt.subplots(1,1,figsize=(6,3)) 56 | 57 | ax.plot(edges, moments[2,...]/delta_t, color = 'black', label = '2nd Kramers─Moyal coefficient') 58 | ax.plot(edges, (b(0)**2 + xi*lamb)*np.ones_like(edges), '--', color = 'black', label = 'Theoretical curve $b^2+λξ$') 59 | ax.set_xlim([-5,5]); ax.set_ylim([3,8]) 60 | 61 | ax.set_xlabel('x', fontsize=16) 62 | ax.set_ylabel('$D_2$(x)', fontsize=16) 63 | ax.legend(fontsize=13) 64 | 65 | 66 | # %% And the fourth Kramers─Moyal coefficient 'moments[4,...]/delta_t' 67 | fig, ax = plt.subplots(1,1,figsize=(6,3)) 68 | 69 | ax.plot(edges, moments[4,...]/delta_t, color = 'black', label = '4th Kramers─Moyal coefficient') 70 | ax.plot(edges, (3*(xi**2)*lamb)*np.ones_like(edges), '--', color = 'black', label = 'Theoretical curve $3λξ^2$') 71 | ax.set_xlim([-5,5]); ax.set_ylim([0,60]) 72 | 73 | ax.set_xlabel('x', fontsize=16) 74 | ax.set_ylabel('$D_4$(x)', fontsize=16) 75 | ax.legend(fontsize=13) 76 | 77 | 78 | # %% Finally, we can use simply the 'jump_amplitude' and 'jump_rate' functions 79 | # to recover the ξ and λ parameters 80 | 81 | xi_est = jd.jump_amplitude(moments = moments, verbose = True) 82 | print(xi_est) 83 | 84 | lamb_est = jd.jump_rate(moments = moments, xi_est = xi, verbose = True) 85 | print(lamb_est/delta_t) 86 | # Don't forget that the jump rate λ needs to be divide by 'delta_t' to yield a 87 | # comparible result. 88 | 89 | # %% ######################################################################### 90 | 91 | # To understand the usage of the q_ratio function, let us generate to sample 92 | # trajectories: one without jumps, denoted d_timeseries, and one with jumps 93 | # denoted j_timeseries. 94 | 95 | # integration time and time sampling 96 | t_final = 10000 97 | delta_t = 0.01 98 | 99 | # Drift function 100 | def a(x): 101 | return -0.5*x 102 | 103 | # Diffusion function 104 | def b(x): 105 | return 0.75 106 | 107 | # generate 2 trajectories 108 | d_timeseries = jd.jd_process(t_final, delta_t, a=a, b=b, xi=0, lamb=0) 109 | j_timeseries = jd.jd_process(t_final, delta_t, a=a, b=b, xi=2.5, lamb=1.75) 110 | 111 | # %% Subsequently we time a time scale to analyse, as 112 | lag = np.logspace(0, 3, 25, dtype=int) 113 | 114 | d_lag, d_Q = jd.q_ratio(lag, d_timeseries) 115 | j_lag, j_Q = jd.q_ratio(lag, j_timeseries) 116 | 117 | # %% we can then finally plot the results 118 | fig, ax = plt.subplots(1,1,figsize=(6,3)) 119 | 120 | ax.loglog(d_lag, d_Q, '-', color = 'black', label='diffusion') 121 | ax.loglog(j_lag, j_Q, 'o-', color = 'black', label='jump-diffusion') 122 | # ax.set_xlim([-5,5]); ax.set_ylim([-5,5]) 123 | 124 | ax.set_xlabel('lag', fontsize=16) 125 | ax.set_ylabel('$Q$-ratio', fontsize=16) 126 | ax.legend(fontsize=13) 127 | 128 | fig.tight_layout() 129 | fig.savefig('q_ratio.png', dpi=300) 130 | 131 | # %% If one wishes to check the formulae behind the corrections of the moments, 132 | # simply choose the desired power: 133 | power = 4 134 | 135 | jd.m_formula(power = power) 136 | --------------------------------------------------------------------------------