├── .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 | 
2 | 
3 | 
4 | [](https://github.com/LRydin/jumpdiff/actions/workflows/CI.yml)
5 | [](https://codecov.io/gh/LRydin/jumpdiff)
6 | [](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 |
--------------------------------------------------------------------------------