release(v1.1.0): additive CPU-only generic numerical primitives

Adds 9 new top-level / sub-modules to the Rust API only (no Python
bindings yet), with at least one analytic unit test per module.

New Rust modules:
- optimal_control::matrix_riccati  (RK4 backward solver)
- timeseries_utils::nonsync_covariance  (Hayashi-Yoshida)
- timeseries_utils::wavelet  (Haar / Daubechies DWT and MODWT)
- risk_measures  (VaR, CVaR, projected sub-gradient CVaR minimisation)
- graph::laplacian + graph::spectral_clustering  (Jacobi + k-means++)
- topology  (Vietoris-Rips persistent homology, bottleneck distance)
- volterra  (Caputo Adams, Markovian lift, second-kind Volterra,
             Fourier inversion of characteristic functions)
- signatures  (truncated tensor signature, log-sig, random reservoir,
               Salvi-Cass-Lyons signature kernel, shuffle product)

All previously stable APIs untouched; abi3-py38 ABI preserved.
New module tests: 29/29 passing. Pre-existing 5 unrelated failures
unchanged.
This commit is contained in:
ThotDjehuty
2026-05-12 10:59:09 +02:00
parent e67b0f8376
commit d780ed81d7
39 changed files with 4044 additions and 3 deletions
+52
View File
@@ -0,0 +1,52 @@
Graph Laplacians and Spectral Clustering
========================================
The module :code:`graph` provides graph Laplacian operators and a
spectral clustering algorithm built on a Jacobi diagonaliser.
Laplacians
----------
For a weighted undirected graph with adjacency matrix :math:`W \in \mathbb{R}^{n\times n}_{\ge 0}`
and degree matrix :math:`D = \mathrm{diag}(W \mathbf{1})`:
- **Combinatorial**: :math:`L = D - W`.
- **Symmetric normalised**: :math:`L_{\mathrm{sym}} = I - D^{-1/2} W D^{-1/2}`.
- **Random-walk normalised**: :math:`L_{\mathrm{rw}} = I - D^{-1} W`.
Each operator is positive semidefinite and the multiplicity of the
zero eigenvalue equals the number of connected components.
Spectral Clustering
-------------------
Given :math:`W` and a target number of clusters :math:`k`:
1. Build :math:`L_{\mathrm{sym}}` (or another Laplacian).
2. Diagonalise via cyclic Jacobi rotations to obtain the eigenpairs
:math:`(\lambda_i, u_i)`.
3. Stack the :math:`k` eigenvectors associated with the smallest
eigenvalues as columns of :math:`U \in \mathbb{R}^{n \times k}`.
4. Normalise rows of :math:`U` and run Lloyd's algorithm with
k-means++ initialisation on the rows.
The Fiedler eigenvalue :math:`\lambda_2` is reported separately as a
proxy for the spectral gap.
API
---
.. code-block:: rust
pub enum LaplacianKind { Combinatorial, SymmetricNormalised, RandomWalk }
pub fn combinatorial_laplacian(w: ArrayView2<f64>) -> Result<Array2<f64>>;
pub fn normalised_laplacian(w: ArrayView2<f64>) -> Result<Array2<f64>>;
pub fn random_walk_laplacian(w: ArrayView2<f64>) -> Result<Array2<f64>>;
pub struct SpectralClusterResult {
pub labels: Vec<usize>,
pub eigenvalues: Vec<f64>,
pub fiedler_value: f64,
}
pub fn spectral_cluster(w: ArrayView2<f64>, k: usize, n_kmeans_iter: usize, seed: u64)
-> Result<SpectralClusterResult>;
+60
View File
@@ -0,0 +1,60 @@
Matrix Riccati Solver
=====================
The module :code:`optimal_control::matrix_riccati` integrates backward in time
the matrix Riccati differential equation
.. math::
\frac{dA(t)}{dt} = -2\,A(t)\,M\,A(t) + Q,
\qquad A(T) = A_T,
together with the affine and constant components
.. math::
\frac{dB(t)}{dt} = -2\,A(t)\,M\,B(t),
\qquad B(T) = B_T,
.. math::
\frac{dC(t)}{dt} = -B(t)^\top\,M\,B(t),
\qquad C(T) = C_T.
Discretisation
--------------
The grid :math:`\{t_n = T - n\,\Delta t\}_{n=0}^{N}` with
:math:`\Delta t = T / N` is traversed backward and a classical RK4 step is
applied to the joint vector field :math:`(A, B, C)`. Each macro step is
optionally subdivided into :math:`s` sub-steps for stability on stiff
problems.
Validation
----------
In the scalar case :math:`A, M, Q \in \mathbb{R}` with :math:`A(T) = 0`,
.. math::
A(t) \;=\; -\sqrt{\frac{Q}{2M}}\;\tanh\!\Big(\sqrt{2QM}\,(T - t)\Big),
a closed form used by the unit test :code:`scalar_riccati_matches_analytic`
to certify :math:`L^\infty` convergence below :math:`10^{-5}` on
:math:`[0, T]`.
API
---
.. code-block:: rust
pub fn solve_matrix_riccati(
m_matrix: ArrayView2<f64>,
q: ArrayView2<f64>,
n: ArrayView2<f64>,
a_terminal: ArrayView2<f64>,
b_terminal: ArrayView1<f64>,
c_terminal: f64,
t_horizon: f64,
config: RiccatiConfig,
) -> Result<RiccatiResult>;
@@ -0,0 +1,48 @@
Asynchronous Covariance (Hayashi--Yoshida)
==========================================
The module :code:`timeseries_utils::nonsync_covariance` implements the
Hayashi--Yoshida estimator of the integrated covariance between two
asynchronously sampled processes :math:`X` and :math:`Y` observed at
distinct, non-overlapping observation grids
:math:`\{t^X_i\}` and :math:`\{t^Y_j\}`.
Estimator
---------
Let :math:`I_i = (t^X_{i-1}, t^X_i]` and :math:`J_j = (t^Y_{j-1}, t^Y_j]`.
The Hayashi--Yoshida estimator is
.. math::
\widehat{\langle X, Y\rangle}_{[0,T]}
\;=\;
\sum_{i, j}\,
\big(X_{t^X_i} - X_{t^X_{i-1}}\big)\,
\big(Y_{t^Y_j} - Y_{t^Y_{j-1}}\big)\,
\mathbf{1}\!\big[I_i \cap J_j \neq \emptyset\big].
It is consistent under non-synchronicity and avoids the *Epps effect*
that plagues naive grid interpolation.
Implementation
--------------
* A two-pointer scan in :math:`O(n_X + n_Y)` collects all overlapping
pairs.
* For matrices of size :math:`d \times d` with large per-asset sample
counts, off-diagonal entries are computed in parallel with Rayon.
API
---
.. code-block:: rust
pub fn hayashi_yoshida_covariance(
t1: &[f64], v1: &[f64],
t2: &[f64], v2: &[f64],
) -> Result<f64>;
pub fn hayashi_yoshida_matrix(
series: &[(Vec<f64>, Vec<f64>)],
) -> Result<Vec<Vec<f64>>>;
+73
View File
@@ -0,0 +1,73 @@
Risk Measures: VaR and CVaR
============================
The module :code:`risk_measures` provides Value-at-Risk and Conditional
Value-at-Risk estimators together with a convex CVaR minimisation
solver over the unit simplex.
Definitions
-----------
For a real random variable :math:`L` (a *loss*), the Value-at-Risk at
confidence level :math:`\alpha \in (0, 1)` is the lower :math:`\alpha`-
quantile
.. math::
\mathrm{VaR}_\alpha(L)
\;=\;
\inf\!\big\{ \ell \in \mathbb{R} : \mathbb{P}(L \le \ell) \ge \alpha \big\}.
The Conditional Value-at-Risk (also called Average Value-at-Risk) is
.. math::
\mathrm{CVaR}_\alpha(L)
\;=\;
\frac{1}{1-\alpha}\,
\int_\alpha^1 \mathrm{VaR}_u(L)\,du.
For a sample :math:`L_1, \dots, L_n` of i.i.d. losses sorted in increasing
order, the empirical CVaR at level :math:`\alpha` is
.. math::
\widehat{\mathrm{CVaR}}_\alpha
\;=\;
\frac{1}{n - k}\, \sum_{i = k+1}^{n} L_{(i)},
\qquad k = \lfloor \alpha\, n \rfloor.
Convex minimisation
-------------------
Rockafellar--Uryasev (2000) showed that
.. math::
\mathrm{CVaR}_\alpha(L)
\;=\;
\min_{\zeta \in \mathbb{R}}\;
\zeta + \frac{1}{1 - \alpha}\,\mathbb{E}\!\big[(L - \zeta)_+\big].
Given samples of a vector :math:`r^{(s)} \in \mathbb{R}^d`,
:code:`minimize_cvar` solves
.. math::
\min_{w \in \Delta_d,\;\zeta \in \mathbb{R}}\;
\zeta + \frac{1}{(1 - \alpha)\, S}\, \sum_{s=1}^S
\big(\zeta - \langle r^{(s)}, w\rangle\big)_+,
over the unit simplex :math:`\Delta_d`, by a projected sub-gradient
method using the Held--Wolfe--Crowder simplex projection.
API
---
.. code-block:: rust
pub fn historical_var(losses: &[f64], alpha: f64) -> Result<f64>;
pub fn parametric_var(mu: f64, sigma: f64, alpha: f64) -> Result<f64>;
pub fn cvar_value(losses: &[f64], alpha: f64) -> Result<f64>;
pub fn minimize_cvar(returns: ArrayView2<f64>, cfg: &CVaRConfig)
-> Result<CVaRResult>;
+119
View File
@@ -0,0 +1,119 @@
Path Signatures
===============
The module :code:`signatures` provides truncated tensor signatures
(Lyons 1998), log-signatures, random reservoir projections, and the
Salvi--Cass--Lyons signature kernel.
Truncated Signature
-------------------
For a continuous path :math:`X : [0, T] \to \mathbb{R}^d` of bounded
variation, the *signature* is the formal series
.. math::
S(X)_{0,T}
\;=\;
1 + \sum_{k \ge 1} \sum_{i_1, \dots, i_k}
S^{i_1, \dots, i_k}_{0, T}\,
e_{i_1} \otimes \dots \otimes e_{i_k},
with iterated Stieltjes integrals
.. math::
S^{i_1, \dots, i_k}_{0, T}
\;=\;
\int_{0 < u_1 < \dots < u_k < T}
dX^{i_1}_{u_1}\, \dots\, dX^{i_k}_{u_k}.
For piecewise-linear input with increments :math:`\Delta_n`, the
truncated signature obeys the multiplicative recursion
.. math::
S^{(M)}_{0, t_n}
\;=\;
S^{(M)}_{0, t_{n-1}}\,\otimes_M\,\exp_M(\Delta_n),
where :math:`\exp_M(\Delta) = \sum_{k=0}^M \Delta^{\otimes k} / k!`.
Log-Signature
-------------
The truncated tensor logarithm
.. math::
\log(S)
\;=\;
\sum_{n \ge 1} \frac{(-1)^{n+1}}{n}\,(S - 1)^{\otimes n}
lives in the truncated free Lie algebra and provides a more
parsimonious representation.
Random Signature
----------------
Following Cuchiero--Schmocker--Teichmann (2023), one drives a random
reservoir on :math:`\mathbb{R}^N`,
.. math::
dZ_t = A_0 Z_t\, dt + \sum_{i=1}^d A_i Z_t\, dX^i_t,
with random matrices :math:`A_i \in \mathbb{R}^{N \times N}` whose
entries are i.i.d. Gaussian with variance :math:`1/N`. The map
:math:`X \mapsto Z_T` is a finite-dimensional random projection of
:math:`S(X)`.
Signature Kernel (Salvi--Cass--Lyons)
-------------------------------------
The signature inner product
.. math::
K(s, t) \;=\; \langle S(X)_{0, s},\; S(Y)_{0, t}\rangle
solves the linear hyperbolic PDE
.. math::
\frac{\partial^2 K}{\partial s\,\partial t}
\;=\;
\langle \dot X_s, \dot Y_t \rangle\, K(s, t),
\qquad
K(s, 0) = K(0, t) = 1.
It is integrated on a uniform grid via the Goursat scheme
.. math::
K_{i+1, j+1}
= K_{i+1, j} + K_{i, j+1} - K_{i, j}
+ \langle \Delta x_i, \Delta y_j\rangle\,
\tfrac{1}{2}(K_{i+1, j} + K_{i, j+1}).
API
---
.. code-block:: rust
pub struct TruncatedSignature {
pub channels: usize,
pub level: usize,
pub tensors: Vec<Vec<f64>>,
}
pub fn path_signature(path: &[Vec<f64>], level: usize) -> Result<TruncatedSignature>;
pub fn log_signature(sig: &TruncatedSignature) -> Result<TruncatedLogSignature>;
pub struct RandomSignatureConfig {
pub reservoir_dim: usize, pub seed: u64, pub variance: f64,
}
pub fn random_signature(path: &[Vec<f64>], cfg: &RandomSignatureConfig)
-> Result<RandomSignatureResult>;
pub fn signature_kernel(x: &[Vec<f64>], y: &[Vec<f64>])
-> Result<SignatureKernelResult>;
+67
View File
@@ -0,0 +1,67 @@
Topological Data Analysis
=========================
The module :code:`topology` implements Vietoris--Rips persistent
homology and the bottleneck distance between persistence diagrams.
Vietoris--Rips Filtration
-------------------------
For a finite point cloud :math:`X = \{x_1, \dots, x_n\} \subset \mathbb{R}^d`
and scale :math:`\varepsilon \ge 0`, the Vietoris--Rips complex is
.. math::
\mathrm{VR}_\varepsilon(X)
\;=\;
\big\{ \sigma \subseteq X : \mathrm{diam}(\sigma) \le \varepsilon \big\}.
Increasing :math:`\varepsilon` yields a filtration; the persistent
homology of this filtration produces, for each homological degree
:math:`k`, a multiset of birth/death pairs
.. math::
D_k(X) = \big\{ (b_i, d_i) : 0 \le b_i < d_i \le \infty \big\}.
Persistence Algorithm
---------------------
The boundary matrix :math:`\partial` is built over :math:`\mathbb{Z}/2`
and reduced left-to-right: for each column :math:`j` we cancel its
lowest entry by adding any earlier column with the same low. Pairs
:math:`(\mathrm{low}(j), j)` give birth/death pairs.
Bottleneck Distance
-------------------
For two diagrams :math:`D` and :math:`D'`,
.. math::
d_B(D, D')
\;=\;
\inf_{\eta : D \to D'}\;
\sup_{x \in D}\, \|x - \eta(x)\|_\infty,
where matchings may pair points with the diagonal
:math:`\Delta = \{(t, t) : t \ge 0\}` at cost :math:`(d - b)/2`.
The implementation binary-searches the threshold :math:`\varepsilon`
and certifies a perfect matching by Hopcroft--Karp on the bipartite
graph of admissible edges.
API
---
.. code-block:: rust
pub struct PersistencePair { pub dim: usize, pub birth: f64, pub death: f64 }
pub struct PersistenceDiagram { pub pairs: Vec<PersistencePair> }
pub fn vietoris_rips_filtration(points: &[Vec<f64>], max_dim: usize, max_eps: f64)
-> Result<Vec<Simplex>>;
pub fn persistent_homology(points: &[Vec<f64>], max_dim: usize, max_eps: f64)
-> Result<PersistenceDiagram>;
pub fn bottleneck_distance(d1: &[PersistencePair], d2: &[PersistencePair])
-> Result<f64>;
+122
View File
@@ -0,0 +1,122 @@
Volterra and Fractional Solvers
================================
The module :code:`volterra` collects four CPU-only generic numerical
primitives for Volterra integral equations and related transforms.
Fractional Caputo Adams Solver
------------------------------
For :math:`\alpha \in (0, 1)`, solve
.. math::
D^\alpha h(t) = F(t, h(t)),
\qquad h(0) = h_0,
with the Diethelm--Ford--Freed (2002) fractional Adams predictor--
corrector. Predictor:
.. math::
h^P_{n+1}
= h_0
+ \frac{\Delta t^\alpha}{\alpha\,\Gamma(\alpha)}
\sum_{k=0}^{n}
\big[(n+1-k)^\alpha - (n-k)^\alpha\big]\, F(t_k, h_k).
Corrector:
.. math::
h_{n+1}
= h_0
+ \frac{\Delta t^\alpha}{\Gamma(\alpha + 2)}
\Big[ F(t_{n+1}, h^P_{n+1}) + \sum_{k=0}^{n} a_{n+1, k}\, F(t_k, h_k) \Big],
with
.. math::
a_{n+1, 0} = n^{\alpha + 1} - (n - \alpha)\,(n+1)^\alpha,
a_{n+1, k} = (n - k + 2)^{\alpha + 1} + (n - k)^{\alpha + 1}
- 2\,(n - k + 1)^{\alpha + 1},
\qquad 1 \le k \le n.
Markovian Lift
--------------
A convolution kernel :math:`K(t)` admitting
.. math::
K(t) = \int_0^\infty e^{-\gamma t}\, \nu(d\gamma)
is approximated by
.. math::
K(t) \;\approx\; \sum_{j=1}^N c_j\, e^{-\gamma_j t},
\qquad c_j \ge 0,
with rates :math:`\gamma_j` on a geometric grid and weights fitted by
non-negative least squares.
Generic Volterra Equation
-------------------------
For
.. math::
y(t) = g(t) + \int_0^t K(t - s, y(s))\, ds,
the trapezoidal product-integration scheme reads
.. math::
y_n = g_n + \Delta t\,\Big[
\tfrac{1}{2} K(t_n, y_0)
+ \sum_{k=1}^{n-1} K(t_n - t_k, y_k)
+ \tfrac{1}{2} K(0, y_n) \Big],
solved implicitly by fixed-point iteration on :math:`y_n`.
Fourier Inversion
-----------------
Recover a density from a characteristic function :math:`\varphi(u)` via
.. math::
f(x) \;\approx\;
\frac{\Delta u}{\pi}\,
\sum_{k=0}^{N_u - 1}
w_k \big[\,\Re \varphi(u_k)\,\cos(u_k x)
+ \Im \varphi(u_k)\,\sin(u_k x)\,\big],
with trapezoidal weights :math:`w_k`.
API
---
.. code-block:: rust
pub fn solve_fractional_ode<F: Fn(f64, f64) -> f64>(
h0: f64, alpha: f64, t_horizon: f64, n_steps: usize, rhs: F,
) -> Result<FractionalOdeResult>;
pub fn geometric_grid_lift<K: Fn(f64) -> f64>(
kernel: K, t_samples: &[f64],
n_factors: usize, gamma_min: f64, gamma_max: f64, nnls_iter: usize,
) -> Result<MarkovianLift>;
pub fn solve_volterra<G, K>(
g: G, kernel: K, t_horizon: f64, n_steps: usize,
fixed_point_iter: usize, fixed_point_tol: f64,
) -> Result<VolterraResult>;
pub fn fourier_invert<P: Fn(f64) -> (f64, f64)>(
phi: P, x_grid: &[f64], u_max: f64, n_u: usize,
) -> Result<DensityResult>;
+51
View File
@@ -0,0 +1,51 @@
Discrete and Maximum-Overlap Wavelet Transforms
================================================
The module :code:`timeseries_utils::wavelet` provides Haar and Daubechies
wavelet transforms with periodic boundary handling.
Filter banks
------------
For an orthogonal scaling filter :math:`\{h_k\}_{k=0}^{L-1}` the quadrature
mirror filter (QMF) is
.. math::
g_k = (-1)^k\, h_{L - 1 - k},
so that :math:`\sum_k h_k = \sqrt{2}` and :math:`\sum_k g_k = 0`.
DWT (one level, periodic)
-------------------------
For an input vector :math:`x \in \mathbb{R}^N` with :math:`N` even,
.. math::
a_n = \sum_{k=0}^{L-1} h_k\, x_{(2n + k)\bmod N},
\qquad
d_n = \sum_{k=0}^{L-1} g_k\, x_{(2n + k)\bmod N},
\qquad n = 0, \dots, N/2 - 1.
Successive levels apply the same filter to the previous approximation
:math:`a^{(j)}`.
MODWT (Maximum Overlap)
-----------------------
The MODWT does not downsample: at level :math:`j`, the filter is dilated
by inserting :math:`2^{j-1} - 1` zeros between successive taps and applied
in a periodic convolution. The result is shift-invariant.
API
---
.. code-block:: rust
pub enum WaveletFamily { Haar, Daubechies(u8) }
pub fn scaling_filter(family: WaveletFamily) -> Result<Vec<f64>>;
pub fn qmf(h: &[f64]) -> Vec<f64>;
pub fn dwt_step(x: &[f64], h: &[f64], g: &[f64]) -> (Vec<f64>, Vec<f64>);
pub fn dwt(x: &[f64], family: WaveletFamily, levels: usize) -> Result<Vec<Vec<f64>>>;
pub fn modwt_step(x: &[f64], h: &[f64], g: &[f64], level: usize) -> (Vec<f64>, Vec<f64>);