"""Curved-fold jump model: exact generator coefficients and analytical bounds. The Lévy density is c*|z|**(-1-alpha) on the entire punctured real line. No finite mark cutoff is used in these generator and probability calculations. """ from __future__ import annotations from dataclasses import dataclass, asdict import math import numpy as np from numpy.typing import ArrayLike, NDArray from scipy.special import gammaln, digamma, ive, kve from scipy.integrate import quad @dataclass(frozen=True) class Parameters: alpha: float = 1.2 c: float = 0.12 kappa: float = 0.25 delta0: float = 1.2 delta_modulation: float = 0.2 sigma_modulation: float = 0.15 amplitude_modulation: float = 0.1 tangent_diffusion: float = 0.4 tangent_reversion: float = 1.0 def __post_init__(self) -> None: if not all(math.isfinite(x) for x in asdict(self).values()): raise ValueError('All parameters must be finite.') if not 0 < self.alpha < 2 or self.c <= 0 or self.kappa <= 0: raise ValueError('Require 0 < alpha < 2 and positive c, kappa.') if self.delta0 < abs(self.delta_modulation): raise ValueError('Dimension coefficient must be nonnegative.') if abs(self.sigma_modulation) >= 1 or abs(self.amplitude_modulation) >= 1: raise ValueError('Noise amplitudes must be uniformly positive.') if self.tangent_diffusion < 0 or self.tangent_reversion < 0: raise ValueError('Nonnegative tangent coefficients are required.') @property def q(self) -> float: return self.alpha / 2 @property def d_min(self) -> float: return self.delta0 - abs(self.delta_modulation) @property def d_max(self) -> float: return self.delta0 + abs(self.delta_modulation) @property def s_min(self) -> float: return 1 - abs(self.sigma_modulation) @property def s_max(self) -> float: return 1 + abs(self.sigma_modulation) @property def a_min(self) -> float: return 1 - abs(self.amplitude_modulation) @property def a_max(self) -> float: return 1 + abs(self.amplitude_modulation) @property def c_min(self) -> float: return self.c * self.kappa**self.q * self.a_min**self.alpha @property def c_max(self) -> float: return self.c * self.kappa**self.q * self.a_max**self.alpha def coefficients(self, v: ArrayLike): t = np.tanh(np.asarray(v, dtype=float)) return (self.delta0 + self.delta_modulation*t, 1+self.sigma_modulation*t, 1+self.amplitude_modulation*t) def mellin_coefficient(p: float, q: float) -> float: """m_q(p), restricted to its positive range p > q-1.""" if not 0 < q < 1 or not p > q-1 or not math.isfinite(p): raise ValueError('Require 0 < q < 1 and p > q-1.') return float(np.exp(gammaln(1-q)+gammaln(1+p)-gammaln(1+p-q))/q) def mellin_quadrature(p: float, q: float) -> float: """Independent real integral, with endpoint subtraction at zero.""" if not 0 < q < 1 or not p > q-1: raise ValueError('Require 0 < q < 1 and p > q-1.') # Integrate after removing the linear term: cancellation at zero is avoided. def remainder(u: float) -> float: if u == 0: return 0.0 if u < 1e-5: r = (p*(p-1)/2*u*u - p*(p-1)*(p-2)/6*u**3 +p*(p-1)*(p-2)*(p-3)/24*u**4) else: r = math.expm1(p*math.log1p(-u)) + p*u return r*u**(-1-q) val = quad(remainder, 0, .5, epsabs=2e-11, epsrel=2e-11)[0] # Split upper endpoint so p<0 is handled as an integrable power. val += quad(remainder, .5, 1, epsabs=2e-10, epsrel=2e-10, limit=300)[0] return float(1/q+p/(1-q)-val) def power_drift_constant(p: float, par: Parameters) -> float: """A positive uniform a_p, or raise if the proposed power is inadmissible.""" if not par.q-1 < p < 1 or p == 0: raise ValueError('p must be nonzero and lie in (q-1,1).') if p > 0: result = par.s_min**2*p*(2-par.d_max-2*p) else: theta = -p result = par.s_min**2*theta*(par.d_min-2-2*theta) if result <= 0: raise ValueError('The requested exponent does not give a negative continuous drift.') return result def power_generator(h: ArrayLike, v: ArrayLike, p: float, par: Parameters, censored: bool = False) -> NDArray[np.float64]: h, v = np.broadcast_arrays(np.asarray(h, float), np.asarray(v, float)) if np.any(h <= 0) or np.any(~np.isfinite(h)): raise ValueError('h must be finite and positive.') d, s, a = par.coefficients(v) c_v = par.c*par.kappa**par.q*a**par.alpha m = mellin_coefficient(p, par.q) - (1/par.q if censored else 0) return s*s*p*(d+2*p-2)*h**(p-1) - c_v*m*h**(p-par.q) def jump_rate(h: ArrayLike, v: ArrayLike, par: Parameters) -> NDArray[np.float64]: h, v = np.broadcast_arrays(np.asarray(h, float), np.asarray(v, float)) if np.any(h <= 0): raise ValueError('Positive discriminants are required.') _, _, a = par.coefficients(v) return par.c*par.kappa**par.q*a**par.alpha/par.q*h**(-par.q) def optimized_jump_bound(h0: float, p: float, par: Parameters) -> tuple[float, float]: """Upper bound on eventual jump termination; returns (bound, optimal collar).""" if h0 <= 0 or not 0 < p < 1-par.q: raise ValueError('Require h0>0 and 0
float:
if not 0 < h0 < L or not 0 < p < 1-par.q:
raise ValueError('Require 0