From 8b4875a8c3f7243eeff6f52bc24e0e4a9cabb2a0 Mon Sep 17 00:00:00 2001 From: Shubham Singh Date: Wed, 8 Apr 2026 10:09:22 +0530 Subject: [PATCH 1/7] pulp-issue-test --- libpysal/graph/_matching.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/libpysal/graph/_matching.py b/libpysal/graph/_matching.py index a5d256c6b..a8ed45467 100644 --- a/libpysal/graph/_matching.py +++ b/libpysal/graph/_matching.py @@ -108,13 +108,15 @@ def _spatial_matching( mp = pulp.LpProblem("optimal-neargraph", sense=pulp.LpMinimize) # a match is as binary decision variable, connecting observation i to observation j - match_vars = pulp.LpVariable.dicts( - "match", - lowBound=0, - upBound=1, - indices=zip(row, col, strict=True), - cat="Continuous" if allow_partial_match else "Binary", - ) + match_vars = { + (i, j): pulp.LpVariable( + f"match_{i}_{j}", + lowBound=0, + upBound=1, + cat="Continuous" if allow_partial_match else "Binary", + ) + for i, j in zip(row, col, strict=True) + } # we want to minimize the geographic distance of links in the graph mp.objective = pulp.lpSum( [ From 1db241b17e1d0adf426eb2cf28884f64767b919d Mon Sep 17 00:00:00 2001 From: Shubham Singh Date: Wed, 8 Apr 2026 10:33:08 +0530 Subject: [PATCH 2/7] fix(graph): support PuLP v4 API for variable creation --- libpysal/graph/_matching.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/libpysal/graph/_matching.py b/libpysal/graph/_matching.py index a8ed45467..1b9ac1bd0 100644 --- a/libpysal/graph/_matching.py +++ b/libpysal/graph/_matching.py @@ -108,15 +108,14 @@ def _spatial_matching( mp = pulp.LpProblem("optimal-neargraph", sense=pulp.LpMinimize) # a match is as binary decision variable, connecting observation i to observation j - match_vars = { - (i, j): pulp.LpVariable( - f"match_{i}_{j}", - lowBound=0, - upBound=1, - cat="Continuous" if allow_partial_match else "Binary", - ) - for i, j in zip(row, col, strict=True) - } + match_vars = {} + for i, j in zip(row, col, strict=True): + name = f"match_{i}_{j}" + cat = "Continuous" if allow_partial_match else "Binary" + if hasattr(mp, "add_variable"): + match_vars[i, j] = mp.add_variable(name, lowBound=0, upBound=1, cat=cat) + else: + match_vars[i, j] = pulp.LpVariable(name, lowBound=0, upBound=1, cat=cat) # we want to minimize the geographic distance of links in the graph mp.objective = pulp.lpSum( [ From 665b9fdd9b8e0b0270c6c5bff7ff19ecd125a5e5 Mon Sep 17 00:00:00 2001 From: Shubham Singh Date: Wed, 8 Apr 2026 10:40:27 +0530 Subject: [PATCH 3/7] fix(graph): support PuLP v4 API for constraints --- libpysal/graph/_matching.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/libpysal/graph/_matching.py b/libpysal/graph/_matching.py index 1b9ac1bd0..62a590495 100644 --- a/libpysal/graph/_matching.py +++ b/libpysal/graph/_matching.py @@ -146,11 +146,16 @@ def _spatial_matching( ] ) sense = int(not allow_partial_match) - mp += pulp.LpConstraint(summand, sense=sense, rhs=n_matches) + if sense == 1: + mp += summand >= n_matches + elif sense == 0: + mp += summand == n_matches + else: + mp += summand <= n_matches if match_between: # but, we may choose to ignore some sources for i in range(n_sources): summand = pulp.lpSum([match_vars[j, i] for j in range(n_targets)]) - mp += pulp.LpConstraint(summand, sense=-1, rhs=n_matches) + mp += summand <= n_matches status = mp.solve(solver) From 333a3f7dd75ee9b99354239a75feab0536811948 Mon Sep 17 00:00:00 2001 From: Shubham Singh Date: Thu, 9 Apr 2026 20:55:35 +0530 Subject: [PATCH 4/7] switch-from-attributes-to-pulp-version --- libpysal/graph/_matching.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libpysal/graph/_matching.py b/libpysal/graph/_matching.py index 62a590495..14396636b 100644 --- a/libpysal/graph/_matching.py +++ b/libpysal/graph/_matching.py @@ -1,6 +1,7 @@ import warnings import numpy +from packaging.version import Version from sklearn.metrics import pairwise_distances from ._utils import _validate_geometry_input @@ -108,11 +109,12 @@ def _spatial_matching( mp = pulp.LpProblem("optimal-neargraph", sense=pulp.LpMinimize) # a match is as binary decision variable, connecting observation i to observation j + PULP4 = Version(pulp.__version__).major >= 4 match_vars = {} for i, j in zip(row, col, strict=True): name = f"match_{i}_{j}" cat = "Continuous" if allow_partial_match else "Binary" - if hasattr(mp, "add_variable"): + if PULP4: match_vars[i, j] = mp.add_variable(name, lowBound=0, upBound=1, cat=cat) else: match_vars[i, j] = pulp.LpVariable(name, lowBound=0, upBound=1, cat=cat) From 799e05827beb9e21ea1435847031a23cf5530895 Mon Sep 17 00:00:00 2001 From: Shubham Singh Date: Thu, 9 Apr 2026 20:57:15 +0530 Subject: [PATCH 5/7] lowercase-pulp4 --- libpysal/graph/_matching.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libpysal/graph/_matching.py b/libpysal/graph/_matching.py index 14396636b..ec02800e4 100644 --- a/libpysal/graph/_matching.py +++ b/libpysal/graph/_matching.py @@ -109,12 +109,12 @@ def _spatial_matching( mp = pulp.LpProblem("optimal-neargraph", sense=pulp.LpMinimize) # a match is as binary decision variable, connecting observation i to observation j - PULP4 = Version(pulp.__version__).major >= 4 + pulp4 = Version(pulp.__version__).major >= 4 match_vars = {} for i, j in zip(row, col, strict=True): name = f"match_{i}_{j}" cat = "Continuous" if allow_partial_match else "Binary" - if PULP4: + if pulp4: match_vars[i, j] = mp.add_variable(name, lowBound=0, upBound=1, cat=cat) else: match_vars[i, j] = pulp.LpVariable(name, lowBound=0, upBound=1, cat=cat) From 6a3849af44b2e2a80cdf55cf1d304e21d88ef85e Mon Sep 17 00:00:00 2001 From: Shubham Singh Date: Sat, 11 Apr 2026 10:28:38 +0530 Subject: [PATCH 6/7] PULPv4-to-PULP_GE_4 --- libpysal/graph/_matching.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libpysal/graph/_matching.py b/libpysal/graph/_matching.py index ec02800e4..e733cd2a1 100644 --- a/libpysal/graph/_matching.py +++ b/libpysal/graph/_matching.py @@ -109,12 +109,12 @@ def _spatial_matching( mp = pulp.LpProblem("optimal-neargraph", sense=pulp.LpMinimize) # a match is as binary decision variable, connecting observation i to observation j - pulp4 = Version(pulp.__version__).major >= 4 + PULP_GE_4 = Version(pulp.__version__).major >= 4 # noqa: N806 match_vars = {} for i, j in zip(row, col, strict=True): name = f"match_{i}_{j}" cat = "Continuous" if allow_partial_match else "Binary" - if pulp4: + if PULP_GE_4: match_vars[i, j] = mp.add_variable(name, lowBound=0, upBound=1, cat=cat) else: match_vars[i, j] = pulp.LpVariable(name, lowBound=0, upBound=1, cat=cat) From 1826dbd6b15163931ca012eee941f51c08c2b40f Mon Sep 17 00:00:00 2001 From: Shubham Singh Date: Sun, 12 Apr 2026 01:09:53 +0530 Subject: [PATCH 7/7] move-PULP_GE_4 --- libpysal/graph/_matching.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libpysal/graph/_matching.py b/libpysal/graph/_matching.py index e733cd2a1..d5e010560 100644 --- a/libpysal/graph/_matching.py +++ b/libpysal/graph/_matching.py @@ -70,6 +70,8 @@ def _spatial_matching( """ try: import pulp + + PULP_GE_4 = Version(pulp.__version__).major >= 4 # noqa: N806 except ImportError as error: raise ImportError("spatial matching requires the pulp library") from error if metric == "precomputed": @@ -109,7 +111,6 @@ def _spatial_matching( mp = pulp.LpProblem("optimal-neargraph", sense=pulp.LpMinimize) # a match is as binary decision variable, connecting observation i to observation j - PULP_GE_4 = Version(pulp.__version__).major >= 4 # noqa: N806 match_vars = {} for i, j in zip(row, col, strict=True): name = f"match_{i}_{j}"