Skip to content

core: Graph.largestEigenvalue via power iteration — 10th graduation#321

Merged
AceHack merged 1 commit intomainfrom
feat/graph-largest-eigenvalue-power-iteration
Apr 24, 2026
Merged

core: Graph.largestEigenvalue via power iteration — 10th graduation#321
AceHack merged 1 commit intomainfrom
feat/graph-largest-eigenvalue-power-iteration

Conversation

@AceHack
Copy link
Copy Markdown
Member

@AceHack AceHack commented Apr 24, 2026

First cartel-detection primitive per Graph ADR #316. Power iteration + L2 normalize + Rayleigh quotient on symmetrized adjacency.

Load-bearing cartel-injection test: baseline 5-node sparse graph vs. baseline + K_4 clique (weight 10). Attacked lambda ≥ 5x baseline lambda. Detection signal in action.

Test invariants:

  • K3 triangle (weight 1) → lambda ≈ 2 (matches K_n theoretical n-1)
  • Symmetric 2-edge (weight 5) → lambda ≈ 5
  • Empty graph → None

21 tests passing. Next: modularity + toy harness.

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings April 24, 2026 07:32
@AceHack AceHack enabled auto-merge (squash) April 24, 2026 07:32
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

…first detection primitive)

First cartel-detection primitive per the Graph ADR (PR #316).
Computes approximate lambda_1 (principal eigenvalue of the
symmetrized adjacency matrix) via standard power iteration with
L2 normalization + Rayleigh quotient.

Surface:
  Graph.largestEigenvalue
      (tolerance: double) (maxIterations: int) (g: Graph<'N>)
      : double option

Method:
- Build adjacency map from edge ZSet (coerce int64 weights to
  double; include negative weights as signed entries)
- Symmetrize: A_sym[i,j] = (A[i,j] + A[j,i]) / 2
- Start with all-ones vector (non-pathological seed; avoids
  zero-vector trap)
- Iterate v <- A_sym * v; v <- v / ||v||
- Stop when |lambda_k - lambda_{k-1}| / (|lambda_k| + eps) <
  tolerance or hit maxIterations
- Return Rayleigh quotient as lambda estimate

Cartel-detection use:
Sharp jump in lambda_1 between baseline graph and injected-
cartel graph indicates a dense subgraph formed. The 11th-ferry
/ 13th-ferry / 14th-ferry spec treats this as the first
trivial-cartel warning signal.

Performance note: dense Array2D adjacency for MVP. Suitable
for toy simulations (50-500 nodes). For larger graphs, Lanczos-
based incremental spectral method is a future graduation.

Tests (4 new, 21 total in GraphTests, all passing):
- None on empty graph
- Symmetric 2-edge (weight 5) graph -> lambda ≈ 5 (exact to
  1e-6)
- K3 triangle (weight 1) -> lambda ≈ 2 (K_n has lambda_1 =
  n-1)
- Cartel-injection test (the LOAD-BEARING one): baseline
  sparse 5-node graph vs. baseline + K_4 clique (weight 10).
  Attacked lambda >= 5x baseline lambda. This is the
  cartel-detection signal in action.

Provenance:
- Concept: Aaron (differentiable firefly network; first-order
  detection signal)
- Formalization: Amara (11th ferry signal-model §2 +
  13th ferry metrics §2 "lambda_1 growth" + 14th ferry
  "principal eigenvalue growth" alert row)
- Implementation: Otto (10th graduation)

Build: 0 Warning / 0 Error.

SPOF (per Otto-106): pure function; deterministic output for
same input (within floating-point). Caller threshold is the
sensitivity SPOF — too low -> false positives, too high ->
missed cartels. Mitigation documented: threshold should come
from baseline-null-distribution percentile, not hard-coded.
Future graduation: null-baseline calibration helper.

Toy cartel detector (Amara Otto-122 validation bar) prerequisite:
this is the first half. Next graduation: modularityScore +
toy harness combining both signals + 90%-detection-across-
1000-FsCheck-seeds property test.

Composes with:
- src/Core/Graph.fs skeleton (PR #317 merged main)
- src/Core/Graph.fs operators (PR #319 pending)
- src/Core/RobustStats.fs (PR #295) for outlier-resistant
  signal combination across many graph-pair comparisons

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@AceHack AceHack force-pushed the feat/graph-largest-eigenvalue-power-iteration branch from 4fadb36 to 9c96226 Compare April 24, 2026 07:34
@AceHack AceHack merged commit fe83f27 into main Apr 24, 2026
11 checks passed
@AceHack AceHack deleted the feat/graph-largest-eigenvalue-power-iteration branch April 24, 2026 07:35
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds the first spectral cartel-detection primitive to the ZSet-backed Graph<'N> substrate by implementing Graph.largestEigenvalue (λ₁) using power iteration over the symmetrized adjacency matrix, along with correctness and “cartel injection” tests.

Changes:

  • Implement Graph.largestEigenvalue (power iteration + Rayleigh quotient) on symmetrized adjacency.
  • Add unit tests for empty graph, simple known-spectrum graphs, and a cartel-clique injection signal test.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
src/Core/Graph.fs Adds Graph.largestEigenvalue implementation and extensive docstring describing the method and intended use.
tests/Tests.FSharp/Algebra/Graph.Tests.fs Adds test coverage for largestEigenvalue, including a load-bearing cartel-injection growth assertion.

Comment thread src/Core/Graph.fs
Comment on lines +201 to +206
/// **Performance note:** builds a dense
/// `IReadOnlyDictionary<'N, Dictionary<'N, double>>` as the
/// adjacency representation. Suitable for MVP / toy
/// simulations (50-500 nodes). For larger graphs, a
/// Lanczos-based incremental spectral method is the next
/// graduation; documented as future work.
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The docstring says this function “builds a dense IReadOnlyDictionary<'N, Dictionary<'N, double>>” adjacency representation, but the implementation actually builds dense Array2D matrices (adj and sym). Please update the docstring to match the actual representation (or change the implementation) so readers don’t optimize/extend based on incorrect assumptions.

Suggested change
/// **Performance note:** builds a dense
/// `IReadOnlyDictionary<'N, Dictionary<'N, double>>` as the
/// adjacency representation. Suitable for MVP / toy
/// simulations (50-500 nodes). For larger graphs, a
/// Lanczos-based incremental spectral method is the next
/// graduation; documented as future work.
/// **Performance note:** builds dense `Array2D<double>`
/// adjacency and symmetric-adjacency matrices for the
/// power iteration. Suitable for MVP / toy simulations
/// (50-500 nodes). For larger graphs, a Lanczos-based
/// incremental spectral method is the next graduation;
/// documented as future work.

Copilot uses AI. Check for mistakes.
Comment thread src/Core/Graph.fs
Comment on lines +208 to +210
/// Provenance: concept Aaron; formalization Amara (11th
/// ferry §2 + 13th ferry §2); implementation Otto (10th
/// graduation).
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

This provenance/attribution block introduces contributor names in a code comment. Repo convention is to avoid name attribution in code/docs/skills and use role references instead (see docs/AGENT-BEST-PRACTICES.md:284-292). Please rephrase this section to remove personal names.

Copilot uses AI. Check for mistakes.
Comment thread src/Core/Graph.fs
Comment on lines +283 to +285
if converged then Some lambda
else if iter >= maxIterations then Some lambda
else None
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

Return semantics/documentation don’t match: the docstring says the function returns None when it fails to converge within maxIterations, but the implementation returns Some lambda when iter >= maxIterations. Also, the final else None branch is unreachable. Please decide on the contract (return None on non-convergence, or update docs and remove the dead branch / consider returning a richer result indicating convergence).

Copilot uses AI. Check for mistakes.
Comment on lines +212 to +224
let baseline =
Graph.fromEdgeSeq [
(1, 2, 1L); (2, 1, 1L)
(3, 4, 1L); (4, 3, 1L)
(2, 5, 1L); (5, 2, 1L)
]
let cartelEdges =
[
for s in [6; 7; 8; 9] do
for t in [6; 7; 8; 9] do
if s <> t then yield (s, t, 10L)
]
let attacked = Graph.fromEdgeSeq (List.append [ (1, 2, 1L); (2, 1, 1L); (3, 4, 1L); (4, 3, 1L); (2, 5, 1L); (5, 2, 1L) ] cartelEdges)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

attacked duplicates the baseline edge list inline instead of reusing baseline (or a shared baselineEdges list). This makes the test harder to maintain and easy to accidentally diverge from the baseline scenario when edits happen.

Suggested change
let baseline =
Graph.fromEdgeSeq [
(1, 2, 1L); (2, 1, 1L)
(3, 4, 1L); (4, 3, 1L)
(2, 5, 1L); (5, 2, 1L)
]
let cartelEdges =
[
for s in [6; 7; 8; 9] do
for t in [6; 7; 8; 9] do
if s <> t then yield (s, t, 10L)
]
let attacked = Graph.fromEdgeSeq (List.append [ (1, 2, 1L); (2, 1, 1L); (3, 4, 1L); (4, 3, 1L); (2, 5, 1L); (5, 2, 1L) ] cartelEdges)
let baselineEdges =
[
(1, 2, 1L); (2, 1, 1L)
(3, 4, 1L); (4, 3, 1L)
(2, 5, 1L); (5, 2, 1L)
]
let baseline = Graph.fromEdgeSeq baselineEdges
let cartelEdges =
[
for s in [6; 7; 8; 9] do
for t in [6; 7; 8; 9] do
if s <> t then yield (s, t, 10L)
]
let attacked = Graph.fromEdgeSeq (List.append baselineEdges cartelEdges)

Copilot uses AI. Check for mistakes.
Comment on lines +231 to +234
// Baseline lambda on sparse 5-node graph is ~1 (max
// single-edge weight). Attacked lambda should be ~30
// (K_4 with weight 10 has lambda_1 = 3*10 = 30, since
// K_n has lambda_1 = n-1 scaled by weight).
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

This comment claims the baseline λ₁ is “~1 (max single-edge weight)”, but the baseline component with nodes 1-2-5 is a 3-node path whose top eigenvalue is √2 (~1.414) (and the 3-4 edge contributes 1). Please adjust the comment to avoid misleading future readers about expected magnitudes.

Suggested change
// Baseline lambda on sparse 5-node graph is ~1 (max
// single-edge weight). Attacked lambda should be ~30
// (K_4 with weight 10 has lambda_1 = 3*10 = 30, since
// K_n has lambda_1 = n-1 scaled by weight).
// Baseline lambda on this sparse 5-node graph is driven by
// the 3-node path 1-2-5, whose top eigenvalue is sqrt(2)
// (~1.414); the separate 3-4 edge contributes 1. Attacked
// lambda should be ~30 (K_4 with weight 10 has lambda_1 =
// 3*10 = 30, since K_n has lambda_1 = n-1 scaled by weight).

Copilot uses AI. Check for mistakes.
AceHack added a commit that referenced this pull request Apr 24, 2026
…urrect (supersedes #319 + #322) (#324)

PRs #319 (operator composition) and #322 (modularity) both hit
DIRTY state from positional rebase conflicts — each appended to
Graph.fs tail, and as main grew with PR #321 (largestEigenvalue),
the appends conflicted. Closed both and re-filed consolidated
fresh from main.

Ships (combining #319 + #322 content):

**Operator composition** (5 functions — ADR property 5):
- Graph.map : ('N -> 'M) -> Graph<'N> -> Graph<'M>
- Graph.filter : ('N * 'N -> bool) -> Graph<'N> -> Graph<'N>
- Graph.distinct : Graph<'N> -> Graph<'N>
- Graph.union : Graph<'N> -> Graph<'N> -> Graph<'N>
- Graph.difference : Graph<'N> -> Graph<'N> -> Graph<'N>

Each is 1-2 lines delegating to the corresponding ZSet operator.

**Modularity score** (Newman's Q formula):
- Graph.modularityScore : Map<'N, int> -> Graph<'N> -> double option
- Computes Q over symmetrized adjacency given a partition;
  nodes missing from partition treated as singleton-community

Tests (7 new in this consolidated ship, 28 total in GraphTests,
all passing):
- map relabels nodes
- filter keeps matching edges
- distinct collapses multi-edges + drops anti-edges
- union + difference round-trip restores original
- modularityScore returns None for empty graph
- modularityScore is high (>0.3) for well-separated two-K3
  communities bridged thin
- modularityScore is 0 for single-community K3 (no boundary,
  no structure; matches theory)

Build: 0 Warning / 0 Error.

Counts as the 9th + 11th graduation (originally ships #319 +
#322 that both got DIRTY). Consolidated to unblock the queue
with a single clean merge.

Superseded PRs (closed):
- #319 feat/graph-operator-composition-map-filter-distinct
- #322 feat/graph-modularity-score

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
AceHack added a commit that referenced this pull request Apr 24, 2026
… detector) (#328)

First full integration of the Graph detection pipeline: combines
largestEigenvalue (spectral growth) + labelPropagation (community
partition) + modularityScore (partition evaluation) into a single
scalar risk score.

Surface:
  Graph.coordinationRiskScore
      (alpha: double) (beta: double)
      (eigenTol: double) (eigenIter: int) (lpIter: int)
      (baseline: Graph<'N>) (attacked: Graph<'N>)
      : double option

Composite formula (MVP):
  risk = alpha * Δλ₁_rel + beta * ΔQ

where:
- Δλ₁_rel = (λ₁(attacked) - λ₁(baseline)) / max(λ₁(baseline), eps)
- ΔQ = Q(attacked, LP(attacked)) - Q(baseline, LP(baseline))

Both signals fire when a dense subgraph is injected: λ₁ grows
because the cartel adjacency has high leading eigenvalue; Q grows
because LP finds the cartel as its own community and Newman Q
evaluates that partition highly.

Weight defaults per Amara 17th-ferry initial priors:
- alpha = 0.5 spectral growth
- beta  = 0.5 modularity shift

Tests (3 new, 34 total in GraphTests, all passing):
- Empty graphs -> None
- Cartel injection -> composite > 1.0 (both signals fire)
- attacked == baseline -> composite near 0 (|score| < 0.2)

Calibration deferred (Amara Otto-132 Part 2 correction #4 — robust
statistics via median + MAD): this MVP uses raw linear weighting
over differences. Full CoordinationRiskScore with robust z-scores
over baseline null-distribution is a future graduation once
baseline-calibration machinery ships. RobustStats.robustAggregate
(PR #295) already provides the median-MAD machinery; just needs a
calibration harness to use it.

14th graduation under Otto-105 cadence. First full integration
ship using 4 Graph primitives composed together (λ₁ + LP +
modularity + composer).

Build: 0 Warning / 0 Error.

Provenance:
- Concept: Aaron (firefly network + trivial-cartel-detect) +
  Amara's composite-score formulations across 12th/13th/14th/
  17th ferries
- Implementation: Otto (14th graduation)

Composes with:
- Graph.largestEigenvalue (PR #321)
- Graph.labelPropagation (PR #326)
- Graph.modularityScore (PR #324)
- RobustStats.robustAggregate (PR #295) — for future robust
  variant

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
AceHack added a commit that referenced this pull request Apr 24, 2026
…ns tracked; 3 already shipped) (#330)

* ferry: Amara 17th absorb — Cartel-Lab Implementation Closure + 5.5 Verification (8 corrections tracked)

Two-part ferry: Amara's deep-research Implementation Closure for
Cartel-Lab + her own GPT-5.5 Thinking verification pass with 8
load-bearing corrections.

Otto correction-pass status (all 8 tracked):
1. λ₁(K₃) = 2 — ALREADY CORRECT PR #321 Otto-127 (independent
   convergence before verification arrived)
2. Modularity relational-not-absolute — ALREADY CORRECT PR #324
   Otto-128 (caught mid-tick via hand-calc)
3. Cohesion/Exclusivity/Conductance replace entropy-collapse —
   SHIPPED PR #329 Otto-135 (3 primitives + 6 tests)
4. Windowed stake covariance acceleration — FUTURE GRADUATION
5. Event-stream → phase pipeline for PLV — FUTURE GRADUATION
6. 'ZSet invertible' → 'deltas support retractions' — ADR
   ALREADY PHRASED CORRECTLY (PR #316 never claimed full invertibility)
7. KSK 'contract' → 'policy layer' — FILED BACKLOG PR #318
   Otto-124 (Max coord pending)
8. SOTA humility — DOC PHRASING (applied in new absorb docs)

Amara's proposed 3-PR split NOT adopted (Otto-105 small-
graduation cadence; content delivered across 7 ticks instead:
PRs #317, #321, #323, #324, #326, #328, #329).

Amara's proposed /cartel-lab/ folder NOT adopted (Otto-108
Conway's-Law: single-module-tree until interfaces harden).
Current Graph.fs + test-support split works.

Aaron's SharderInfoTheoreticTests flake flag (trailing Otto-132
note) filed as BACKLOG PR #327 Otto-133 — unrelated hygiene
item.

Amara's Otto-136 follow-up note: '#323 conceptually accepted,
do not canonicalize until sharder test is seed-locked/
recalibrated'. Acknowledged — #323 lives in tests/Simulation/
already (test-scoped); 'canonicalize' = future promotion to
src/Core/NetworkIntegrity/ per Amara's PR #3 split suggestion;
that's gated on #327 completion.

§33 archive header compliance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* lint: fix line-start PR-number header false-positive in 17th-ferry absorb

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
AceHack added a commit that referenced this pull request Apr 24, 2026
…correction) (#332)

Completes the input pipeline for TemporalCoordinationDetection.
phaseLockingValue (PR #298): PLV expects phases in radians but
didn't prescribe how events become phases. This ship fills the
gap.

17th graduation under Otto-105 cadence. Addresses Amara 17th-ferry
Part 2 correction #5: 'Without phase construction, PLV is just a
word.'

Surface (2 pure functions):
- PhaseExtraction.epochPhase : double -> double[] -> double[]
  Periodic-epoch phase. φ(t) = 2π · (t mod period) / period.
  Suited to consensus-protocol events with fixed cadence (slot
  duration, heartbeat, epoch boundary).
- PhaseExtraction.interEventPhase : double[] -> double[] -> double[]
  Circular phase between consecutive events. For sample t in
  [t_k, t_{k+1}), phase = 2π · (t - t_k) / (t_{k+1} - t_k).
  Suited to irregular event-driven streams.

Both return double[] of phase values in [0, 2π) radians. Empty
output on degenerate inputs (no exception). eventTimes assumed
sorted ascending; samples outside the event range get 0 phase
(callers filter to interior if they care).

Hilbert-transform analytic-signal approach (Amara's Option B)
deferred — needs FFT support which Zeta doesn't currently ship.
Future graduation when signal-processing substrate lands.

Tests (12, all passing):
epochPhase:
- t=0 → phase 0
- t=period/2 → phase π
- wraps cleanly at period boundary
- handles negative sample times correctly
- returns empty on invalid period (≤0) or empty samples

interEventPhase:
- empty on <2 events or empty samples
- phase 0 at start of first interval
- phase π at midpoint
- adapts to varying interval lengths (O(log n) binary search
  for bracketing interval)
- returns 0 before first and after last event (edge cases)

Composition with phaseLockingValue:
- Two nodes with identical epochPhase period → PLV = 1
  (synchronized)
- Two nodes with same period but constant offset → PLV = 1
  (perfect phase locking at non-zero offset is still locking)

This composes the full firefly-synchronization detection
pipeline end-to-end for event-driven validator streams:
  validator event times → PhaseExtraction → phaseLockingValue
  → temporal-coordination-detection signal

5 of 8 Amara 17th-ferry corrections now shipped:
#1 λ₁(K₃)=2 ✓ already correct (PR #321)
#2 modularity relational ✓ already correct (PR #324)
#3 cohesion/exclusivity/conductance ✓ shipped (PR #331)
#4 windowed stake covariance ✓ shipped (PR #331)
#5 event-stream → phase pipeline ✓ THIS SHIP
Remaining: #4 robust-z-score composite variant (future);
#6 ADR phrasing (already correct); #7 KSK naming (BACKLOG
#318 awaiting Max coord); #8 SOTA humility (doc-phrasing
discipline).

Build: 0 Warning / 0 Error.

Provenance:
- Concept: Aaron firefly-synchronization design
- Formalization: Amara 17th-ferry correction #5 with 3-option
  menu (epoch / Hilbert / circular)
- Implementation: Otto (17th graduation; options A + C shipped,
  Hilbert deferred)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants