PCIST-1.0 annex

Rendered from spec/PCIST-1.0.mdDownload source
Contents
  1. What this document is and why it exists
  2. Notation
  3. Step 1 — Preprocessing
  4. Step 2 — Dimensionality reduction
  5. Step 3 — Distance matrices
  6. Step 4 — State transitions
  7. Step 5 — Per-component complexity and the index
  8. Conformance
  9. What is still not fixed here

PCIST-1.0 · normative annex to METHOD.md §2.1 · in force from 2026-09-22

What this document is and why it exists

METHOD.md §2.1 defined the reference computation by naming PCIst and pointing at a library. gate/RESULT-DUAL.md showed what that costs: a second implementation, written from everything the standard and the published paper actually say, produced values for the same evoked responses differing from the reference by 3.76× to 35.67×, with ICC(2,1) = 0.043. Four of the five points the implementer had to guess were guessed wrongly, and none of the four could have been guessed correctly from the published sources.

This annex removes the guessing. It specifies the computation in full, so that a third party can reimplement it and obtain the same number to numerical tolerance, which is what §12 has been inviting them to do since 0.3.6 without supplying the means.

Incorporation by reference into a versioned annex in this repository is not the defect this replaces. The defect was that the operative content lived in a third party's source tree at an unpinned revision. Here the content is present, versioned with the standard, and changed only by amendment under GOVERNANCE.md.

Notation

Input is a trial-averaged evoked response Y, a real matrix of n_ch channels by n_t samples, and a vector t of sample times in milliseconds with the perturbation at t = 0. Baseline times are negative. Parameters are those named in §2.1's parameter sets: baseline_window = (b₀, b₁), response_window = (r₀, r₁), k, min_snr, max_var, n_steps, resample, embed, and avgref.

Define the index function

idx(t, x) = the number of entries of t strictly less than x.

Every window below is taken as the half-open index range [idx(t, w₀), idx(t, w₁)). The sample at the window's upper bound is excluded. This is a choice, it changes the sample count, and it is stated because it cannot be inferred.

Step 1 — Preprocessing

  1. If avgref is true, subtract from every channel, at each sample, the mean across channels at that sample. The default is false; §2.1 records why the earlier prescription was withdrawn.
  2. Baseline correction is not applied by this computation unless explicitly requested. When requested, subtract from each channel its own mean over all samples with t < −50 ms. Readings in this register baseline-correct upstream instead, and either route must be declared.
  3. Crop Y and t to the index range [idx(t, b₀), idx(t, r₁)). All later windows are computed against the cropped t.
  4. If resample is set, resample to n = ⌊(t_last − t_first) / 1000 × resample⌋ samples using Fourier-method resampling — the signal is transformed, the spectrum truncated or zero-padded, and transformed back — applied along the time axis jointly with the time vector. Polynomial or linear interpolation is not equivalent and will not reproduce the reference. resample is required by §2.1 and may not be omitted.

Step 2 — Dimensionality reduction

  1. Take the response-window block of the preprocessed signal, Y_r, of shape n_ch × n_response.

  2. Obtain a channel-space basis V whose columns are the left-singular vectors of Y_r, ordered by descending singular value s₁ ≥ s₂ ≥ …. Decomposing Y_rᵀ and taking its right-singular matrix gives the same basis; the reference does it that way and the two routes are interchangeable.

    An earlier draft of this annex warned that decomposing Y_r rather than its transpose "yields a basis in the wrong space." That warning was wrong and the conformance vector set found it: for A = U S Vᵀ, the right-singular matrix of Aᵀ is U, so the two routes agree exactly. A per-component sign flip is possible and is immaterial, because every later step uses |x_i − x_j|. spec/vectors/VECTORS-1.0.json asserts the invariance on all eight vectors rather than leaving it as an argument.

  3. Project the whole cropped signal — baseline and response together — onto the basis: S = Vᵀ Y. Row c of S is component c over all cropped samples. The basis comes from the response alone; the projection covers everything.

  4. Explained variance of component c is 100 · s_c² / Σ_j s_j².

  5. Retain the leading m components, where m is the smallest count whose cumulative explained variance reaches max_var percent, computed as m = n_ch − #{c : cumvar_c ≥ max_var} + 1, and m = n_ch when max_var = 100.

  6. For each retained component compute

    snr_c = √( mean over the response window of S_c² ÷ mean over the baseline window of S_c² )

    and keep the components with snr_c strictly greater than min_snr. The parameter k does not appear in this ratio. It enters at step 4.

  7. If no component survives, PCIst is 0.

Step 3 — Distance matrices

For each surviving component c, and separately for its baseline block (length n_b) and its response block (length n_r):

D[i, j] = | x_i − x_j |

the absolute difference between the component's amplitude at samples i and j. The diagonal is zero and both matrices are symmetric. Where embed is true the component is first time-delay embedded with L dimensions and delay tau, samples 0 … (L−1)·tau are dropped from the front, and D becomes the Euclidean norm over the embedding dimensions; embed is false in every parameter set §2.1 currently defines.

Step 4 — State transitions

This is the step no published source specifies, and the step where an implementer will otherwise go wrong. A state transition is not a thresholded distance. It is a change in the recurrence relation between one sample and the next.

For a threshold ε:

  1. Form the recurrence mask R = [D ≤ ε], taking the value 1 where the two samples are within ε of each other and 0 otherwise, inclusive of the diagonal, which is therefore always 1.

    The direction of the inequality does not matter, and an earlier draft of this annex implied it did. If R' = 1 − R then diff(R') = −diff(R), and step 2 takes the absolute value of that difference, so complementing the mask cannot change the transition count. is specified for definiteness, not because would be wrong. The vector set asserts this invariance too.

  2. Form the transition matrix by differencing the mask along its second index and taking absolute values:

    T[i, j] = | R[i, j+1] − R[i, j] | for j = 0 … n−2, with a final column of zeros appended so that T is n × n.

    T[i, j] is 1 exactly where sample i changes between being inside and outside ε of sample j as j advances by one. Differencing is one-sided; the matrix is not symmetrized.

  3. The number of state transitions is the total, normalized by the squared window length:

    NST(ε) = ( Σ_i Σ_j T[i, j] ) ÷ n²

    The sum runs over the full n × n matrix, including the diagonal and the appended zero column. Normalizing by rather than by the count of off-diagonal pairs is a choice and it changes the value.

The threshold is swept over n_steps values spaced linearly from

ε_min = the median of every entry of the baseline distance matrix, including its zero diagonal, to ε_max = the maximum entry of the response distance matrix, multiplied by max_thr_p (default 1.0).

The range is set by the baseline's median and the response's maximum. Using the pooled distances, or the minimum rather than the median, will not reproduce the reference.

Step 5 — Per-component complexity and the index

For each surviving component, over the swept thresholds:

ΔNST_c = n_r · max_ε [ NST_r(ε) − k · NST_b(ε) ], and 0 where that is negative.

k is the noise-control multiplier on the baseline count, not a term in the SNR and not a weight on the response. The maximum is taken over thresholds before multiplying by the response length, and n_r is the only length factor applied — the baseline count is not separately weighted by n_b.

Finally

PCIst = Σ_c ΔNST_c

summed over the surviving components.

Conformance

An implementation conforms to PCIST-1.0 where, on the eight vectors of spec/vectors/VECTORS-1.0.json, it reproduces every expected value to a relative tolerance of 1 × 10⁻⁶. The vectors ship as generators — a seed and a few shape parameters — so the input is reproducible in any language with a standard normal generator and an FFT, with nothing to download and nothing to drift.

The set is diagnostic, not merely pass or fail. Each vector records what eight specific mis-implementations return on it, one broken clause at a time, four of them errors an implementation written from the pre-specification standard actually made. An implementation that fails is matched against that table and told which clause it most likely got wrong, by section. Every mutation is distinguished by at least one vector, and the build refuses to publish a set with a blind spot — which is how the two immaterial choices noted above were found. A reading declares the specification version it was computed under in computation.spec_version, alongside the library, version and commit that §2.1 already requires.

What is still not fixed here

This annex specifies the computation. It does not settle the parameter sets, which are per-paradigm and versioned separately in §2.1, nor the resample figure for mouse_spes_v1, which §2.1 still marks provisional. It also does not make the computed value comparable across implementations of different specifications: §2.2's finding stands, and a value remains meaningful by comparison rather than in itself.