Source code for ctg.brownian_motion
"""Brownian motion helper utilities.
This module provides a compact implementation of the Levy–Ciesielski
expansion used to sample Brownian paths from a finite set of parameters.
The functions are intentionally small and deterministic which makes them
useful for tests and reproducibility.
"""
import numpy as np
from math import ceil, log, sqrt
[docs]
def param_LC_W(yy, tt, T):
"""Compute Levy–Ciesielski expansion samples of Brownian motion.
Args:
yy: Parameter vector or array used to construct sample paths.
tt: 1D array of time points in [0, T] where the paths are evaluated.
T: Final time for the expansion (used for rescaling).
Returns:
2D numpy array where each row is a sampled Brownian path evaluated
at the times in ``tt``.
"""
# Make yy 2D array (Ny, dimy)
yy = np.atleast_2d(yy)
assert len(yy.shape) == 2, "param_LC_Brownian_motion: yy must be 2D"
# Make tt 1D (Nt, )
tt = np.atleast_1d(tt)
tt = tt.flatten()
assert len(tt.shape) == 1
tol = 1e-12
assert (
np.amin(tt) >= -tol and np.amax(tt) <= T + tol
), "param_LC_Brownian_motion: tt not within [0,T] (with tolerance)"
# Get # LC-levels
L = ceil(log(yy.shape[1], 2)) # number of levels
# Extend yy to the next power of 2 with 0
fill = np.zeros((yy.shape[0], 2**L - yy.shape[1]))
yy = np.column_stack((yy, fill))
BB = LC_matrix(L, tt, T)
W = np.matmul(yy, BB)
return W
[docs]
def LC_matrix(L: int, tt: np.ndarray, T: float) -> np.ndarray:
"""
Generate matrix for sampling Levy-Ciesielski expansion of level L on [0, T] on time nodes tt.
Args:
L (int): Number of levels in the Levy-Ciesieslky expansion. We
tt (np.ndarray): 1D array of time points at which to evaluate the basis functions.
T (float): Final time, used to rescale the time points.
Returns:
np.ndarray: A (dim_y, len(tt)) matrix where each row is a basis function evaluated at the given time points.
"""
n_t = tt.size
# Rescale tt (to be reverted!)
tt = tt / T
# Compute basis B
BB = np.zeros((2**L, n_t))
BB[0, :] = tt # first basis function is the linear one
for lev in range(1, L + 1):
n_j = 2 ** (lev - 1) # number of basis functions at level l
for j in range(1, n_j + 1):
basis_fun = 0 * tt # basis is 0 where not assegned below
# define increasing part basis function
ran1 = np.where((tt >= (2 * j - 2) / (2**lev)) & (tt <= (2 * j - 1) / (2**lev)))
basis_fun[ran1] = tt[ran1] - (2 * j - 2) / 2**lev
# define decreasing part basis function
ran2 = np.where((tt >= (2 * j - 1) / (2**lev)) & (tt <= (2 * j) / (2**lev)))
basis_fun[ran2] = -tt[ran2] + (2 * j) / 2**lev
n_b = 2 ** (lev - 1) + j - 1 # prev. lev.s (complete) + curr. lev (partial)
BB[n_b, :] = 2 ** ((lev - 1) / 2) * basis_fun
# Revert time rescaling
BB *= sqrt(T)
return BB