>_STRYKE-POLARS
The full pandas + numpy + scipy-class surface in one cdylib. No core bloat. pandas DataFrame + Series + Index + IO + Categorical, numpy ndarray + ufuncs + linalg + random + fft + polynomial + masked arrays + datetime64, plus signal processing, statistical tests, image filters, ML metrics, graph algorithms, time-series analytics, sparse matrix, distance / clustering, JSON / bit / format helpers — all in a single cdylib, dlopen'd in-process by stryke via use Polars.
.stk packages-D warnings)#![deny])Install
# build the cdylib + install as a stryke package
git clone https://github.com/MenkeTechnologies/stryke-polars
cd stryke-polars
make install # cargo build --release && s pkg install -g .
# or from a published GitHub release
s pkg install -g github:MenkeTechnologies/stryke-polars
# verify
echo 'use Polars; say Polars::version()->{version}' | stryke
The cdylib (libstryke_polars.dylib on macOS, .so on Linux) lands under ~/.stryke/store/stryke-polars@<ver>/ (per make install). use Polars resolves the installed package, dlopen's it once per stryke session, and binds the FFI entry points lazily. Core stryke never links against polars/ndarray/openblas/rustfft.
Quick start
Every wrapper is namespaced under a per-family stryke package: use Polars::DataFrame binds Polars::DataFrame::*, use Polars::NdArray binds Polars::NdArray::*, and so on (one package per lib/*.stk module). DataFrame ops are stateless — each call takes the raw column hash (a reference, \%cols), not a previously-built handle.
use Polars
use Polars::DataFrame
# version blob: { version, polars, ndarray }
val $v = Polars::version()
p "stryke-polars $v->{version} (polars $v->{polars}, ndarray $v->{ndarray})"
# DataFrame from a column dict (key -> array)
val %cols = (
ticker => ["AAPL", "MSFT", "NVDA"],
px => [231.4, 422.1, 138.9],
)
# new() validates the shape and returns a { shape, columns } blob
val $meta = Polars::DataFrame::new(\%cols)
val ($nrows, $ncols) = @{ Polars::DataFrame::shape(\%cols) } # => (3, 2)
val @columns = @{ Polars::DataFrame::columns(\%cols) } # => (ticker, px)
val @types = @{ Polars::DataFrame::dtypes(\%cols) }
val $head = Polars::DataFrame::head(\%cols, 2)
val $proj = Polars::DataFrame::select(\%cols, ["ticker"])
numpy-style ndarray math through the NdArray package:
use Polars::NdArray val $a = Polars::NdArray::arange(0, 12, 1) val $b = Polars::NdArray::reshape($a, [3, 4]) val $s = Polars::NdArray::sum($a) # => 66 val $m = Polars::NdArray::mean($a) # => 5.5
group-by aggregations (from examples/groupby.stk):
use Polars::DataFrame
val %sales = (
region => ["east", "east", "west", "west", "north", "north"],
revenue => [100, 150, 90, 130, 60, 85],
units => [10, 12, 9, 11, 6, 8],
)
val $by_region = Polars::DataFrame::groupby_sum(\%sales, ["region"])
val $by_region_m = Polars::DataFrame::groupby_mean(\%sales, ["region"])
val $by_region_c = Polars::DataFrame::groupby_count(\%sales, ["region"])
Two self-contained, runnable tours ship in examples/: discover.stk (shape / head / select / columns / dtypes / to_json) and groupby.stk (multi-key aggregations). Run either with s examples/<name>.stk. The full per-family surface is exercised by the suites in t/ (s test t/).
Surface — pandas + numpy core
Counts below are the live cdylib export totals per family prefix (grep -c 'pub extern "C" fn polars__<prefix>_' src/*.rs). The cdylib prefix column is the flat FFI symbol prefix; the stryke-side surface is the namespaced Polars::<Family>::<verb> package form (see the Naming Convention).
| Family | Stryke package | cdylib prefix | Shipped |
|---|---|---|---|
pandas Series (+ .str.* / .dt.*) | Polars::Series | polars__sr_* | 206 |
| numpy ndarray | Polars::NdArray / NdArrayExt | polars__arr_* | 177 |
| pandas DataFrame | Polars::DataFrame / DataFrameExt | polars__df_* | 119 |
| numpy ufuncs | Polars::Ufunc / UfuncExt | polars__np_* | 94 |
| pandas Index / MultiIndex / DatetimeIndex | Polars::Index | polars__idx_* | 69 |
| numpy masked arrays | Polars::Masked | polars__ma_* | 61 |
| numpy random distributions | Polars::Random / RandomExt | polars__rand_* | 40 |
| numpy linalg | Polars::Linalg / LinalgExt | polars__linalg_* | 37 |
| pandas Categorical | Polars::Categorical | polars__cat_* | 35 |
| numpy datetime64 / timedelta64 | Polars::DateTime64 | polars__dt64_* | 32 |
| pandas IO (read/write CSV, JSON, JSONL, TSV, HTML, Markdown) | Polars::IO | polars__pd_* | 29 |
| numpy polynomial (Chebyshev, Hermite, Legendre, Laguerre…) | Polars::Polynomial / PolynomialExt | polars__poly_* | 17 |
pandas DataFrame .str.* accessor | Polars::Text | polars__str_* | 16 |
| numpy fft | Polars::FFT / FFTExt | polars__fft_* | 13 |
pandas DataFrame .dt.* accessor | Polars::DateTime64 | polars__dt_* | 13 |
| dtype parsing / canonicalization | Polars::DataFrame | polars__parse_* | 1 |
| Pandas + numpy subtotal | 959 |
Surface — scipy-class extensions
Beyond pandas + numpy parity, the package ships a scipy-class extension layer. Every entry is a real Rust implementation backing the listed .stk package; counts are the live per-prefix export totals.
| Family | Stryke package | cdylib prefix | Shipped |
|---|---|---|---|
| Image filters (blur, Sobel/Prewitt/Laplacian, morphology, rotate, resize) | Polars::Image | polars__img_* | 42 |
| Distances + distribution PDFs/CDFs (Euclidean, Manhattan, Minkowski, Cosine, Jaccard, Mahalanobis, …) | Polars::Dist | polars__dist_* | 35 |
| Signal processing (convolve, correlate, smoothing, peak finding, MACD, RSI, Bollinger) | Polars::Signal | polars__sig_* | 35 |
| Bit manipulation (popcount, rotate, MSB/LSB, base conversion, …) | Polars::Bit | polars__bit_* | 31 |
| Text / NLP (tokenize, ngrams, Levenshtein, LCS, camel↔snake) | Polars::Text | polars__text_* | 27 |
| Statistical tests (t-test, chi², Mann-Whitney, KS, ANOVA, Levene, ACF/PACF, linregress) | Polars::Stattest | polars__stattest_* | 24 |
| Statistics (zscore, IQR, skew/kurt, pearson/spearman, MAE/MSE/R², KL/JS divergence) | Polars::Stat | polars__stat_* | 24 |
| Misc math (factorial, choose, Fibonacci, primes, GCD/LCM, modpow, …) | Polars::Misc | polars__misc_* | 23 |
| ML metrics (accuracy, precision/recall/F1, ROC AUC, MCC, Cohen kappa, …) | Polars::Metric | polars__metric_* | 23 |
| Groupby aggregations | Polars::GroupBy | polars__gb_* | 21 |
| Hashing (djb2/djb2a, FNV-1/1a 64+32, Jenkins, CRC32, MurmurHash3-32, BKDR, RS, JS, AP, DEK, PJW, ELF, SAX, Fletcher-16) | Polars::Hash | polars__hash_* | 18 |
| Formatting (round, percent, currency, scientific, commas, human bytes, ordinal, Roman) | Polars::Fmt | polars__fmt_* | 18 |
| Window functions (Hann, Hamming, Blackman, Kaiser, Tukey, Lanczos, Parzen, …) | Polars::Window | polars__win_* | 17 |
| Sparse matrices (COO format) | Polars::Sparse | polars__sparse_* | 15 |
| JSON helpers (parse, stringify, path, keys/values, pluck, merge) | Polars::Json | polars__json_* | 14 |
| Time-series analytics (lag, diff, log/simple returns, drawdown, Sharpe, Sortino, VaR/CVaR) | Polars::TS | polars__ts_* | 13 |
| Graph algorithms (BFS, DFS, Dijkstra, connected components, Floyd-Warshall, PageRank) | Polars::Graph | polars__graph_* | 13 |
| Geometric (haversine, bearing, polygon area/perimeter, point-in-polygon, rotate/translate) | Polars::Geo | polars__geo_* | 12 |
| Boolean array ops | Polars::Bool | polars__bool_* | 11 |
| Set ops | Polars::Set | polars__set_* | 10 |
| Encoding (one-hot, label, binary, target, base64, hex, URL) | Polars::Encoding | polars__enc_* | 8 |
| Checksums (XOR, simple, Adler-32, BSD-16, internet) | Polars::Checksum | polars__sum_* | 7 |
| Interpolation (linear, nearest, zero-order, cubic-natural, log, bilinear grid) | Polars::Interp | polars__interp_* | 6 |
| Optimization (bisection, golden section, Newton-Raphson, secant, gradient descent) | Polars::Opt | polars__opt_* | 5 |
| Clustering (k-means, silhouette, DBSCAN neighbors) | Polars::Cluster | polars__cluster_* | 5 |
| scipy-class subtotal | 457 | ||
Grand total (+ polars__version) | 1417 |
Per-family .stk wrappers under lib/ expose every cdylib symbol as a typed Polars::<Family>::<verb> stryke fn — 46 packages, 1505 wrapper fns total. See the lib/ directory for the wrapper sources and the README API Reference for the package list. The 41 cdylib prefix families map onto these 46 packages (several families split a base + Ext package, e.g. arr_* → NdArray + NdArrayExt).
Why a package, not a builtin
stryke's core stays small on purpose — most one-liner / awk-replacement work doesn't need 200+ transitive crates of pandas + numpy machinery. The full DataFrame + ndarray + linalg + FFT surface hits a different scale: heavy deps, narrow use cases. The opt-in package tier exists precisely for this shape of code.
Parquet / Arrow IO routes through stryke-arrow to share a single arrow-rs link in-process — stryke-polars does not link arrow-rs directly to avoid dlsym conflicts.
Quality gates
Every commit on main runs through the CI matrix in .github/workflows/ci.yml. Pull requests must clear the same gates:
| Gate | Invariant |
|---|---|
cargo fmt --all --check | No drift from rustfmt; every file fmt-clean |
cargo clippy --all-targets --locked -- -D warnings | Zero clippy warnings (no module-level #![allow] escape hatches) |
cargo test --locked --no-fail-fast | 167 hand-crafted unit tests, all green on macos-latest + ubuntu-latest |
RUSTDOCFLAGS=-D warnings cargo doc --no-deps | Rustdoc clean; #![deny(missing_docs)] in lib.rs means every public item carries a /// comment |
| Release build (3 targets) | x86_64-unknown-linux-gnu, x86_64-apple-darwin, aarch64-apple-darwin — cdylib uploaded as build artifact |
| Contract gates | README newline + badge + h2 + https-link checks, workflow newline + no-tab checks, shell-script shebang + executable bit checks |
The 167 Rust unit tests assert mathematical correctness against canonical values — F(30) = 832,040, Levenshtein(kitten, sitting) = 3, Chebyshev L∞ distance = max(|x−y|), Haversine NYC→LA ≈ 3936 km, Adler-32("Wikipedia") = 0x11E60398 (RFC 1950 vector), and so on. The ffi_test::call harness in lib.rs exercises every test through the production JSON-over-FFI envelope (CString round-trip, catch_unwind on panic, allocation freed via stryke_free_cstring) so the tests cover the FFI seam alongside the math.
FFI layer
Each polars__* export takes a single *const c_char (NUL-terminated JSON args) and returns a *mut c_char (NUL-terminated JSON result). The cdylib owns the returned allocation; the stryke side must release it via the cdylib-exported stryke_free_cstring. stryke's rust_ffi::load_cdylib wires this automatically.
JSON envelope on success is the per-fn shape. JSON envelope on error is {"error": "<message>"}. Panics inside the cdylib are caught via catch_unwind and surfaced as errors — the stryke runtime never crashes on a cdylib bug.
Naming convention
Stryke-side wrappers are namespaced packages, one per lib/*.stk module: use Polars::DataFrame binds Polars::DataFrame::head, use Polars::Linalg binds Polars::Linalg::*, and so on. The cdylib-side FFI symbols are flat, double-underscore-namespaced polars__ plus a per-family verb prefix:
| cdylib symbol shape | Family |
|---|---|
polars__df_<verb> | DataFrame |
polars__sr_<verb> | Series |
polars__arr_<verb> | ndarray |
polars__np_<verb> | ufuncs |
polars__idx_<verb> | Index |
polars__linalg_<verb> / polars__rand_<verb> / polars__fft_<verb> / polars__poly_<verb> / polars__ma_<verb> / polars__dt64_<verb> | namespaced numpy families |
polars__pd_read_<fmt> / polars__pd_to_<fmt> | pandas IO |
Backing crates
The heavy numerics live in the cdylib only; core stryke never links any of them.
| Subsystem | Backing crate(s) |
|---|---|
| DataFrame / Series / Index / pandas IO | polars 0.45 (full feature set) |
| ndarray + ufuncs | ndarray 0.16 + rayon |
| linalg | nalgebra 0.33 (pure-Rust SVD / QR / LU / Cholesky / Eigen / solve / inv / det) |
| random | rand + rand_distr + ndarray-rand + rand_chacha |
| fft | rustfft 6.2 + realfft 3.4 |
| polynomial | hand-rolled on ndarray (recurrence formulas) |
| masked arrays | ndarray (mask vec parallel to data) |
| datetime64 / timedelta64 | chrono + chrono-tz 0.10 |
| Decimal dtype | rust_decimal 1.36 |
| FFI envelope | serde_json, anyhow, std::panic::catch_unwind |
The linalg subsystem uses pure-Rust nalgebra rather than ndarray-linalg / OpenBLAS to keep the dependency chain vendorable and avoid an openblas-src → openblas-build toolchain requirement at build time.
Layout
stryke-polars/ ├── Cargo.toml # crate-type = ["cdylib"] ├── src/ │ ├── lib.rs # FFI plumbing + polars__version │ └── df.rs / sr.rs / nd.rs / linalg.rs / ... # per-family modules ├── lib/ # stryke .stk wrappers (one per family) ├── stryke.toml # stryke package manifest ├── t/ # stryke-side integration tests (s test t/) ├── tests/ # Rust-side cargo tests + contract gates ├── examples/ # runnable .stk examples ├── Makefile # `make install` builds + installs └── docs/ # this site (GitHub Pages)
Sibling packages
Part of the stryke package family. Browse the others via the MenkeTechnologiesMeta umbrella repo:
- stryke-arrow — Apache Arrow / Parquet / Feather
- stryke-aws — S3, DynamoDB, SQS, Lambda, STS
- stryke-azure — Blob, Queues, Cosmos DB, Key Vault, Entra
- stryke-clickhouse — ClickHouse
- stryke-demo — live demos for every connector
- stryke-docker — Docker daemon API
- stryke-duckdb — embedded DuckDB SQL engine
- stryke-email — email + campaign client
- stryke-fleet — parallel expect/PTY automation
- stryke-gcp — Cloud Storage, Pub/Sub, Secret Manager, BigQuery, Firestore
- stryke-grpc — reflection-based gRPC client
- stryke-gui — mouse / keyboard / screen / clipboard automation
- stryke-k8s — Kubernetes
- stryke-kafka — Apache Kafka
- stryke-mcpd — MCP servers without a runtime
- stryke-mongo — MongoDB
- stryke-mssql — Microsoft SQL Server
- stryke-mysql — MySQL / MariaDB
- stryke-neo4j — Neo4j graph (Bolt)
- stryke-office — Office docs / PDF / images
- stryke-parquet — Parquet file inspector
- stryke-postgres — PostgreSQL
- stryke-redis — Redis / Valkey
- stryke-scrape — web scraping / crawling
- stryke-scylla — ScyllaDB / Cassandra
- stryke-search — Elasticsearch / OpenSearch
- stryke-selenium — browser automation (WebDriver)
- stryke-spark — Spark Connect (no JVM)
- stryke-utils — pure-stryke utility belt
- stryke-zmq — ZeroMQ messaging