Synthetic signals whose answers are known before anything runs.
python make_reference_study.py # writes config.json and assets/timeseries/
python -m dims_analysis.cli run --config config.jsonEverywhere else in this project a check can only ask "did it crash?". On real
data that is all that is available: DET = 0.2571 is a number nobody can
verify. Here the answer is arithmetic — a sine at a 2 s period recurs every
2 s, a copy delayed 0.4 s puts its cross-recurrence line 0.4 s off the
diagonal, and two independent red noises exceed a 95 % chance level in 5 % of
cells by construction.
| signal | what it is for |
|---|---|
sine |
periodic structure at a known spacing |
sine_lagged |
the same, delayed 0.4 s — a known lag and a known phase |
noise_a, noise_b |
independent AR(1), a known absence of relationship |
flat |
zero variance — the degenerate case |
quantised |
many equal distances, so a 7 % target cannot be met |
sine_x10 |
the same sine, ten times larger — every analysis normalises, so nothing may change |
sine_2x |
the same 2 s sine over the same 20.48 s, sampled twice as fast |
eff_hand_l, eff_hand_r, eff_other |
three effectors: the first two share 70 % of their variation, the third shares none |
The three effectors carry the structure a cross-effector network exists to find, and deliberately the shape of the real Karnatak result — within-person coupling real, between-person at chance. Measured: 0.75 of cells above chance for the coupled pair, 0.04 and 0.08 for the other two. One solid edge, two dashed.
Coupling is made by sharing a red-noise component, not by adding a sinusoid, and that choice is itself a finding. A first attempt shared a 3 s sine: the coupled pair read 1.00 above chance inside that band, but so did the unrelated pairs at 0.30 and 0.42. The AR(1) null assumes both signals are red noise, and a signal carrying a deterministic rhythm is not — so the null understates its level and independence stops looking like independence.
The data is generated rather than committed: producing it is the same from-zero path a real study takes, and a formula describes it exactly, so carrying a megabyte of CSV would only add something to drift.
The assertions live in tests/reference/, one file
per analysis. They used to sit inside this folder, which meant every temporary
copy of this study carried a copy of them:
tests/reference/test_rqa.py |
recurrence, determinism, the achieved rate, a constant signal |
tests/reference/test_crqa.py |
the known lag, 20 samples off the diagonal |
tests/reference/test_crosswavelet.py |
the known phase, the chance level, and the Monte Carlo that produces it |
tests/reference/test_network.py |
what the cross-effector network needs from a payload, and how it breaks |
tests/reference/test_metrics.py |
RR, DET, LAM and L_MAX, for both RQA and cross-RQA, against pyrqa |
tests/reference/test_coherence_metrics.py |
cross-wavelet: the scale axis, amplitude invariance, and the signature of the defect this project was rebuilt around |
tests/reference/test_determinism.py |
two runs give identical output; one step does not erase another |
tests/reference/test_wavelet_reference.py |
the transform and all three significance levels, against Torrence & Compo (1998) — see TORRENCE_COMPO.md |
tests/reference/test_baseline.py |
every number, pinned against baseline.json |
cd dims
python -m pytest tests/reference -q # 120 tests, about 22 sTwo kinds, and both are needed.
The first four files check properties: DET agrees with its own definition and with pyrqa, the cross-recurrence line sits at the lag that was put in, the phase is 2·π·f·τ, unrelated signals beat chance 5 % of the time. A property survives a change that shifts every value slightly — which is exactly what a refactor of how something is stored or computed can do.
test_baseline.py is the one that does not. It pins every number the analyses
currently produce, through summary.py, which reads meaning rather than
bytes — so replacing the payload format moves that one file and leaves the
expected values alone.
Checked by sabotage, three ways:
| what was broken | what the baseline said |
|---|---|
| reduction back to striding | rqa/noise_a.rate_drawn: 0.0709746 → 0.0735611 |
| recurrence target 7 % → 8 % | mean_DET: 0.300346 → 0.340744, and the provenance directly |
| phase averaged as a scalar | mean_phase_rad: 0.003938 → 0.002944 |
When the baseline fails, do not regenerate it. Look at which number moved
and by how much, decide whether the new value is better, and only then run
python tests/reference/make_baseline.py — saying in the commit why each number moved.
Regenerating first is how a regression becomes the new normal.
The cross-wavelet file tests the simulation two ways, and both are needed. That it is calibrated — signals drawn from the null exceed the 95 % level in 5 % of cells — and that it is computed: the level rises at both ends of the scale range, where fewer independent cycles fit, so a stub returning a plausible constant fails. Checked: a constant passes the calibration tests and fails the shape test.
Each names the defect it would have caught. There is no xfail left: the two
that pinned the averaged significance levels as known-wrong are now ordinary
passing tests. Where a defect is recorded rather than fixed, mark it
xfail(strict=True) — strictness means it fails the moment it starts passing,
so a fix cannot go unnoticed and a stale pin cannot survive.
This suite is a gate, not a convenience. Every change to steps/rqa.py,
steps/crqa.py, steps/crosswavelet.py or anything under common/ runs it
before being committed, and the generated core-update.yml in every study runs
it against a candidate release before offering the bump — because the analyses
are not vendored, so a study that bumps takes this code and bakes its answers
into committed assets.
One caution: pyrqa is importorskiped, so a machine without OpenCL stays
green while the independent oracle for DET, LAM and RR does not run at all.
Pass -rs and read the skips. CI installs pocl-opencl-icd and fails outright
if pyrqa is missing.
Requirements this study exists to check:
docs/contracts/analysis-output.md.
Where every suite lives and why: tests/README.md.
Not hypothetical: each of these was live in the shipped code when the reference study was built, and each was found by giving the analyses data whose answer was already known.
| found | what it was |
|---|---|
recurrence_rate = -0.000977517 |
a constant signal normalised to NaN, so the matrix came out empty and the line of identity was subtracted from zero. A negative share of cells, on a dashboard caption. |
LAM = 1.0137 |
a share of points above 1. The diagonal and vertical line scans ignore different things, so they cannot share a denominator — fixing DET's had inflated LAM's. pyrqa said which of the two values was right. |
| 33.7 % against a 7 % target | a quantised signal puts the threshold percentile on a plateau. Recorded now, with a warning that DET and LAM are not comparable across different achieved rates. |
| the cross-wavelet cap | 1024 samples against a 500-point cap drew 512 — the step computed its own reduction factors by floor division. |
numpy.randn |
pycwt's red-noise generator branches at exactly zero autocorrelation and calls a function removed in NumPy 2, so white noise silently lost its chance level. |
| the wrong distribution, three times | the cross-wavelet spectrum is the square root of a product of two chi-squares (Torrence & Compo eq. 30), not a chi-square. All three significance levels used the chi-square and evaluated the background at the mean of the two AR(1) coefficients: the local level was 1.50-2.39x too high, the time-averaged 1.32-1.37x, the scale-averaged 1.267x. |
Three things were also measured and turned out not to be defects, which is the other half of the value: DET on a pure sine is 0.899 and not ~1.0; the coherence chance level barely responds to how red the noise is; and the fraction of cells above that level is 0.05 only when the cone of influence is excluded — including it gives 0.126.