Metadata-Version: 2.4
Name: simplicial-sindy
Version: 0.1.0
Summary: Discover simplicial complexes (nodes, edges, triangles) from multivariate time series with sparse regression under a simplicial-hierarchy constraint.
Author-email: Shubhajit Roy <royshubhajit@iitgn.ac.in>
License: MIT
Project-URL: Homepage, https://github.com/UW-Madison-CBML/simplicial-sindy
Project-URL: Issues, https://github.com/UW-Madison-CBML/simplicial-sindy/issues
Keywords: sindy,simplicial complex,topology,system identification,higher-order interactions,time series,eeg
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Mathematics
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.22
Requires-Dist: scipy>=1.8
Requires-Dist: joblib>=1.2
Provides-Extra: gpu
Requires-Dist: cupy-cuda12x>=12.0; extra == "gpu"
Provides-Extra: progress
Requires-Dist: tqdm>=4.65; extra == "progress"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Dynamic: license-file

# simplicial-sindy

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.

## Install

```bash
pip install simplicial-sindy
```

Optional extras:

```bash
pip install "simplicial-sindy[gpu]"       # CuPy (CUDA 12) backend
pip install "simplicial-sindy[progress]"  # tqdm progress bars
```

## Quickstart

```python
import 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`).

## Configuration

`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.

## Backend and threading

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.

```python
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.

## Reproducibility

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:

```bash
OMP_NUM_THREADS=1 MKL_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 python your_script.py
```

or call it as the very first thing in your script, above `import numpy`:

```python
from simplicial_sindy import configure_threads
configure_threads(1)
import numpy as np           # BLAS picks up the setting here
```

Single-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).

## Also included

A lighter graph-only (`d_max=1`) STLSQ pipeline and the shared Savitzky-Golay preprocessing:

```python
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, ...]
```

## Citation

If you use this package in academic work, please cite the accompanying paper.

## License

MIT © Shubhajit Roy
