Conversation
…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>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
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, andGraph.differenceas thin wrappers over ZSet operators. - Add
Graph.modularityScore(Newman modularity on symmetrized adjacency) returningdouble option. - Extend
Graph.Tests.fswith 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. |
| | Some c -> c | ||
| | None -> -(i + 1) |
There was a problem hiding this comment.
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).
| | Some c -> c | |
| | None -> -(i + 1) | |
| | Some c -> true, c | |
| | None -> false, i |
|
|
||
| /// **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. |
There was a problem hiding this comment.
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.
| /// 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. |
| 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 |
There was a problem hiding this comment.
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.
| 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 |
…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>
… 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>
…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>
…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>
…-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>
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).