Skip to content

core: Graph operator composition + modularityScore — consolidated (supersedes DIRTY #319 + #322)#324

Merged
AceHack merged 1 commit intomainfrom
feat/graph-operator-composition-and-modularity-consolidated
Apr 24, 2026
Merged

core: Graph operator composition + modularityScore — consolidated (supersedes DIRTY #319 + #322)#324
AceHack merged 1 commit intomainfrom
feat/graph-operator-composition-and-modularity-consolidated

Conversation

@AceHack
Copy link
Copy Markdown
Member

@AceHack AceHack commented Apr 24, 2026

Closes positional-rebase-conflict loop on PRs #319 and #322. Content re-applied on a fresh branch from current main. 28 tests passing.

Counts as 9th + 11th graduation (both originally stuck DIRTY).

…urrect (supersedes #319 + #322)

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>
Copilot AI review requested due to automatic review settings April 24, 2026 07:45
@AceHack AceHack enabled auto-merge (squash) April 24, 2026 07:45
@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.

@AceHack AceHack merged commit 7f40a22 into main Apr 24, 2026
13 checks passed
@AceHack AceHack deleted the feat/graph-operator-composition-and-modularity-consolidated branch April 24, 2026 07:47
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

Consolidates previously-stuck work from #319 and #322 by adding composable Graph operators (delegating to ZSet algebra) and implementing Newman modularity as a Graph metric, along with new unit tests.

Changes:

  • Add Graph.map, Graph.filter, Graph.distinct, Graph.union, and Graph.difference as thin wrappers over ZSet operators.
  • Add Graph.modularityScore (Newman modularity on symmetrized adjacency) returning double option.
  • Extend Graph.Tests.fs with coverage for the new operators and modularity behavior.

Reviewed changes

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

File Description
src/Core/Graph.fs Adds ZSet-backed operator composition helpers and implements modularityScore.
tests/Tests.FSharp/Algebra/Graph.Tests.fs Adds tests validating operator semantics and modularity scoring on small graphs.

Comment thread src/Core/Graph.fs
Comment on lines +353 to +354
| Some c -> c
| None -> -(i + 1)
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.

modularityScore assigns missing nodes to community ids -(i + 1). Because callers can legally use negative community labels in partition, this can collide and incorrectly treat unrelated nodes as being in the same community, contradicting the doc comment that missing nodes become singleton groups with unique labels. Consider generating fresh ids that are guaranteed not to overlap with any existing label (e.g., allocate from minLabel-1 downward, or map communities to an internal discriminated union like Known of int | Missing of int).

Suggested change
| Some c -> c
| None -> -(i + 1)
| Some c -> true, c
| None -> false, i

Copilot uses AI. Check for mistakes.
Comment thread src/Core/Graph.fs

/// **Modularity score (Q)** for a node partition. Newman's
/// formula over the symmetrized adjacency. Returns Some Q
/// in [-0.5, 1]; None for empty graph or zero total 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.

The doc comment claims modularityScore returns Q in [-0.5, 1], but Graph explicitly supports negative edge weights (anti-edges) and this implementation incorporates signed weights into A_sym/degrees. With signed weights, modularity is not generally bounded in that range. Either restrict/normalize the input to non-negative weights (and document/validate that), or relax the stated range to avoid misleading callers.

Suggested change
/// in [-0.5, 1]; None for empty graph or zero total weight.
/// for non-empty graphs with non-zero total symmetrized
/// weight; None for empty graphs or when the total
/// symmetrized weight is zero. Because `Graph` permits
/// signed edge weights and this computation incorporates
/// those signed weights directly, no fixed numeric bound
/// is guaranteed for the returned score.

Copilot uses AI. Check for mistakes.
Comment thread src/Core/Graph.fs
Comment on lines +350 to +358
let community i =
let node = nodeList.[i]
match Map.tryFind node partition with
| Some c -> c
| None -> -(i + 1)
let mutable q = 0.0
for i in 0 .. n - 1 do
for j in 0 .. n - 1 do
if community i = community j then
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.

modularityScore recomputes community i/community j (including a Map.tryFind) inside the nested for i/for j loops, which adds avoidable overhead on an already O(n²) routine. Precompute a communities: int[] once (and possibly a lookup from community id to node indices) before the double loop to keep the inner loop allocation/lookup-free.

Suggested change
let community i =
let node = nodeList.[i]
match Map.tryFind node partition with
| Some c -> c
| None -> -(i + 1)
let mutable q = 0.0
for i in 0 .. n - 1 do
for j in 0 .. n - 1 do
if community i = community j then
let communities =
Array.init n (fun i ->
let node = nodeList.[i]
match Map.tryFind node partition with
| Some c -> c
| None -> -(i + 1))
let mutable q = 0.0
for i in 0 .. n - 1 do
for j in 0 .. n - 1 do
if communities.[i] = communities.[j] then

Copilot uses AI. Check for mistakes.
AceHack added a commit that referenced this pull request Apr 24, 2026
…r composite cartel detection) (#326)

Simple label-propagation community detector. Each node starts in
its own community; each iteration, every node adopts the label
with greatest weighted neighbor-vote (ties broken by lowest
community id for determinism). Stops when no label changes or
maxIterations reached.

Surface:
  Graph.labelPropagation : int -> Graph<'N> -> Map<'N, int>

Trade-offs (documented in XML-doc):
- Fast: O(iterations × edges), no dense-matrix.
- Quality: below Louvain on complex structures; catches obvious
  dense cliques reliably (exactly the trivial-cartel-detect case).
- Determinism: tie-break by lowest id.
- NOT a replacement for Louvain; dependency-free first pass.

Composes with modularityScore (PR #324): LP produces partition,
modularity evaluates it. Full end-to-end pattern verified in
test labelPropagation produces partition consumable by
modularityScore — two K3 cliques bridged thin → Q > 0.3.

Tests (3 new, 31 total in GraphTests, all passing):
- Empty graph -> empty map
- Two K3 cliques converge to two labels (nodes within a clique
  share label)
- LP partition consumed by modularityScore yields Q > 0.3
  (cartel-detection pipeline correctness)

Amara Otto-132 17th-ferry observation: her proposed
'CoordinationRiskScore' combines λ₁ + ΔQ + covariance + sync +
exclusivity + influence. This graduation ships the ΔQ prerequisite
(partition from LP + Q from modularityScore). Remaining primitives
queued for future graduations per Otto-105 cadence.

Build: 0 Warning / 0 Error.

Provenance:
- 12th ferry §5 + 13th ferry §2 + 14th ferry alert row
- Implementation: Otto (13th graduation)

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>
AceHack added a commit that referenced this pull request Apr 24, 2026
…-ferry §B + §F + corrections #2 #7 #9 (#342)

Research-grade design doc for the Stage-2 rung of Amara's
corrected promotion ladder. Specifies: (a) placement under
src/Experimental/CartelLab/ (not src/Core/ — that's Stage 4);
(b) MetricVector type with PLV magnitude AND offset split
(correction #6); (c) INullModelGenerator interface +
Preserves/Avoids table columns; (d) IAttackInjector
forward-looking interface (Stage 3); (e) Wilson-interval
reporting contract with {successes, trials, lowerBound,
upperBound} schema (correction #2 — no more "~95% CI ±5%"
handwave); (f) RobustZScoreMode with Hybrid fallback
(correction #7 — percentile-rank when MAD < epsilon);
(g) explicit artifact-output layout under artifacts/
coordination-risk/ with five files + run-manifest.json
(correction #9).

6-stage promotion path (0 doc / 1 ADR / 2.a skeleton /
2.b full null-models + first attack / 3 attack suite /
4 Core/NetworkIntegrity / 5 Aurora-KSK) matches Amara's
corrected ladder and Otto-105 cadence.

Doc-only change; no code, no tests, no workflow, no
BACKLOG tail touch (avoids positional-conflict pattern
that cost #334#341 re-file this session).

This is the 7th of 10 18th-ferry operationalizations:
- #1/#10 test-classification (#339)
- #2 Wilson-interval design specified (this doc)
- #6 PLV phase-offset shipped (#340)
- #7 MAD=0 Hybrid mode specified (this doc)
- #9 artifact layout specified (this doc)
- #4 exclusivity already shipped (#331)
- #5 modularity relational already shipped (#324)

Remaining: Wilson-interval IMPLEMENTATION (waits on #323 +
Stage 2.a), MAD=0 Hybrid IMPLEMENTATION (waits on #333 +
Stage 2.a), conductance-sign doc (waits on #331), Stage-2.a
skeleton itself.

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