-
Notifications
You must be signed in to change notification settings - Fork 1
sync: forward-port AceHack correctness/perf fixes (Graph.fs Gershgorin + RobustStats NaN guard + TemporalCoordination helper) #646
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -238,6 +238,31 @@ module Graph = | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| for j in 0 .. n - 1 do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sym.[i, j] <- (adj.[i, j] + adj.[j, i]) / 2.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Per Codex review on PR #26 (P1): plain power iteration | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // converges to the eigenpair with largest |λ|, not largest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // algebraic λ. For signed adjacencies (e.g. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // [[0,-2],[-2,0]] with eigenvalues +2/-2), magnitude alone | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // can return -2 — the wrong answer for largest-eigenvalue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // semantics. Fix: spectral shift A' = A + ρI where | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ρ = max_i Σ_j |A[i,j]| (∞-norm row-sum bound). By | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Gershgorin, every eigenvalue of symmetric A lies in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // [-ρ, +ρ], so A' has eigenvalues in [0, 2ρ] — all | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // non-negative — and largest-magnitude of A' = largest- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // algebraic of A' = largest-algebraic of A plus ρ. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Subtract ρ at the end. Negligible cost; correctness | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // is the win. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+241
to
+253
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Per Codex review on PR #26 (P1): plain power iteration | |
| // converges to the eigenpair with largest |λ|, not largest | |
| // algebraic λ. For signed adjacencies (e.g. | |
| // [[0,-2],[-2,0]] with eigenvalues +2/-2), magnitude alone | |
| // can return -2 — the wrong answer for largest-eigenvalue | |
| // semantics. Fix: spectral shift A' = A + ρI where | |
| // ρ = max_i Σ_j |A[i,j]| (∞-norm row-sum bound). By | |
| // Gershgorin, every eigenvalue of symmetric A lies in | |
| // [-ρ, +ρ], so A' has eigenvalues in [0, 2ρ] — all | |
| // non-negative — and largest-magnitude of A' = largest- | |
| // algebraic of A' = largest-algebraic of A plus ρ. | |
| // Subtract ρ at the end. Negligible cost; correctness | |
| // is the win. | |
| // Per the AceHack review that identified this bug (P1): | |
| // plain power iteration converges to the eigenpair with | |
| // largest |λ|, not largest algebraic λ. For signed | |
| // adjacencies (e.g. [[0,-2],[-2,0]] with eigenvalues | |
| // +2/-2), magnitude alone can return -2 — the wrong | |
| // answer for largest-eigenvalue semantics. Fix: spectral | |
| // shift A' = A + ρI where ρ = max_i Σ_j |A[i,j]| | |
| // (∞-norm row-sum bound). By Gershgorin, every | |
| // eigenvalue of symmetric A lies in [-ρ, +ρ], so A' has | |
| // eigenvalues in [0, 2ρ] — all non-negative — and | |
| // largest-magnitude of A' = largest-algebraic of A' = | |
| // largest-algebraic of A plus ρ. Subtract ρ at the end. | |
| // Negligible cost; correctness is the win. |
Copilot
AI
Apr 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P0: degenerate <- true on l2Norm av = 0.0 will return None for some valid signed matrices where the all-ones seed lands in the 0-eigenspace of the shifted matrix (i.e., an eigenvalue exactly -shift in the original). Example: A = -J (all -1 off-diagonals) has shift = n, and (A + shift·I)·1 = 0 even though λ_max(A) = 0. Instead of failing immediately, consider (a) shifting by shift + ε to keep the spectrum strictly positive, and/or (b) reseeding (e.g., a different deterministic seed vector) when the iterate is zero so largestEigenvalue doesn’t spuriously return None.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Retry eigen solve before returning None on degenerate shift
When A + ρI maps the all-ones seed to zero (for example A=[[0,-1],[-1,0]], where ρ=1), the loop marks degenerate and this branch returns None even though the largest eigenvalue is well-defined (λ_max=1). This is a regression from the intended correctness fix: signed graphs can now lose the spectral signal entirely, and coordinationRiskScore will treat them as undefined instead of scoring them. Please add a restart path (alternate seed/random seed) before falling back to None for nonzero-shift degenerate cases.
Useful? React with 👍 / 👎.
Copilot
AI
Apr 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
singletonBase = (min minLabel 0) - 1 can underflow if minLabel = Int32.MinValue (and even if it wraps, it can collide with caller-supplied labels again). Since community is only used for equality inside this function, consider using a non-int internal key (e.g., a struct/DU tag distinguishing “from partition” vs “singleton i”) to guarantee disjointness without relying on potentially-overflowing arithmetic.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -144,4 +144,12 @@ module RobustStats = | |
| | None -> None | ||
| | Some m -> | ||
| let scale = 1.4826 * max m MadFloor | ||
| Some ((measurement - med) / scale) | ||
| let z = (measurement - med) / scale | ||
| // Per Codex review on PR #26 (P2): if `measurement` | ||
| // is non-finite (NaN / ±Infinity), the ratio is also | ||
| // non-finite. Returning Some NaN propagates silently | ||
| // through downstream `coordinationRiskScore` arithmetic | ||
| // and corrupts the score. Match the same NaN-guard | ||
| // pattern as `TemporalCoordinationDetection`'s | ||
| // Pearson + circular-mean (Otto-358 fix). | ||
|
Comment on lines
+148
to
+154
|
||
| if Double.IsFinite z then Some z else None | ||
|
Comment on lines
+147
to
+155
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,6 +51,41 @@ module TemporalCoordinationDetection = | |
| /// `xs[i]`; negative `tau` aligns `ys[i]` with `xs[i - tau]`. | ||
| /// A detector asking "does `ys` lead `xs` by `k` steps?" passes | ||
| /// `tau = k`. | ||
| /// Internal: same algorithm as `crossCorrelation` but takes | ||
| /// already-materialized arrays. Avoids `Seq.toArray` re-walks | ||
| /// when called in a tight loop (e.g. by `crossCorrelationProfile`). | ||
| /// Per Codex review on PR #26 (TemporalCoordinationDetection.fs:100): | ||
| /// the public API materializes once, then this helper is used | ||
| /// for every lag. | ||
|
Comment on lines
+54
to
+59
|
||
| let private crossCorrelationArrays (xArr: double[]) (yArr: double[]) (tau: int) : double option = | ||
| 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 | ||
| let r = cov / sqrt (varX * varY) | ||
| if Double.IsFinite r then Some r else None | ||
|
|
||
| let crossCorrelation (xs: double seq) (ys: double seq) (tau: int) : double option = | ||
| let xArr = Seq.toArray xs | ||
| let yArr = Seq.toArray ys | ||
|
|
@@ -78,7 +113,14 @@ module TemporalCoordinationDetection = | |
| varX <- varX + dx * dx | ||
| varY <- varY + dy * dy | ||
| if varX = 0.0 || varY = 0.0 then None | ||
| else Some (cov / sqrt (varX * varY)) | ||
| else | ||
| // Guard against non-finite inputs (NaN/Infinity in upstream | ||
| // telemetry). Returning Some NaN would silently poison | ||
| // downstream detectors that treat Some as a valid measurement; | ||
| // None is the correct undefined-state signal. Per Codex review | ||
| // on PR #26. | ||
| let r = cov / sqrt (varX * varY) | ||
| if Double.IsFinite r then Some r else None | ||
|
Comment on lines
+117
to
+123
|
||
|
|
||
| /// Cross-correlation across the full lag range `[-maxLag, maxLag]`. | ||
| /// Returns one entry per lag; `None` entries indicate lags where | ||
|
|
@@ -89,8 +131,13 @@ module TemporalCoordinationDetection = | |
| let crossCorrelationProfile (xs: double seq) (ys: double seq) (maxLag: int) : (int * double option) array = | ||
| if maxLag < 0 then [||] | ||
| else | ||
| // Materialize once, then loop with crossCorrelationArrays | ||
| // so we don't re-walk the seq for every lag (Codex review | ||
| // PR #26: O(n*lags) → O(n + lags*overlap)). | ||
| let xArr = Seq.toArray xs | ||
| let yArr = Seq.toArray ys | ||
| [| for tau in -maxLag .. maxLag -> | ||
| tau, crossCorrelation xs ys tau |] | ||
| tau, crossCorrelationArrays xArr yArr tau |] | ||
|
|
||
| /// Epsilon floor for the magnitude of the phase-difference | ||
| /// mean-vector. Used by `meanPhaseOffset` and | ||
|
|
@@ -124,7 +171,16 @@ module TemporalCoordinationDetection = | |
| sumCos <- sumCos + cos d | ||
| sumSin <- sumSin + sin d | ||
| let n = double aArr.Length | ||
| Some (struct (sumCos / n, sumSin / n, aArr.Length)) | ||
| let meanCos = sumCos / n | ||
| let meanSin = sumSin / n | ||
| // Guard against non-finite phase inputs: cos/sin of NaN/Infinity | ||
| // produces NaN, which would propagate through PLV / phase-offset / | ||
| // phaseLockingWithOffset as Some NaN — undermining downstream | ||
| // gating that treats Some as valid evidence. None is the correct | ||
| // undefined-state signal. Per Codex review on PR #26. | ||
| if Double.IsFinite meanCos && Double.IsFinite meanSin then | ||
| Some (struct (meanCos, meanSin, aArr.Length)) | ||
| else None | ||
|
|
||
| /// **Phase-locking value (PLV)** — the magnitude of the mean | ||
| /// complex phase-difference vector between two phase series. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
largestEigenvalueimplementation is now explicitly handling signed-adjacency corner cases (spectral shift + degeneracy guard), but the existing test suite only covers non-negative examples (e.g. K3, symmetric positive edge). Please add regression tests for signed matrices (e.g. 2-node with negative edge weight, and a case whereλ_maxis 0 butλ_minis large-magnitude negative) to lock in the intended “largest algebraic eigenvalue” semantics.