Discover simplicial complexes — nodes, edges, and triangles — from multivariate time series.
Classical network inference recovers only pairwise edges. simplicial-sindy fits a sparse
polynomial dynamical model (SINDy) with an ADMM group-lasso under a second-order-cone simplicial
hierarchy constraint, so the recovered structure is a valid simplicial complex: every triangle's
three boundary edges are guaranteed present. This exposes genuine higher-order (triadic)
interactions, not just pairwise correlations.
pip install simplicial-sindyOptional extras:
pip install "simplicial-sindy[gpu]" # CuPy (CUDA 12) backend
pip install "simplicial-sindy[progress]" # tqdm progress barsimport numpy as np
from simplicial_sindy import SindyConfig, process_data_in_windows
X = np.random.randn(20, 20_000) # (n_nodes, n_timestamps)
cfg = SindyConfig(fs=200.0, win_len=3200, stride=1600, d_max=2,
tau2_q=0.95, tau3_q=0.95)
windows = process_data_in_windows(X, cfg) # one result per sliding window
w = windows[0]
w["edges"] # [(i, j), ...]
w["edge_features"]["vector"] # S2 interaction score per edge (same order)
w["triangles"] # [(i, j, k), ...]
w["triangle_features"]["vector"] # S3 score per triangle (same order)
w["window_start"], w["window_end"]Every window payload also carries node_features, incidence matrices
(edge_features["incidence"], triangle_features["incidence_edges"]), the chosen thresholds
(tau2, tau3), and counts (n_edges, n_triangles, n_closed_triangles).
SindyConfig groups every parameter the pipeline reads (validated on construction):
| Group | Fields |
|---|---|
| Windowing | fs, win_sg, order, win_len, stride, overlap |
| Library | d_max (2 → triangles) |
| Local samples | r_target_pc, k_min, k_max |
| ADMM solver | scale, simpl_rho, admm_rho, admm_overrelax, max_iters, row_norm_nnz_thr |
| Extraction | tau2_q, tau3_q (score quantiles) |
stride=None derives the stride from overlap. Use cfg.replace(win_len=4096) for variants and
cfg.effective_stride to see the resolved value. An argparse.Namespace with the same field names
also works — the pipeline only reads attributes.
Importing the package is side-effect free: it never prints, never mutates os.environ, and
never allocates on the GPU. CPU (NumPy) is the default.
import simplicial_sindy as ss
ss.set_backend("auto") # use CuPy if it works, else NumPy
ss.gpu_available() # -> bool
ss.configure_threads(1) # opt-in: pin BLAS/OpenMP threads (solver parallelises over windows)Environment overrides: SIMPLICIAL_SINDY_BACKEND=cpu|gpu|auto, SIMPLICIAL_SINDY_FORCE_CPU=1,
and PARALLEL_SINDY_N_JOBS for the joblib worker count.
The ADMM solve is iterative, so the number of BLAS threads changes floating-point reduction order
and can flip a handful of simplices that sit exactly on the tau2_q / tau3_q quantile boundary.
For bit-exact, run-to-run reproducible output, pin BLAS to one thread. This must happen before
NumPy loads its BLAS, so either set it in the shell:
OMP_NUM_THREADS=1 MKL_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 python your_script.pyor call it as the very first thing in your script, above import numpy:
from simplicial_sindy import configure_threads
configure_threads(1)
import numpy as np # BLAS picks up the setting hereSingle-threaded BLAS is also usually faster here, because the pipeline already parallelises across windows with joblib. Verified: with threads pinned, this package reproduces the reference results exactly (identical edges, triangles, and scores to 0 ULP).
A lighter graph-only (d_max=1) STLSQ pipeline and the shared Savitzky-Golay preprocessing:
from simplicial_sindy import preprocess_timeseries, PreprocessConfig, run_sindy_windows
X_proc, dXdt = preprocess_timeseries(X, PreprocessConfig(fs=200.0, win_sg=29, poly_order=3))
results = run_sindy_windows(X, window_size=3200, stride=1600) # -> [WindowResult, ...]