From 5b3658c2ce382469e3177c40b0d80c4dd02128ca Mon Sep 17 00:00:00 2001 From: Aaron Stainback Date: Fri, 24 Apr 2026 01:34:23 -0400 Subject: [PATCH] =?UTF-8?q?core:=20TemporalCoordinationDetection.crossCorr?= =?UTF-8?q?elation=20+=20profile=20=E2=80=94=202nd=20graduation=20(11th=20?= =?UTF-8?q?ferry,=20Aaron-designed)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ships the first foundational primitive from Aaron's differentiable firefly network + trivial cartel detect design (11th ferry, PR #296, Aaron-designed / Amara-formalized). Second graduation under the Otto-105 cadence, landing same tick as the ferry absorb. Aaron Otto-105: "the diffenrencable firefly network with trivial cartel detect was my design i'm very interested in that." Module naming — Aaron Otto-106 two clarifications: 1. "Coordination.fs this is going to be confusing name when we have distributed consensus/coordination of our nodes and control plane?" — renamed to TemporalCoordinationDetection to reserve the plain-Coordination namespace for distributed consensus. 2. "TemporalCoordination is it all about detection might as well add that suffix" — added Detection suffix. Surface: - TemporalCoordinationDetection.crossCorrelation : double seq -> double seq -> int -> double option Pearson cross-correlation at a single lag tau. Returns None when overlap < 2 samples or either window is constant (undefined variance). Positive tau aligns ys[i+tau] with xs[i]; negative tau aligns ys[i] with xs[i-tau]. - TemporalCoordinationDetection.crossCorrelationProfile : double seq -> double seq -> int -> (int * double option)[] Computes correlation across the full range [-maxLag, maxLag]. Attribution: - Concept (temporal coordination detection, firefly-synchronization metaphor, trivial-cartel-detect as first-order-signal tier) = Aaron's design - Technical formulation (Pearson cross-correlation at lag, correlation-profile shape) = Amara's formalization (11th ferry) - Implementation = Otto Why Pearson-normalized: scale-invariant in both axes; meaningful signal at [-1, 1] across streams with very different magnitudes (small-stake vs large-stake nodes) rather than arbitrary scale. .gitignore: .playwright-mcp/ added. Per-session browser state (screenshots / console / page-dump YAMLs) is per-run artifact; parallel to drop/ staging per PR #265 Otto-90. Tests (10, all passing): - Identical series at lag 0 -> 1.0 - Negated series at lag 0 -> -1.0 - Constant series -> None (undefined variance) - One-step-shifted series at lag 1 -> 1.0 - Negative lag alignment - Single-element overlap -> None - Lag larger than series -> None - Profile length = 2*maxLag + 1 - Profile identical series peaks at lag 0 - Profile with maxLag < 0 -> empty Build: 0 Warning(s), 0 Error(s). Next graduation candidates (feedback memory queue): - PLV (phase-locking value) — composes over crossCorrelation - BurstAlignment detector — cluster logic over profile - ModularitySpike / EigenvectorCentralityDrift — need graph substrate - antiConsensusGate (10th ferry) — independent path Composes with: src/Core/RobustStats.fs (PR #295). Co-Authored-By: Claude Opus 4.7 --- .gitignore | 8 +- src/Core/Core.fsproj | 1 + src/Core/TemporalCoordinationDetection.fs | 96 +++++++++++++++++ .../TemporalCoordinationDetection.Tests.fs | 101 ++++++++++++++++++ tests/Tests.FSharp/Tests.FSharp.fsproj | 1 + 5 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 src/Core/TemporalCoordinationDetection.fs create mode 100644 tests/Tests.FSharp/Algebra/TemporalCoordinationDetection.Tests.fs diff --git a/.gitignore b/.gitignore index 8581be00..aba273e1 100644 --- a/.gitignore +++ b/.gitignore @@ -89,4 +89,10 @@ tools/tla/states/ # bun + TypeScript tooling — post-setup scripting surface per # docs/DECISIONS/2026-04-20-tools-scripting-language.md. The # bun.lock file IS committed; node_modules is not. -node_modules/ \ No newline at end of file +node_modules/ + +# playwright-mcp artifacts — per-session browser state +# (screenshots / console / page-dump YAMLs). Regenerated per +# use; not needed in-repo. Parallel to drop/ staging per +# PR #265 Otto-90. +.playwright-mcp/ diff --git a/src/Core/Core.fsproj b/src/Core/Core.fsproj index 7073ae30..e43b4c64 100644 --- a/src/Core/Core.fsproj +++ b/src/Core/Core.fsproj @@ -40,6 +40,7 @@ + diff --git a/src/Core/TemporalCoordinationDetection.fs b/src/Core/TemporalCoordinationDetection.fs new file mode 100644 index 00000000..fb250f57 --- /dev/null +++ b/src/Core/TemporalCoordinationDetection.fs @@ -0,0 +1,96 @@ +namespace Zeta.Core + +open System + + +/// **Temporal Coordination Detection — foundational primitives.** +/// +/// This module hosts the single-node-shippable detection primitives +/// from Aaron's *differentiable firefly network + trivial cartel +/// detect* design, as formalized by Amara in the 11th courier +/// ferry (`docs/aurora/2026-04-24-amara-temporal-coordination- +/// detection-cartel-graph-influence-surface-11th-ferry.md`). Full +/// multi-node architecture awaits Zeta's multi-node foundation; +/// these pure-function primitives ship incrementally per the +/// Otto-105 graduation-cadence. +/// +/// **Attribution.** The underlying concepts (temporal-coordination +/// detection as a primary defence surface, the firefly-synchronization +/// metaphor, trivial-cartel-detect as the first-order-signal tier) +/// are Aaron's design. Amara's contribution is the technical +/// vocabulary and specific formulations (phase-locking value, cross- +/// correlation, modularity spikes, eigenvector centrality drift, …). +/// This module implements Amara's formalizations; the design intent +/// behind them is Aaron's. +/// +/// **Scope of this first graduation.** Only `crossCorrelation` and +/// `crossCorrelationProfile` ship here — the single most portable +/// primitive for detecting timing coordination between two event +/// streams. Downstream primitives (phase-locking value, burst- +/// alignment clusters, modularity-spike detectors, eigenvector- +/// centrality drift) are separate graduation candidates that +/// compose over this one. +/// +/// **Why it matters for cartel detection.** Honest distributed +/// actors produce noisy, partially-independent event streams; +/// coordinated actors produce phase-aligned streams. Pearson cross- +/// correlation at lag τ quantifies how similarly two series move +/// at a time offset, yielding the core signal for the "obvious +/// coordinated timing" (trivial-cartel) case. Subtler coordination +/// requires the harder primitives added in later graduations. +[] +module TemporalCoordinationDetection = + + /// Pearson cross-correlation of two series at lag `tau`. The + /// value is normalized to `[-1.0, 1.0]` when defined; returns + /// `None` when either series has fewer than two elements that + /// overlap at the given lag, or when either overlap window is + /// constant (standard deviation = 0, undefined denominator). + /// + /// Lag semantics: positive `tau` aligns `ys[i + tau]` with + /// `xs[i]`; negative `tau` aligns `ys[i]` with `xs[i - tau]`. + /// A detector asking "does `ys` lead `xs` by `k` steps?" passes + /// `tau = k`. + let crossCorrelation (xs: double seq) (ys: double seq) (tau: int) : double option = + let xArr = Seq.toArray xs + let yArr = Seq.toArray ys + let startX, startY = + if tau >= 0 then 0, tau + else -tau, 0 + let overlap = min (xArr.Length - startX) (yArr.Length - startY) + if overlap < 2 then None + else + let mutable meanX = 0.0 + let mutable meanY = 0.0 + for i in 0 .. overlap - 1 do + meanX <- meanX + xArr.[startX + i] + meanY <- meanY + yArr.[startY + i] + let n = double overlap + meanX <- meanX / n + meanY <- meanY / n + let mutable cov = 0.0 + let mutable varX = 0.0 + let mutable varY = 0.0 + for i in 0 .. overlap - 1 do + let dx = xArr.[startX + i] - meanX + let dy = yArr.[startY + i] - meanY + cov <- cov + dx * dy + varX <- varX + dx * dx + varY <- varY + dy * dy + if varX = 0.0 || varY = 0.0 then None + else Some (cov / sqrt (varX * varY)) + + /// Cross-correlation across the full lag range `[-maxLag, maxLag]`. + /// Returns one entry per lag; `None` entries indicate lags where + /// the overlap window was too short or degenerate (flat input) + /// to compute a correlation. + /// + /// Intended downstream use: feed to a burst-alignment / cluster + /// detector that flags lags where `|corr|` is unusually high + /// versus a baseline, the operational "firefly detection" case + /// from the 11th ferry's signal model. + let crossCorrelationProfile (xs: double seq) (ys: double seq) (maxLag: int) : (int * double option) array = + if maxLag < 0 then [||] + else + [| for tau in -maxLag .. maxLag -> + tau, crossCorrelation xs ys tau |] diff --git a/tests/Tests.FSharp/Algebra/TemporalCoordinationDetection.Tests.fs b/tests/Tests.FSharp/Algebra/TemporalCoordinationDetection.Tests.fs new file mode 100644 index 00000000..f8acce01 --- /dev/null +++ b/tests/Tests.FSharp/Algebra/TemporalCoordinationDetection.Tests.fs @@ -0,0 +1,101 @@ +module Zeta.Tests.Algebra.TemporalCoordinationDetectionTests + +open System +open FsUnit.Xunit +open global.Xunit +open Zeta.Core + + +// ─── crossCorrelation at lag 0 ───────── + +[] +let ``crossCorrelation of identical series is 1 at lag 0`` () = + let xs = [ 1.0; 2.0; 3.0; 4.0; 5.0 ] + TemporalCoordinationDetection.crossCorrelation xs xs 0 + |> Option.map (fun v -> Math.Round(v, 9)) + |> should equal (Some 1.0) + +[] +let ``crossCorrelation of negated series is -1 at lag 0`` () = + let xs = [ 1.0; 2.0; 3.0; 4.0; 5.0 ] + let ys = xs |> List.map (fun v -> -v) + TemporalCoordinationDetection.crossCorrelation xs ys 0 + |> Option.map (fun v -> Math.Round(v, 9)) + |> should equal (Some -1.0) + +[] +let ``crossCorrelation of constant series is None (undefined variance)`` () = + // A flat series has zero variance; Pearson correlation is + // undefined. Detectors must get None, not NaN or 0.0. + let xs = [ 5.0; 5.0; 5.0; 5.0 ] + let ys = [ 1.0; 2.0; 3.0; 4.0 ] + TemporalCoordinationDetection.crossCorrelation xs ys 0 |> should equal (None: double option) + + +// ─── crossCorrelation at nonzero lag ───────── + +[] +let ``crossCorrelation detects a one-step lag alignment`` () = + // ys is xs shifted right by 1. At tau=1, the aligned windows + // are the same shape, so correlation should be 1. + let xs = [ 1.0; 2.0; 3.0; 4.0; 5.0 ] + let ys = [ 0.0; 1.0; 2.0; 3.0; 4.0 ] + TemporalCoordinationDetection.crossCorrelation xs ys 1 + |> Option.map (fun v -> Math.Round(v, 9)) + |> should equal (Some 1.0) + +[] +let ``crossCorrelation at negative lag aligns x ahead of y`` () = + // Same series, probed at tau=-1: the aligned windows are + // xs[1..] vs ys[..], both length 4, identical shape within + // their respective slices when xs = ys. + let xs = [ 1.0; 2.0; 3.0; 4.0; 5.0 ] + TemporalCoordinationDetection.crossCorrelation xs xs -1 + |> Option.map (fun v -> Math.Round(v, 9)) + |> should equal (Some 1.0) + + +// ─── crossCorrelation edge cases ───────── + +[] +let ``crossCorrelation with single-element overlap is None`` () = + // Overlap of 1 is below the 2-sample minimum; must return None. + let xs = [ 1.0; 2.0 ] + let ys = [ 1.0; 2.0 ] + TemporalCoordinationDetection.crossCorrelation xs ys 1 |> should equal (None: double option) + +[] +let ``crossCorrelation with lag larger than series returns None`` () = + let xs = [ 1.0; 2.0; 3.0 ] + let ys = [ 1.0; 2.0; 3.0 ] + TemporalCoordinationDetection.crossCorrelation xs ys 10 |> should equal (None: double option) + + +// ─── crossCorrelationProfile ───────── + +[] +let ``crossCorrelationProfile returns 2 maxLag + 1 entries`` () = + let xs = [ 1.0; 2.0; 3.0; 4.0; 5.0; 6.0 ] + let ys = [ 1.0; 2.0; 3.0; 4.0; 5.0; 6.0 ] + let profile = TemporalCoordinationDetection.crossCorrelationProfile xs ys 2 + profile.Length |> should equal 5 + profile |> Array.map fst |> should equal [| -2; -1; 0; 1; 2 |] + +[] +let ``crossCorrelationProfile identical series peaks at lag 0`` () = + // Zero-lag correlation of a series with itself is 1.0. + let xs = [ 1.0; 2.0; 3.0; 4.0; 5.0; 6.0; 7.0; 8.0 ] + let profile = TemporalCoordinationDetection.crossCorrelationProfile xs xs 3 + let zeroLagCorr = + profile + |> Array.find (fun (lag, _) -> lag = 0) + |> snd + zeroLagCorr + |> Option.map (fun v -> Math.Round(v, 9)) + |> should equal (Some 1.0) + +[] +let ``crossCorrelationProfile with negative maxLag returns empty array`` () = + let xs = [ 1.0; 2.0; 3.0 ] + let ys = [ 1.0; 2.0; 3.0 ] + TemporalCoordinationDetection.crossCorrelationProfile xs ys -1 |> should equal ([||]: (int * double option) array) diff --git a/tests/Tests.FSharp/Tests.FSharp.fsproj b/tests/Tests.FSharp/Tests.FSharp.fsproj index b65af635..f9e64744 100644 --- a/tests/Tests.FSharp/Tests.FSharp.fsproj +++ b/tests/Tests.FSharp/Tests.FSharp.fsproj @@ -18,6 +18,7 @@ +