From 51ee271388fbe354e1beb7fb7547d4db5d000491 Mon Sep 17 00:00:00 2001 From: Luca Date: Sun, 22 Feb 2026 13:36:18 +0000 Subject: [PATCH] Add double exponential --- Makefile | 34 --------- app/double_exponential_sampling.py | 81 ++++++++++++++++++++++ app/gaussian_sampling.py | 1 - dev/marimo | 3 + dev/nbconvert | 7 -- dev/nbsync | 7 -- dev/notebook_config.json | 8 --- dev/start-jupyter | 11 --- mkdocs.yml | 1 + notebooks/examples/exponential_sampling.md | 65 ----------------- notebooks/examples/gaussian_sampling.md | 60 ---------------- notebooks/examples/overview.md | 19 ----- notebooks/examples/poisson_sampling.md | 60 ---------------- quantflow/utils/distributions.py | 67 +++++++++--------- 14 files changed, 119 insertions(+), 305 deletions(-) create mode 100644 app/double_exponential_sampling.py delete mode 100755 dev/nbconvert delete mode 100755 dev/nbsync delete mode 100644 dev/notebook_config.json delete mode 100755 dev/start-jupyter delete mode 100644 notebooks/examples/exponential_sampling.md delete mode 100644 notebooks/examples/gaussian_sampling.md delete mode 100644 notebooks/examples/overview.md delete mode 100644 notebooks/examples/poisson_sampling.md diff --git a/Makefile b/Makefile index ead7175c..de3bad8d 100644 --- a/Makefile +++ b/Makefile @@ -24,29 +24,6 @@ install-dev: ## Install development dependencies marimo: ## Run marimo for editing notebooks @./dev/marimo edit -.PHONY: notebook -notebook: ## Run Jupyter notebook server - @poetry run ./dev/start-jupyter 9095 - - -.PHONY: book -book: ## Build static jupyter {book} - poetry run jupyter-book build notebooks --all - @cp notebooks/CNAME notebooks/_build/html/CNAME - - -.PHONY: nbconvert -nbconvert: ## Convert notebooks to myst markdown - poetry run ./dev/nbconvert - -.PHONY: nbsync -nbsync: ## Sync python myst notebooks to .ipynb files - needed for vs notebook development - poetry run ./dev/nbsync - -.PHONY: sphinx-config -sphinx-config: ## Build sphinx config - poetry run jupyter-book config sphinx notebooks - .PHONY: docs docs: ## build documentation @cp docs/index.md readme.md @@ -56,21 +33,10 @@ docs: ## build documentation docs-serve: ## serve documentation @poetry run mkdocs serve --livereload --watch quantflow --watch docs -.PHONY: sphinx -sphinx: ## Build sphinx docs - poetry run sphinx-build notebooks path/to/book/_build/html -b html - - .PHONY: publish publish: ## Release to pypi @poetry publish --build -u __token__ -p $(PYPI_TOKEN) - -.PHONY: publish-book -publish-book: ## publish the book to github pages - poetry run ghp-import -n -p -f notebooks/_build/html - - .PHONY: tests tests: ## Unit tests @./dev/test diff --git a/app/double_exponential_sampling.py b/app/double_exponential_sampling.py new file mode 100644 index 00000000..4c95adfd --- /dev/null +++ b/app/double_exponential_sampling.py @@ -0,0 +1,81 @@ +import marimo + +__generated_with = "0.19.7" +app = marimo.App(width="medium") + + +@app.cell +def _(): + import marimo as mo + from app.utils import nav_menu + nav_menu() + return (mo,) + + +@app.cell +def _(mo): + mo.md(r""" + # Double Exponential Sampling + + Here we sample the Asymmetric Laplace distribution, a.k.a double exponential + We will set the mean to 0 and the variance to 1 so that the distribution is fully determined by the asymmetric parameter $\kappa$. + + ```python + from quantflow.utils.distributions import DoubleExponential + ``` + """) + return + + +@app.cell +def _(): + from quantflow.utils.distributions import DoubleExponential + from quantflow.utils import bins + import numpy as np + + def simulate_double_exponential(log_kappa: float, samples: int): + pr = DoubleExponential.from_moments(kappa=np.exp(log_kappa)) + data = pr.sample(samples) + pdf = bins.pdf(data, num_bins=50, symmetric=0) + pdf["simulation"] = pdf["pdf"] + pdf["analytical"] = pr.pdf(pdf.index) + cha = pr.pdf_from_characteristic() + return pdf, cha + return (simulate_double_exponential,) + + +@app.cell +def _(mo): + samples = mo.ui.slider(start=100, stop=10000, step=100, value=1000, debounce=True, label="Samples") + log_kappa = mo.ui.slider(start=-2, stop=2, step=0.1, value=0.1, debounce=True, label="Asymmetry - $\log \kappa$") + + controls = mo.hstack([samples, log_kappa], justify="start") + controls + return log_kappa, samples + + +@app.cell +def _(log_kappa, samples, simulate_double_exponential): + df, cha = simulate_double_exponential(log_kappa.value, samples.value) + return cha, df + + +@app.cell +def _(cha, df): + import plotly.graph_objects as go + + simulation = go.Bar(x=df.index, y=df["simulation"], name="simulation") + analytical = go.Scatter(x=df.index, y=df["analytical"], name="analytical") + characteristic = go.Scatter(x=cha.x, y=cha.y, name="from characteristic", mode="markers") + fig = go.Figure(data=[simulation, characteristic, analytical]) + fig + return + + +@app.cell +def _(): + return + + +if __name__ == "__main__": + app.run() diff --git a/app/gaussian_sampling.py b/app/gaussian_sampling.py index e3d7d696..8cbf68fc 100644 --- a/app/gaussian_sampling.py +++ b/app/gaussian_sampling.py @@ -8,7 +8,6 @@ def _(): import marimo as mo from app.utils import nav_menu - nav_menu() return (mo,) diff --git a/dev/marimo b/dev/marimo index 70c3fb0d..bfe63fb9 100755 --- a/dev/marimo +++ b/dev/marimo @@ -2,5 +2,8 @@ set -e export PYTHONPATH=${PWD}:${PYTHONPATH} +ENV_FILE="${PWD}/.env" +touch ${ENV_FILE} +export $(grep -v '^#' ${ENV_FILE} | xargs) poetry run marimo "$@" diff --git a/dev/nbconvert b/dev/nbconvert deleted file mode 100755 index 76b4453a..00000000 --- a/dev/nbconvert +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -set -e - -for file in notebooks/**/*.ipynb -do - jupytext "$file" -s -done diff --git a/dev/nbsync b/dev/nbsync deleted file mode 100755 index d929bf95..00000000 --- a/dev/nbsync +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -set -e - -for file in notebooks/**/*.md -do - jupytext "$file" -s -done diff --git a/dev/notebook_config.json b/dev/notebook_config.json deleted file mode 100644 index 86563fa0..00000000 --- a/dev/notebook_config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "ServerApp": { - "password": "sha1:f0ac62b893bc:1d3d98395d095914c994e1a84f767e47a6d3b4a4" - }, - "@jupyterlab/apputils-extension:themes": { - "theme": "JupyterLab Dark" - } -} diff --git a/dev/start-jupyter b/dev/start-jupyter deleted file mode 100755 index 00e293c7..00000000 --- a/dev/start-jupyter +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -set -e - -PORT=$1 - -export PYTHONPATH=${PWD}:${PYTHONPATH} -ENV_FILE="${PWD}/.env" -touch ${ENV_FILE} -export $(grep -v '^#' ${ENV_FILE} | xargs) - -jupyter-lab --port=${PORT} diff --git a/mkdocs.yml b/mkdocs.yml index 9d6c1408..772fa9bd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -57,6 +57,7 @@ nav: - Examples: - Gaussian Sampling: examples/gaussian-sampling - Poisson Sampling: examples/poisson-sampling + - Double Exponential Sampling: examples/double-exponential-sampling - Hurst: examples/hurst - Supersmoother: examples/supersmoother - API Reference: diff --git a/notebooks/examples/exponential_sampling.md b/notebooks/examples/exponential_sampling.md deleted file mode 100644 index 97108f97..00000000 --- a/notebooks/examples/exponential_sampling.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.16.6 -kernelspec: - display_name: .venv - language: python - name: python3 ---- - -# Exponential Sampling - -Here we sample the Asymmetric Laplace distribution. We will set the mean to 0 and the variance to 1 so that the distribution is fully determined by the asymmetric parameter $\kappa$. - -```{admonition} Interactive notebook not enabled in docs - how to run it interactively? -The widget below is not enabled in the documentation. You can run the notebook to see the widget in action, see [contributing](../reference/contributing.md) for instructions on how to run the notebook. -``` - -```{code-cell} ipython3 -from quantflow.utils.distributions import DoubleExponential -from quantflow.utils import bins -import numpy as np -import ipywidgets as widgets -import plotly.graph_objects as go - -def simulate(): - pr = DoubleExponential.from_moments(kappa=np.exp(asym.value)) - data = pr.sample(samples.value) - pdf = bins.pdf(data, num_bins=50, symmetric=0) - pdf["simulation"] = pdf["pdf"] - pdf["analytical"] = pr.pdf(pdf.index) - cha = pr.pdf_from_characteristic() - return pdf, cha - -def on_change(change): - df, cha = simulate() - fig.data[0].x = df.index - fig.data[0].y = df["simulation"] - fig.data[1].x = df.index - fig.data[1].y = df["analytical"] - fig.data[2].x = cha.x - fig.data[2].y = cha.y - -asym = widgets.FloatSlider(description="asymmetry (log of k)", min=-2, max=2) -samples = widgets.IntSlider(description="paths", min=100, max=10000, step=100) -asym.value = 0 -samples.value = 1000 -asym.observe(on_change) -samples.observe(on_change) - -df, cha = simulate() -simulation = go.Bar(x=df.index, y=df["simulation"], name="simulation") -analytical = go.Scatter(x=df.index, y=df["analytical"], name="analytical") -cha = go.Scatter(x=cha.x, y=cha.y, name="from characteristic", mode="markers") -fig = go.FigureWidget(data=[simulation, cha, analytical]) - -widgets.VBox([asym, samples, fig]) -``` - -```{code-cell} ipython3 - -``` diff --git a/notebooks/examples/gaussian_sampling.md b/notebooks/examples/gaussian_sampling.md deleted file mode 100644 index 2fe24683..00000000 --- a/notebooks/examples/gaussian_sampling.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.16.6 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -# Gaussian Sampling - -Here we sample the gaussian OU process for different mean reversion speed and number of paths. - -```{admonition} Interactive notebook not enabled in docs - how to run it interactively? -The widget below is not enabled in the documentation. You can run the notebook to see the widget in action, see [contributing](../reference/contributing.md) for instructions on how to run the notebook. -``` - -```{code-cell} ipython3 -from quantflow.sp.ou import Vasicek -from quantflow.utils import plot -import ipywidgets as widgets -import plotly.graph_objects as go - -def simulate(): - pr = Vasicek(rate=0.5, kappa=kappa.value) - paths = pr.sample(samples.value, 1, 1000) - pdf = paths.pdf(num_bins=50) - pdf["simulation"] = pdf["pdf"] - pdf["analytical"] = pr.marginal(1).pdf(pdf.index) - return pdf - -def on_intensity_change(change): - df = simulate() - fig.data[0].x = df.index - fig.data[0].y = df["simulation"] - fig.data[1].x = df.index - fig.data[1].y = df["analytical"] - -kappa = widgets.FloatSlider(description="mean reversion", min=0.1, max=5) -samples = widgets.IntSlider(description="paths", min=100, max=10000, step=100) -kappa.value = 1 -samples.value = 1000 -kappa.observe(on_intensity_change) -samples.observe(on_intensity_change) - -df = simulate() -simulation = go.Bar(x=df.index, y=df["simulation"], name="simulation") -analytical = go.Scatter(x=df.index, y=df["analytical"], name="analytical") -fig = go.FigureWidget(data=[simulation, analytical]) - -widgets.VBox([kappa, samples, fig]) -``` - -```{code-cell} ipython3 - -``` diff --git a/notebooks/examples/overview.md b/notebooks/examples/overview.md deleted file mode 100644 index 738e0c17..00000000 --- a/notebooks/examples/overview.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.16.6 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -# Examples - -This section is a collection of random examples - -```{tableofcontents} -``` diff --git a/notebooks/examples/poisson_sampling.md b/notebooks/examples/poisson_sampling.md deleted file mode 100644 index 3b83e64b..00000000 --- a/notebooks/examples/poisson_sampling.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -jupytext: - text_representation: - extension: .md - format_name: myst - format_version: 0.13 - jupytext_version: 1.16.6 -kernelspec: - display_name: Python 3 (ipykernel) - language: python - name: python3 ---- - -# Poisson Sampling - -Evaluate the MC simulation for The Poisson process against the analytical PDF. - -```{admonition} Interactive notebook not enabled in docs - how to run it interactively? -The widget below is not enabled in the documentation. You can run the notebook to see the widget in action, see [contributing](../reference/contributing.md) for instructions on how to run the notebook. -``` - -```{code-cell} -from quantflow.sp.poisson import PoissonProcess -from quantflow.utils import plot -import ipywidgets as widgets -import plotly.graph_objects as go - -def simulate(): - pr = PoissonProcess(intensity=intensity.value) - paths = pr.sample(samples.value, 1, 1000) - pdf = paths.pdf(delta=1) - pdf["simulation"] = pdf["pdf"] - pdf["analytical"] = pr.marginal(1).pdf(pdf.index) - return pdf - -def on_intensity_change(change): - df = simulate() - fig.data[0].x = df.index - fig.data[0].y = df["simulation"] - fig.data[1].x = df.index - fig.data[1].y = df["analytical"] - -intensity = widgets.IntSlider(description="intensity") -samples = widgets.IntSlider(description="paths", min=100, max=10000, step=100) -intensity.value = 50 -samples.value = 1000 -intensity.observe(on_intensity_change) -samples.observe(on_intensity_change) - -df = simulate() -simulation = go.Bar(x=df.index, y=df["simulation"], name="simulation") -analytical = go.Scatter(x=df.index, y=df["analytical"], name="analytical") -fig = go.FigureWidget(data=[simulation, analytical]) - -widgets.VBox([intensity, samples, fig]) -``` - -```{code-cell} - -``` diff --git a/quantflow/utils/distributions.py b/quantflow/utils/distributions.py index 6f51364d..b0b9ab34 100644 --- a/quantflow/utils/distributions.py +++ b/quantflow/utils/distributions.py @@ -4,6 +4,7 @@ import numpy as np from pydantic import Field from scipy import stats +from typing_extensions import Annotated, Doc from .marginal import Marginal1D from .types import FloatArray, FloatArrayLike, Vector @@ -96,15 +97,12 @@ def cdf(self, x: FloatArrayLike) -> FloatArrayLike: class Normal(Distribution1D): - r"""A :class:`.Distribution1D` for the `Normal distribution`_ - - The normal distribution is a continuous probability distribution with PDF + r"""The normal distribution is a continuous probability distribution with PDF given by - .. math:: + \begin{equation} f(x) = \frac{e^{-\frac{\left(x - \mu\right)^2}{2\sigma^2}}}{\sqrt{2\pi\sigma^2}} - - .. _Normal distribution: https://en.wikipedia.org/wiki/Normal_distribution + \end{equation} """ mu: float = Field(default=0, description="mean") @@ -146,34 +144,40 @@ def set_variance(self, variance: float) -> None: class DoubleExponential(Exponential): - r"""A :class:`.Marginal1D` for the generalized double exponential distribution + r"""The generalized double exponential distribution - This is also know as the Asymmetric Laplace distribution (`ALD`_) which is + This is also know as the Asymmetric Laplace distribution which is a continuous probability distribution with PDF - .. math:: - \begin{align} - f(x) &= \frac{\lambda}{\kappa + \frac{1}{\kappa}} - e^{-\left(x - m\right) \lambda s(x) \kappa^{s(x)}}\\ - s(x) &= {\tt sgn}\left({x - m}\right) - \end{align} + \begin{align} + f(x) &= \frac{\lambda}{\kappa + \frac{1}{\kappa}} + e^{-\left(x - m\right) \lambda s(x) \kappa^{s(x)}}\\ + s(x) &= {\tt sgn}\left({x - m}\right) + \end{align} - where `m` is the :attr:`.loc` parameter, :math:`\lambda` is the :attr:`.decay` - parameter, and :math:`\kappa` is the asymmetric :attr:`.kappa` parameter. + where $m$ is the `loc` parameter, + $\lambda$ is the `decay` parameter, + and $\kappa$ is the asymmetric parameter. The Asymmetric Laplace distribution is similar to the Gaussian/normal distribution, but is sharper at the peak, it has fatter tails and allow for skewness. It represents the difference between two independent, exponential random variables. - - .. _ALD: https://en.wikipedia.org/wiki/Asymmetric_Laplace_distribution """ - loc: float = Field(default=0, description="location parameter") - """The location parameter `m`""" - decay: float = Field(default=1, gt=0, description="exponential decay rate") - r"""The exponential decay rate :math:`\lambda`""" - kappa: float = Field(default=1, gt=0, description="asymmetric parameter") - """Asymmetric parameter - when k=1, the distribution is symmetric""" + loc: float = Field( + default=0, description=r"location parameter $m$ of the distribution" + ) + decay: float = Field( + default=1, gt=0, description=r"exponential decay rate $\lambda$" + ) + kappa: float = Field( + default=1, + gt=0, + description=( + r"asymmetric parameter $\kappa$ - when $\kappa=1$," + " the distribution is symmetric" + ), + ) @property def log_kappa(self) -> float: @@ -188,17 +192,14 @@ def from_variance_and_asymmetry(cls, variance: float, asymmetry: float) -> Self: def from_moments( cls, *, - mean: float = 0, - variance: float = 1, - kappa: float = 1, + mean: Annotated[float, Doc("The mean of the distribution")] = 0, + variance: Annotated[float, Doc("The variance of the distribution")] = 1, + kappa: Annotated[ + float, Doc("The asymmetry parameter of the distribution, 1 for symmetric") + ] = 1, ) -> Self: r"""Create a double exponential distribution from the mean, variance - and asymmetry - - :param mean: The mean of the distribution - :param variance: The variance of the distribution - :param kappa: The asymmetry parameter of the distribution, 1 for symmetric - """ + and asymmetry""" k2 = kappa * kappa decay = np.sqrt((1 + k2 * k2) / (variance * k2)) return cls(decay=decay, kappa=kappa, loc=mean - (1 - k2) / (kappa * decay))