-
Notifications
You must be signed in to change notification settings - Fork 6
Fix non-deterministic behavior and incorrect variable selection in Carpanzano tearing algorithm #72
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
Merged
AayushSabharwal
merged 4 commits into
JuliaComputing:main
from
behzadd1992:fix/carpanzano-deterministic-tearing-bugfix
Apr 29, 2026
Merged
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
40fae5b
1. Fix non-determinism and Heuristic 2 logic bug in carpanzano_tearing
behzadd1992 ff3a9bb
Clean up comments in carpanzano_tearing.jl
behzadd1992 5c8c3cd
Refactor TestSystemStructure in carpanza_tearing.jl
behzadd1992 1b6c126
refactor(tearing): use OrderedSet instead of sort() for deterministic…
behzadd1992 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -92,7 +92,8 @@ function find_single_solvable_eq!( | |
| ) where {F} | ||
| (; graph, solvable_graph) = structure | ||
| nbors = nbors_buffer | ||
| for ieq in active_eqs | ||
| # Sort active_eqs iteration for deterministic equation selection regardless of hash order | ||
| for ieq in sort(collect(active_eqs)) | ||
| empty!(nbors) | ||
| append!(nbors, Iterators.filter(in(active_vars), 𝑠neighbors(graph, ieq))) | ||
| length(nbors) == 1 || continue | ||
|
|
@@ -161,7 +162,8 @@ function carpanzano_tear_scc!( | |
| # is the corresponding number of incident edges. | ||
| empty!(enodes_with_min_incidence) | ||
| min_incidence_cnt = typemax(Int) | ||
| for ieq in active_eqs | ||
| # Sort active_eqs for deterministic tie-breaking regardless of hash order | ||
| for ieq in sort(collect(active_eqs)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can also be removed if |
||
| cnt = count(in(active_vars), 𝑠neighbors(graph, ieq)) | ||
| cnt > min_incidence_cnt && continue | ||
| if cnt == min_incidence_cnt | ||
|
|
@@ -206,11 +208,14 @@ function carpanzano_tear_scc!( | |
| alg_var = 0 | ||
| max_incidence_cnt = typemin(Int) | ||
| min_solvable_cnt = typemax(Int) | ||
| for ivar in active_vars | ||
| # Sort active_vars for deterministic selection; also fix missing max/min updates | ||
| for ivar in sort(collect(active_vars)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The same applies for |
||
| cnt = count(in(active_eqs), 𝑑neighbors(graph, ivar)) | ||
| solvable_cnt = count(in(active_eqs), 𝑑neighbors(solvable_graph, ivar)) | ||
| if iszero(alg_var) || cnt > max_incidence_cnt || cnt == max_incidence_cnt && solvable_cnt < min_solvable_cnt | ||
| alg_var = ivar | ||
| max_incidence_cnt = cnt | ||
| min_solvable_cnt = solvable_cnt | ||
| end | ||
| end | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| # Tests for carpanzano_tearing.jl - determinism and Heuristic 2 correctness | ||
|
|
||
| using BipartiteGraphs | ||
| import Graphs: add_edge! | ||
|
|
||
| struct TestSystemStructure <: StateSelection.SystemStructure | ||
| graph::BipartiteGraph{Int,Nothing} | ||
| solvable_graph::BipartiteGraph{Int,Nothing} | ||
| var_to_diff::DiffGraph | ||
| eq_to_diff::DiffGraph | ||
| end | ||
|
|
||
| @testset "carpanzano_tearing" begin | ||
| @testset "Heuristic 2 picks variable with maximum incidence" begin | ||
| # Graph: 3 equations (source vertices), 3 variables (destination vertices). | ||
| # Every edge is also present in the solvable_graph (all variables solvable in | ||
| # all equations they appear in), so Heuristic 1 never finds a non-solvable | ||
| # variable and falls through to Heuristic 2. | ||
| # | ||
| # Incidence of each variable (number of equations it appears in): | ||
| # v1 (idx 1): eq1, eq2 => 2 | ||
| # v2 (idx 2): eq1, eq2, eq3 => 3 # maximum -> must be torn (algebraic) | ||
| # v3 (idx 3): eq1, eq3 => 2 | ||
| # | ||
| # Before the fix, max_incidence_cnt was never updated inside the Heuristic 2 | ||
| # loop, so every variable satisfied cnt > typemin(Int) and the last variable | ||
| # in hash-iteration order was chosen -- non-deterministic and wrong. | ||
| graph = BipartiteGraph(3, 3) | ||
| for (eq, var) in [(1,1),(1,2),(1,3),(2,1),(2,2),(3,2),(3,3)] | ||
| add_edge!(graph, eq, var) | ||
| end | ||
| solvable_graph = BipartiteGraph(3, 3) | ||
| for (eq, var) in [(1,1),(1,2),(1,3),(2,1),(2,2),(3,2),(3,3)] | ||
| add_edge!(solvable_graph, eq, var) | ||
| end | ||
|
|
||
| structure = TestSystemStructure( | ||
| graph, solvable_graph, | ||
| DiffGraph(3), DiffGraph(3) | ||
| ) | ||
| alg = StateSelection.CarpanzanoTearing() | ||
| # complete() makes invview(var_eq_matching) available, which the algorithm uses | ||
| # internally to find the variable matched to a solved equation. | ||
| var_eq_matching = complete( | ||
| Matching{Union{Unassigned, SelectedState}}(3), 3 | ||
| ) | ||
| active_vars = Set{Int}([1, 2, 3]) | ||
| active_eqs = Set{Int}([1, 2, 3]) | ||
|
|
||
| StateSelection.carpanzano_tear_scc!( | ||
| alg, structure, var_eq_matching, active_vars, active_eqs | ||
| ) | ||
|
|
||
| # v2 has the highest incidence (3 equations) -- Heuristic 2 must choose it as | ||
| # the algebraic (torn) variable, leaving it unmatched. | ||
| @test var_eq_matching[2] === unassigned | ||
| # v1 and v3 must each be matched to some equation. | ||
| @test var_eq_matching[1] isa Int | ||
| @test var_eq_matching[3] isa Int | ||
| end | ||
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
This would also be resolved if
active_eqsis made anOrderedSet{Int}instead ofSet{Int}, since then the equations are always iterated over in insertion order. This approach avoids two allocations fromcollectandsort.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.
Thanks for reviewing, Aayush!
I considered that direction as well, but in this code path
active_eqs/active_varsare populated from SCC data that itself comes fromSet/Dict-backed structures, so their insertion order is already hash-dependent. If we simply switch toOrderedSet{Int}, we'd just be preserving that hash-dependent insertion order and would still have the same non-determinism problem.To make
OrderedSeta drop-in replacement here, we'd also need to ensure those sets are constructed in a deterministic way (e.g. inserting elements in sorted order during SCC decomposition), which is a larger refactor touching the callers and the SCC building logic.Given that, I went with the explicit
sort(collect(...))as the minimal, local fix that:An
OrderedSet-based approach would be a worthwhile follow-up optimization, but only after auditing and sorting all insertion sites in the SCC decomposition.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.
I went back and traced the SCC construction more carefully, and I think your suggestion is actually safe to apply directly. I commited the suggested changes.
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.
Yep, we sort SCCs