A small, honest instrument for asking "is there structure here?" — built well enough to know when the honest answer is "we can't know."
It started as a naive question — model a curve from perfect chaos to perfect stability (π) — and resolved into something sharper:
Order and chaos are not intrinsic properties of a signal. They are relative to the lens you measure with, and the one lens that would make the distinction absolute — Kolmogorov complexity K — is uncomputable, and adversarially so.
π is the proof: it looks maximally random to every computable lens, yet has a tiny generating program. It isn't a point on the order↔chaos curve; it's the pin showing the curve must fold into a higher-dimensional space whose decisive axis can't be computed.
ordchaos.py places any 1-D sequence on six complementary, validated lenses
(core library needs only numpy + mpmath; matplotlib is for the demo scripts):
| lens | function | what it sees |
|---|---|---|
| (H, C) plane | statistical_complexity |
Bandt–Pompe permutation entropy + MPR Jensen–Shannon complexity |
| surrogate significance | surrogate_test |
is structure more than linearly-filtered noise? (IAAFT) |
| calibrated compression | compression_complexity |
LZ redundancy (gzip/bz2/lzma), random → 1.0 |
| CTM / BDM | bdm_complexity, ctm |
algorithmic complexity from an empirical Turing-machine table (works on short strings) |
| ε-machine | epsilon_machine |
the generating machine — causal states, transitions, Cμ, hμ (CSSR-style, with determinization) |
| program-search K | program_search_k |
one-sided algorithmic complexity estimate |
place(series) returns (H, C, K); everything else is a direct call. See
test_ordchaos.py for 131 known-value checks (golden-mean Cμ = 0.918,
period-3, GUE statistics, calibration, etc.).
import ordchaos as oc
oc.place(my_signal) # -> Placement(H, C, K, in_bounds, feats)
m = oc.epsilon_machine(my_signal) # the reconstructed causal-state machine
oc.simulate(m, 1000) # generate synthetic data with the same causal structure
oc.predict(m, history) # next-symbol distribution (the minimal optimal predictor)
oc.joint_epsilon_machine(X) # causal states of a vector process (X: n_samples × channels)The ε-machine is a generative model, not just a descriptor: simulate walks
it to produce synthetic sequences that re-fit to the same machine (a golden-mean
machine's output keeps Cμ and never emits the forbidden 00), and predict
returns the next-symbol distribution with hμ as the honest error floor.
The toolkit can confirm hidden structure soundly (a short program found ⇒
definitely not random) but can never certify randomness (none found ⇒ maybe
you didn't search hard enough). The program_search_k LCG demonstration makes
this concrete: a pseudo-random stream with tiny true K is mislabeled "random"
because its generator isn't in the search space. K is one-sided and adversarially
uncomputable — which is why every statistical lens here is, by design, blind to π.
The K side has more lenses and a fourth coordinate:
| capability | function | what it does |
|---|---|---|
| linear complexity | linear_complexity |
Berlekamp–Massey: the shortest LFSR reproducing the binarized series — exact, ~n/2 for random, tiny for any linear PRNG (LFSR, m-sequence, xorshift bit-stream) |
| expanded K battery | program_search_k |
now also cracks LFSRs and detects elementary-CA center columns (rule 30 = the "π of CAs": tiny program, looks random, fools the linear lens but is caught here), plus a wider named-constant library (√5, √7, ∛2, ζ(3), Catalan, γ, …) |
| logical depth | logical_depth |
Bennett's fourth axis (the long-deferred one): not how short the program is (K) but how much work it does — π is K≈0 yet deep (a one-liner, but costly to compute), a periodic ramp is K≈0 and shallow, random is high-K and shallow. Deterministic (operation counts, never wall-clock); one-sided like K |
| MT predictor | predict_mt |
recovers MT19937 state from 624 outputs and predicts the rest exactly — a famous PRNG cracked by prediction (kept standalone: MT's ~20 kbit state means it does not lower K) |
oc.logical_depth(my_signal) # -> {'k', 'generator', 'depth_ops', 'depth_per_symbol', 'deep'}
oc.linear_complexity(my_bits) # -> ~1 random, ~0 linear/orderedThe six lenses characterize one 1-D signal. Four further tools extend that to relationships, time, and uncertainty — same honest, validated discipline:
| capability | function | what it does |
|---|---|---|
| multivariate (H, C) | mv_statistical_complexity |
joint permutation entropy + MPR complexity over the (d!)^channels joint-pattern space; redundant channels lower joint H, independent ones push it toward 1 |
| cross-series causality | transfer_entropy, coupling, transfer_entropy_test |
symbolic transfer entropy TE(X→Y) in bits — directional, so it separates driver from driven; Miller–Madow bias-corrected, with a shuffled-source significance test and effective-TE estimate. epochs= keeps history windows from crossing trial/segment seams (no spurious coupling at concatenation joins) |
| continuous causality (kNN) | transfer_entropy_ksg, coupling_ksg |
Kraskov/Frenzel–Pompe k-nearest-neighbor TE — no quantization; a quantization-free cross-check on the symbolic estimator. Uses scipy's cKDTree when installed (O(N log N); ~130× faster at N=8k), numpy-only brute-force fallback otherwise |
| network inference | conditional_transfer_entropy, causal_network |
conditional TE removes common-driver confounds — CTE(X→Y|Z)≈0 when Z drives both — turning a multi-channel TE matrix into a directed connectivity graph; fdr=True controls the false discovery rate (Benjamini–Hochberg) across all edges for statistically sound large networks |
| change points | change_points, sliding |
model-free location of regime shifts via binary segmentation on a windowed statistic, gated by both an effect-size jump and a permutation test |
| uncertainty | bootstrap_ci, hc_ci |
moving-block-bootstrap confidence intervals for any scalar lens (blocks preserve temporal dependence; the interval always brackets the estimate). Returns a named CI(point, lo, hi); iid=True for the ordinary i.i.d. bootstrap |
| paired effect + power | paired_test, min_detectable_effect, statistical_power |
matched-pair significance for discrete win/loss (exact sign test + sign-flip permutation, apt where a mean-bootstrap isn't) and the "know when you can't know" companion — the smallest effect N pairs could even detect, so a sub-floor result is named honestly instead of over-claimed |
| 2-D / spatial | spatial_statistical_complexity, spatial_forbidden_fraction |
the Ribeiro complexity-entropy plane for images/fields — 2-D ordinal patterns over dx×dy patches place an image exactly as the 1-D plane places a series (random→(1,0), gradient→(0,0), textures high on the arc) |
| coupling delay + embedding | coupling_delay, embedding_delay, embedding_dimension |
which lag carries the coupling (a delay-d copy peaks at exactly d), plus τ (first autocorrelation zero) and d (false nearest neighbors: logistic→1, Hénon→2, noise high) — removes the fixed-parameter guesswork |
| missing data | skipnan= on the (H,C) lenses, interpolate_gaps |
gappy series: skip windows containing NaN (measure the gap-free runs) or linearly bridge holes; NaN still raises without skipnan (no silent wrong number) |
| process distance | process_distance, distance_matrix, machine_distance |
"how different are these two dynamics?" — Jensen–Shannon distance of ordinal-pattern (or ε-machine word) distributions, in [0,1]; 0 = same process, ~1 = disjoint. Feeds clustering, taxonomy, anomaly-by-drift |
| information decomposition | partial_information_decomposition |
splits I(target; S1, S2) into redundant / unique / synergistic atoms (method='imin'/'mmi'; BROJA/CCS via the dit recipe): a copied target is redundant, target = S1 XOR S2 is purely synergistic. Also returns co_information — a synergy signal independent of the redundancy definition (<0 synergy, >0 redundancy) |
oc.coupling(x, y) # {'te_xy', 'te_yx', 'net', 'direction'}
oc.causal_network(X) # directed TE graph over X's channels (confound-free)
oc.change_points(signal) # -> [sample indices where dynamics shift]
oc.hc_ci(signal) # -> {'H': (point, lo, hi), 'C': (point, lo, hi)}
oc.mv_statistical_complexity(X) # X is (n_samples, n_channels)For live data, OnlineStatComplexity maintains the Bandt–Pompe histogram
incrementally — update(x) is O(d log d) per sample and returns the running
(H, C). It is exact (fed the whole series it equals the batch estimator);
a trailing window decays old patterns out for non-stationary monitoring. On
the performance side, the symbolic TE estimators are fully vectorized (mixed-
radix coding + np.unique, no Python loop), and transfer_entropy_ksg uses
scipy's cKDTree when the [scale] extra is installed (drop-in pip install ordchaos[scale]), keeping the core numpy + mpmath.
m = oc.OnlineStatComplexity(window=1000)
for x in stream:
H, C = m.update(x) # running (H, C), livereport(x) runs the battery and returns a JSON-serializable verdict; ordchaos analyze file.csv does the same from the shell. ε-machines and causal networks
save/load as JSON for reproducible pipelines, and batch_report maps the
battery over a corpus.
ordchaos analyze signal.csv # one-page characterization + verdict
ordchaos analyze signal.csv --json # same, machine-readable
ordchaos batch *.csv # corpus -> JSON listoc.report(signal) # {H, C, K, depth, ..., 'verdict': '...'}
oc.report_html(oc.report(signal)) # self-contained HTML one-pager (no deps)
oc.report_figure(signal, "fig.png") # signal + (H,C) plane (needs [demos])
oc.save_machine(oc.epsilon_machine(signal), "m.json"); oc.load_machine("m.json")the_fold.py— the whole thesis in one figure. Places π, e, √2, φ, an LCG stream and genuine noise on(H, C, K)viaoc.place(): they collapse into a single corner of the (H,C) plane (order and chaos indistinguishable), and only the uncomputable K axis folds them apart — π splits to K≈0 while noise stays at K≈1, and the LCG stays stuck with the noise despite a tiny true K (the axis is one-sided). Start here.
Each script below characterizes a real object and reports an honest verdict:
cf_experiment.py/cf_sqrtD.py/cf_degree.py/cf_bounded.py— continued fractions. Representation surfaces quadratic-irrational structure invisible in base-10 (ε-machine recovers the exact period); Cμ(√D) = log₂(CF period) = a Pell/regulator invariant; degree-≥3 algebraics are empirically Gauss–Kuzmin / unbounded (an open problem).zeta_zeros.py— reproduces Montgomery–Odlyzko: Riemann ζ-zero spacings follow GUE, not Poisson; no structure beyond GUE. (A tempting baseline artifact was caught and not over-claimed.)gw_strain.py— LIGO GW150914: characterizes detector noise (coloration, 60 Hz/violin lines, non-stationarity); recovers the actual chirp to 20 ms by whitening. Draws its own limit: a characterizer, not a transient detector.ising_phase.py— Cμ as a model-free phase-transition locator: it peaks at the 2D-Ising critical temperature without ever seeing the magnetization.complexity_entropy_*.py,effective_dim_vs_M.py,k_axis_program_search.py,surrogate_demo.py,bdm_demo.py— the conceptual scaffolding: the (H,C) plane, the M-dimensional sweep showing the computable manifold is ~5–7-D, the program-search K-axis (win + wall), and the demos.
ctm_build.py builds ctm_table.json from 3M small Turing machines (the genuine
coding-theorem-method construction, small-scale).
This is a characterization toolkit, not a predictor or detector. The applications recover known structure; the discoveries here were of the tools' correctness, not of nature. The components (computational mechanics, CTM/BDM, surrogate testing) predate this — the value is integration, a six-lens cross-check, full ownership, and a validation/honest-attribution discipline.
External datasets (data/, gitignored) are re-fetchable; see each script header
for the source.
pip install -e . # core library (numpy + mpmath)
pip install -e ".[scale]" # + scipy for the KSG fast path (large data)
pip install -e ".[demos]" # also pull matplotlib for the demo scripts
pip install -r requirements.txt # exact pinned versions the checks were validated againstThe CTM table (ctm_table.json) loads relative to the module, so an editable
install keeps it side by side; that is the supported install today.
python3 test_ordchaos.py # 131 known-value checks (also run in CI)
python3 the_fold.py # the central claim in one figure (the fold)
python3 ising_phase.py # Cμ finds the Ising transition
python3 cf_experiment.py # the continued-fraction representation flipCI runs the 131 known-value checks on every push/PR across Python 3.10–3.12, plus
a pinned-environment job, so "validated" stays continuously true rather than a
one-time claim — see .github/workflows/ci.yml.