Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
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/
1 change: 1 addition & 0 deletions src/Core/Core.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<Compile Include="Residuated.fs" />
<Compile Include="Aggregate.fs" />
<Compile Include="RobustStats.fs" />
<Compile Include="TemporalCoordinationDetection.fs" />
<Compile Include="Window.fs" />
<Compile Include="Advanced.fs" />
<Compile Include="Fusion.fs" />
Expand Down
96 changes: 96 additions & 0 deletions src/Core/TemporalCoordinationDetection.fs
Original file line number Diff line number Diff line change
@@ -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.
Comment on lines +11 to +15
///
/// **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.
Comment on lines +9 to +24
///
/// **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.
[<AutoOpen>]
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 |]
Comment on lines +95 to +96
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Materialize inputs once in crossCorrelationProfile

crossCorrelationProfile calls crossCorrelation xs ys tau for every lag, and crossCorrelation immediately does Seq.toArray on both inputs each time. For lazy, side-effecting, or single-pass seq sources (for example stream-backed generators), this can yield inconsistent/empty results across lags because each lag re-enumerates the source; even for replayable sequences it performs avoidable full rescans per lag. Materializing xs/ys once inside crossCorrelationProfile (or adding an array-based helper) avoids this correctness and performance pitfall.

Useful? React with 👍 / 👎.

Comment on lines +92 to +96
101 changes: 101 additions & 0 deletions tests/Tests.FSharp/Algebra/TemporalCoordinationDetection.Tests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
module Zeta.Tests.Algebra.TemporalCoordinationDetectionTests

open System
open FsUnit.Xunit
open global.Xunit
open Zeta.Core


// ─── crossCorrelation at lag 0 ─────────

[<Fact>]
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)

[<Fact>]
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)

[<Fact>]
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 ─────────

[<Fact>]
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)

[<Fact>]
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 ─────────

[<Fact>]
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)

[<Fact>]
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 ─────────

[<Fact>]
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 |]

[<Fact>]
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)

[<Fact>]
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)
1 change: 1 addition & 0 deletions tests/Tests.FSharp/Tests.FSharp.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<Compile Include="Algebra/ZSet.Overflow.Tests.fs" />
<Compile Include="Algebra/IndexedZSet.Tests.fs" />
<Compile Include="Algebra/RobustStats.Tests.fs" />
<Compile Include="Algebra/TemporalCoordinationDetection.Tests.fs" />

<!-- Circuit/ -->
<Compile Include="Circuit/Circuit.Tests.fs" />
Expand Down
Loading