deps: replace hopcroftkarp with scipy.sparse.csgraph.maximum_bipartite_matching - #107
deps: replace hopcroftkarp with scipy.sparse.csgraph.maximum_bipartite_matching#107sushovan4 wants to merge 1 commit into
Conversation
Closes the two changes requested in scikit-tda#106. `hopcroftkarp` is GPLv3 and was last released 2019-10-11, so no install carrying persim can offer a permissive dependency closure -- and since ripser depends on persim, that propagates across much of the scikit-tda stack. `scipy.sparse.csgraph.maximum_bipartite_matching` is Hopcroft-Karp, and scipy is already present in every working install via scikit-learn, though it was undeclared. So this removes a dependency and adds none: the scipy entry makes an existing requirement visible. The call site changes shape rather than name. hopcroftkarp took a dict of string-keyed adjacency sets and returned a dict holding both directions of every matched pair, so a perfect matching was tested as len(res) == 2 * n. scipy takes a CSR matrix and returns an array where res[i] is the column matched to row i, or -1 -- so the test becomes np.all(res >= 0), which is equivalent here because D is square. Building the CSR matrix from a boolean array drops non-edges rather than storing them as explicit zeros, which is what maximum_bipartite_matching reads. Bottleneck distances are unchanged: verified identical on the existing suite and on 40 randomly generated diagram pairs. The returned matching may differ where more than one is optimal. The distance is unique; the matching attaining it need not be, and the two routines break ties differently. On 17 of those 40 pairs the matching differs while the distance does not, and the row count can differ too, since a diagonal-to-diagonal row is dropped on the way out. Both remain valid: every point of both diagrams is accounted for exactly once and no pair exceeds the distance. test_matching_is_ optimal_when_several_are pins that contract on a pair where the choice is real, and deliberately does not pin the pairs themselves.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #107 +/- ##
==========================================
- Coverage 79.40% 78.83% -0.57%
==========================================
Files 20 20
Lines 1481 1479 -2
Branches 271 270 -1
==========================================
- Hits 1176 1166 -10
- Misses 229 235 +6
- Partials 76 78 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Closing this in favour of #108, which does the same swap and is the one to review. Apologies for the duplicate on your tracker — that was our error, not a disagreement about the approach. Everything in the description above applies to #108 as well, and two points are worth carrying over rather than losing with this thread: The returned matching can differ where more than one is optimal. The bottleneck distance is unique; the matching attaining it need not be, and the two routines break ties differently. On 40 randomly generated diagram pairs the distance was identical every time and the matching differed on 17 of them. The row count can differ too, since a diagonal-to-diagonal row is dropped on the way out. Both remain valid — every point of both diagrams accounted for exactly once, no pair exceeding the distance — but a consumer depending on the exact pairs rather than on the distance would see it. It is also faster, same distances throughout, measured on random pairs: 5.2x at 20 bars per diagram, 16.8x at 50, 26.9x at 100, 31.5x at 200. The old path rebuilt a Python dict of sets at every step of the binary search. Happy to fold the tie-breaking test into #108 if you would like it there. |
Closes the two changes requested in #106.
Why
hopcroftkarpis GPLv3 and was last released 2019-10-11 (three releases, single author), so no install carryingpersimcan offer a permissive dependency closure — and sinceripserdepends onpersim, that propagates across much of the scikit-tda stack.Why it adds nothing
scipy.sparse.csgraph.maximum_bipartite_matchingis Hopcroft–Karp, and scipy is already present in every working install via scikit-learn, though it was undeclared. Thescipyentry makes an existing requirement visible rather than introducing a new one;hopcroftkarpleaves. Net change to the closure is one package removed.The call site changes shape, not just name
hopcroftkarptook a dict of string-keyed adjacency sets and returned a dict holding both directions of every matched pair, so a perfect matching was tested aslen(res) == 2 * n. scipy takes a CSR matrix and returns an array whereres[i]is the column matched to rowi, or-1— so the test becomesnp.all(res >= 0), which is equivalent here becauseDis square. Building the CSR matrix from a boolean array drops non-edges rather than storing them as explicit zeros, which is whatmaximum_bipartite_matchingreads.Distances are unchanged
Verified identical across the existing suite and on 40 randomly generated diagram pairs.
The returned matching may differ, and I would rather flag it than have you find it
The bottleneck distance is unique; the optimal matching attaining it need not be, and the two routines break ties differently. On 17 of those 40 pairs the matching differed while the distance did not.
The row count can differ too. Where one routine sends a point to the diagonal and the other pairs it across, a diagonal-to-diagonal row is dropped on the way out, so the returned array can be shorter or longer. Both remain valid: every point of both diagrams is accounted for exactly once, and no pair exceeds the distance.
If any downstream consumer depends on the exact pairs rather than on the distance, this is the change that would reach it. Happy to adjust if you would rather preserve the previous tie-breaking exactly, though I think that would mean pinning an implementation detail rather than a contract.
test_matching_is_optimal_when_several_arepins the contract every optimal matching satisfies, on a pair where the choice is real rather than hypothetical, and deliberately does not pin the pairs themselves.Incidentally, it is faster
Same distances throughout, measured on random diagram pairs:
The old path rebuilt a Python dict of sets at every step of the binary search.
Verification
hopcroftkarpuninstalled, so nothing imports it residually.Left open
Happy to pin the scipy floor to whatever matches your convention for the rest of the requirements.
maximum_bipartite_matchinghas been inscipy.sparse.csgraphsince 1.4, so anything scikit-learn already implies would do.